From 1986508d7cad508d95551314d6e7e08bb06b298a Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Mon, 20 Jul 2026 23:46:09 -0300 Subject: [PATCH 1/7] feat(network): implement dynamic command buffer Introduce a dynamic network buffer to decouple game logic ticks from network package arrival. This replaces the old strict lockstep 1-frame lookahead, which caused the simulation to stall on any minor network jitter. - Refactored GameEngine::canUpdateNetworkGameLogic() to consume frames based on real time accumulator instead of pure network readiness. - Added dynamic buffer tracking methods to NetworkInterface. - Removed legacy m_useFpsLimit = false overrides across StagingRoom, LANAPI, and GameSpy layers that bypassed the frame limiter in multiplayer. - Updated the network HUD to display the actual buffered frames count (getBufferedFramesAvailable()) formatted as (Buf: X). Changes applied to both Generals and GeneralsMD. --- .../Include/GameNetwork/NetworkInterface.h | 6 +++ .../GameSpy/StagingRoomGameInfo.cpp | 2 +- .../Source/GameNetwork/LANAPICallbacks.cpp | 2 +- .../GameEngine/Source/GameNetwork/Network.cpp | 51 ++++++++++++++----- .../GameEngine/Source/Common/GameEngine.cpp | 34 ++++++++++++- .../GameEngine/Source/GameClient/InGameUI.cpp | 4 +- .../Source/GameNetwork/GameSpyGameInfo.cpp | 2 - .../W3DDevice/GameClient/W3DDisplay.cpp | 7 ++- .../GameEngine/Source/Common/GameEngine.cpp | 34 ++++++++++++- .../GameEngine/Source/GameClient/InGameUI.cpp | 4 +- .../Source/GameNetwork/GameSpyGameInfo.cpp | 2 - .../W3DDevice/GameClient/W3DDisplay.cpp | 7 ++- 12 files changed, 125 insertions(+), 30 deletions(-) diff --git a/Core/GameEngine/Include/GameNetwork/NetworkInterface.h b/Core/GameEngine/Include/GameNetwork/NetworkInterface.h index b137260ecfa..94c037f3dc4 100644 --- a/Core/GameEngine/Include/GameNetwork/NetworkInterface.h +++ b/Core/GameEngine/Include/GameNetwork/NetworkInterface.h @@ -64,6 +64,12 @@ class NetworkInterface : public SubsystemInterface virtual void parseUserList( const GameInfo *game ) = 0; ///< Parse a userlist, creating connections virtual void startGame() = 0; ///< Sets the network game frame counter to -1 virtual UnsignedInt getRunAhead() = 0; ///< Get the current RunAhead value + + // GeneralsX @feature fbraz3 dynamic network buffer decoupling + virtual void setDynamicBufferSize(UnsignedInt size) = 0; + virtual UnsignedInt getDynamicBufferSize() = 0; + virtual UnsignedInt getBufferedFramesAvailable() = 0; + virtual UnsignedInt getFrameRate() = 0; ///< Get the current allowed frame rate. virtual UnsignedInt getPacketArrivalCushion() = 0; ///< Get the smallest packet arrival cushion since this was last called. diff --git a/Core/GameEngine/Source/GameNetwork/GameSpy/StagingRoomGameInfo.cpp b/Core/GameEngine/Source/GameNetwork/GameSpy/StagingRoomGameInfo.cpp index 8f4cf72c56f..9fa6f6d9197 100644 --- a/Core/GameEngine/Source/GameNetwork/GameSpy/StagingRoomGameInfo.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameSpy/StagingRoomGameInfo.cpp @@ -857,7 +857,7 @@ void GameSpyStagingRoom::launchGame() GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); msg->appendIntegerArgument(GAME_INTERNET); - TheWritableGlobalData->m_useFpsLimit = false; + // TheWritableGlobalData->m_useFpsLimit = false; // GeneralsX @bugfix Removed frame limiter override // Set the seeds InitRandom( getSeed() ); diff --git a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp index 2445271916f..d119ce49a6d 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp @@ -255,7 +255,7 @@ void LANAPI::OnGameStart() GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); msg->appendIntegerArgument(GAME_LAN); - TheWritableGlobalData->m_useFpsLimit = false; + // TheWritableGlobalData->m_useFpsLimit = false; // GeneralsX @bugfix Removed frame limiter override // Set the seeds InitRandom( m_currentGame->getSeed() ); diff --git a/Core/GameEngine/Source/GameNetwork/Network.cpp b/Core/GameEngine/Source/GameNetwork/Network.cpp index f45ff04812f..1f4d0cc9cac 100644 --- a/Core/GameEngine/Source/GameNetwork/Network.cpp +++ b/Core/GameEngine/Source/GameNetwork/Network.cpp @@ -113,6 +113,11 @@ class Network : public NetworkInterface virtual void setLocalAddress(UnsignedInt ip, UnsignedInt port) override; virtual UnsignedInt getRunAhead() override { return m_runAhead; } + + virtual void setDynamicBufferSize(UnsignedInt size) override { m_dynamicBufferSize = size; } + virtual UnsignedInt getDynamicBufferSize() override { return m_dynamicBufferSize; } + virtual UnsignedInt getBufferedFramesAvailable() override; + virtual UnsignedInt getFrameRate() override { return m_frameRate; } virtual UnsignedInt getPacketArrivalCushion() override; ///< Returns the smallest packet arrival cushion since this was last called. virtual Bool isFrameDataReady() override; @@ -209,6 +214,8 @@ class Network : public NetworkInterface // GeneralsX @bugfix BenderAI 13/02/2026 Use int64_t instead of __int64 (C99 standard, fighter19 pattern) int64_t m_perfCountFreq; ///< The frequency of the performance counter. + UnsignedInt m_dynamicBufferSize; + int64_t m_nextFrameTime; ///< When did we execute the last frame? For slugging the GameLogic... Bool m_frameDataReady; ///< Is the frame data for the next frame ready to be executed by TheGameLogic? @@ -339,6 +346,7 @@ void Network::init() m_frameDataReady = FALSE; m_isStalling = FALSE; m_didSelfSlug = FALSE; + m_dynamicBufferSize = 5; // Default value, will be adjustable later m_localStatus = NETLOCALSTATUS_PREGAME; @@ -589,6 +597,16 @@ Bool Network::AllCommandsReady(UnsignedInt frame) { return m_conMgr->allCommandsReady(frame);// && m_conMgr->allCRCsReady(frame); } +UnsignedInt Network::getBufferedFramesAvailable() { + UnsignedInt currentFrame = TheGameLogic->getFrame(); + UnsignedInt readyCount = 0; + // Limit the search so we don't loop forever if network is somehow way ahead + while(AllCommandsReady(currentFrame + readyCount) && readyCount < 100) { + readyCount++; + } + return readyCount; +} + /** * Take commands from the connection manager and put them on TheCommandList. * The commands need to be put on in the same order across all clients. @@ -730,26 +748,31 @@ void Network::update() endOfGameCheck(); } - if (AllCommandsReady(TheGameLogic->getFrame())) { // If all the commands are ready for the next frame... + UnsignedInt buffered = getBufferedFramesAvailable(); + UnsignedInt actualTargetBuffer = m_dynamicBufferSize; + + // GeneralsX @feature fbraz3 16/07/2026 Clamp target buffer by RunAhead so we don't stall permanently + if (actualTargetBuffer > m_runAhead) { + actualTargetBuffer = m_runAhead; + } + + if (m_isStalling) { + if (buffered >= actualTargetBuffer) { + m_isStalling = FALSE; + } + } else { + if (buffered == 0) { + m_isStalling = TRUE; + } + } + + if (!m_isStalling) { m_conMgr->handleAllCommandsReady(); -// DEBUG_LOG(("Network::update - frame %d is ready", TheGameLogic->getFrame())); if (timeForNewFrame()) { // This needs to come after any other pre-frame execution checks as this changes the timing variables. RelayCommandsToCommandList(TheGameLogic->getFrame()); // Put the commands for the next frame on TheCommandList. m_frameDataReady = TRUE; // Tell the GameEngine to run the commands for the new frame. } } - else { - // GeneralsX @bugfix GitHubCopilot 27/04/2026 Keep stall check in nanoseconds on Linux - int64_t curTime; - #ifdef _WIN32 - QueryPerformanceCounter((LARGE_INTEGER *)&curTime); - #else - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - curTime = static_cast(ts.tv_sec) * 1000000000 + ts.tv_nsec; - #endif - m_isStalling = curTime >= m_nextFrameTime; - } } void Network::liteupdate() { diff --git a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp index 6361bc18904..604d8118d9a 100644 --- a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp @@ -768,12 +768,44 @@ Bool GameEngine::canUpdateNetworkGameLogic() { DEBUG_ASSERTCRASH(TheNetwork != nullptr, ("TheNetwork is null")); + // Accumulate real time to keep pace with the renderer, just like regular game logic + const Int logicTimeScaleFps = TheFramePacer->getActualLogicTimeScaleFps(0); + if (logicTimeScaleFps > 0 && logicTimeScaleFps != RenderFpsPreset::UncappedFpsValue) + { + m_logicTimeAccumulator += TheFramePacer->getUpdateTime(); + } + if (TheNetwork->isFrameDataReady()) { // Important: The Network is definitely no longer stalling. TheFramePacer->setGameHalted(false); - return true; + if (logicTimeScaleFps <= 0) + { + return false; // Game logic is halted/frozen. + } + + if (logicTimeScaleFps == RenderFpsPreset::UncappedFpsValue) + { + return true; // Game logic steps map exactly 1-to-1 with the render frames. + } + + const Real logicTimeStepSeconds = TheFramePacer->getLogicTimeStepSeconds(0); + if (m_logicTimeAccumulator >= logicTimeStepSeconds) + { + m_logicTimeAccumulator -= logicTimeStepSeconds; + return true; + } + } + else + { + // Network is stalling. Cap the accumulated time so that when the network catches up, + // the game logic only speeds up for a brief period (1 second cap) to consume the buffer. + const Real maxAccumulatedTime = 1.0f; + if (m_logicTimeAccumulator > maxAccumulatedTime) + { + m_logicTimeAccumulator = maxAccumulatedTime; + } } return false; diff --git a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp index e969bc91be2..b433862a7d1 100644 --- a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -6006,12 +6006,12 @@ void InGameUI::updateRenderFpsString() void InGameUI::drawNetworkLatency(Int &x, Int &y) { - const UnsignedInt networkLatencyFrames = TheNetwork->getRunAhead(); + const UnsignedInt networkLatencyFrames = TheNetwork->getBufferedFramesAvailable(); if (networkLatencyFrames != m_lastNetworkLatencyFrames) { UnicodeString latencyStr; - latencyStr.format(L"%u", networkLatencyFrames); + latencyStr.format(L"(Buf: %u) ", networkLatencyFrames); m_networkLatencyString->setText(latencyStr); m_lastNetworkLatencyFrames = networkLatencyFrames; } diff --git a/Generals/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp b/Generals/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp index 708f90ce5cc..b2e7d0cc792 100644 --- a/Generals/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp +++ b/Generals/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp @@ -560,8 +560,6 @@ void GameSpyLaunchGame() GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); msg->appendIntegerArgument(GAME_INTERNET); - TheGlobalData->m_useFpsLimit = false; - // Set the random seed InitGameLogicRandom( TheGameSpyGame->getSeed() ); DEBUG_LOG(("InitGameLogicRandom( %d )", TheGameSpyGame->getSeed())); diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 13c61210fa8..3202fb6ccdc 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -1241,8 +1241,11 @@ void W3DDisplay::gatherDebugStats() } #endif - fpsString.format( L"FPS: %.2f", fps); - m_benchmarkDisplayString->setText( fpsString ); + if (TheNetwork != nullptr) { + UnicodeString bufStr; + bufStr.format(L" (Buf: %d)", TheNetwork->getBufferedFramesAvailable()); + unibuffer.concat(bufStr); + } Int polyPerFrame = Debug_Statistics::Get_DX8_Polygons(); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index bc4a3550bf9..e0d8dea6b2c 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -931,12 +931,44 @@ Bool GameEngine::canUpdateNetworkGameLogic() { DEBUG_ASSERTCRASH(TheNetwork != nullptr, ("TheNetwork is null")); + // Accumulate real time to keep pace with the renderer, just like regular game logic + const Int logicTimeScaleFps = TheFramePacer->getActualLogicTimeScaleFps(0); + if (logicTimeScaleFps > 0 && logicTimeScaleFps != RenderFpsPreset::UncappedFpsValue) + { + m_logicTimeAccumulator += TheFramePacer->getUpdateTime(); + } + if (TheNetwork->isFrameDataReady()) { // Important: The Network is definitely no longer stalling. TheFramePacer->setGameHalted(false); - return true; + if (logicTimeScaleFps <= 0) + { + return false; // Game logic is halted/frozen. + } + + if (logicTimeScaleFps == RenderFpsPreset::UncappedFpsValue) + { + return true; // Game logic steps map exactly 1-to-1 with the render frames. + } + + const Real logicTimeStepSeconds = TheFramePacer->getLogicTimeStepSeconds(0); + if (m_logicTimeAccumulator >= logicTimeStepSeconds) + { + m_logicTimeAccumulator -= logicTimeStepSeconds; + return true; + } + } + else + { + // Network is stalling. Cap the accumulated time so that when the network catches up, + // the game logic only speeds up for a brief period (1 second cap) to consume the buffer. + const Real maxAccumulatedTime = 1.0f; + if (m_logicTimeAccumulator > maxAccumulatedTime) + { + m_logicTimeAccumulator = maxAccumulatedTime; + } } return false; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index d8cc749800c..814c0b49cac 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -6141,12 +6141,12 @@ void InGameUI::updateRenderFpsString() void InGameUI::drawNetworkLatency(Int &x, Int &y) { - const UnsignedInt networkLatencyFrames = TheNetwork->getRunAhead(); + const UnsignedInt networkLatencyFrames = TheNetwork->getBufferedFramesAvailable(); if (networkLatencyFrames != m_lastNetworkLatencyFrames) { UnicodeString latencyStr; - latencyStr.format(L"%u", networkLatencyFrames); + latencyStr.format(L"(Buf: %u) ", networkLatencyFrames); m_networkLatencyString->setText(latencyStr); m_lastNetworkLatencyFrames = networkLatencyFrames; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp index b8386b756e2..5053fd5a9fc 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp @@ -560,8 +560,6 @@ void GameSpyLaunchGame() GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); msg->appendIntegerArgument(GAME_INTERNET); - TheGlobalData->m_useFpsLimit = false; - // Set the random seed InitGameLogicRandom( TheGameSpyGame->getSeed() ); DEBUG_LOG(("InitGameLogicRandom( %d )", TheGameSpyGame->getSeed())); diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 2974a4c2611..62194244220 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -1313,8 +1313,11 @@ void W3DDisplay::gatherDebugStats() } #endif - fpsString.format( L"FPS: %.2f", fps); - m_benchmarkDisplayString->setText( fpsString ); + if (TheNetwork != nullptr) { + UnicodeString bufStr; + bufStr.format(L" (Buf: %d)", TheNetwork->getBufferedFramesAvailable()); + unibuffer.concat(bufStr); + } Int polyPerFrame = Debug_Statistics::Get_DX8_Polygons(); From 905b4ce95008495100cdc2f4bd5a618e308e6e05 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Tue, 21 Jul 2026 17:34:32 -0300 Subject: [PATCH 2/7] fix(network): remove dual-pacing race condition causing desyncs Decouple Network timeForNewFrame pacing from GameEngine pacing. The network no longer attempts to pace the logic loop. Instead, the GameEngine now acts as the orchestrator, querying the network for ready frames and manually requesting consumption via consumeFrameData() immediately before updating GameLogic. This ensures a strict 1-to-1 sync without race conditions. --- .../Include/GameNetwork/NetworkInterface.h | 1 + .../GameEngine/Source/GameNetwork/Network.cpp | 79 +++---------------- .../GameEngine/Source/Common/GameEngine.cpp | 5 ++ .../GameEngine/Source/Common/GameEngine.cpp | 5 ++ 4 files changed, 22 insertions(+), 68 deletions(-) diff --git a/Core/GameEngine/Include/GameNetwork/NetworkInterface.h b/Core/GameEngine/Include/GameNetwork/NetworkInterface.h index 94c037f3dc4..3554d508b89 100644 --- a/Core/GameEngine/Include/GameNetwork/NetworkInterface.h +++ b/Core/GameEngine/Include/GameNetwork/NetworkInterface.h @@ -69,6 +69,7 @@ class NetworkInterface : public SubsystemInterface virtual void setDynamicBufferSize(UnsignedInt size) = 0; virtual UnsignedInt getDynamicBufferSize() = 0; virtual UnsignedInt getBufferedFramesAvailable() = 0; + virtual void consumeFrameData(UnsignedInt logicFrame) = 0; virtual UnsignedInt getFrameRate() = 0; ///< Get the current allowed frame rate. virtual UnsignedInt getPacketArrivalCushion() = 0; ///< Get the smallest packet arrival cushion since this was last called. diff --git a/Core/GameEngine/Source/GameNetwork/Network.cpp b/Core/GameEngine/Source/GameNetwork/Network.cpp index 1f4d0cc9cac..b6bd0e47366 100644 --- a/Core/GameEngine/Source/GameNetwork/Network.cpp +++ b/Core/GameEngine/Source/GameNetwork/Network.cpp @@ -117,6 +117,7 @@ class Network : public NetworkInterface virtual void setDynamicBufferSize(UnsignedInt size) override { m_dynamicBufferSize = size; } virtual UnsignedInt getDynamicBufferSize() override { return m_dynamicBufferSize; } virtual UnsignedInt getBufferedFramesAvailable() override; + virtual void consumeFrameData(UnsignedInt logicFrame) override; virtual UnsignedInt getFrameRate() override { return m_frameRate; } virtual UnsignedInt getPacketArrivalCushion() override; ///< Returns the smallest packet arrival cushion since this was last called. @@ -198,7 +199,6 @@ class Network : public NetworkInterface void processRunAheadCommand(NetRunAheadCommandMsg *msg); ///< Do what needs to be done when we get a new run ahead command. void processDestroyPlayerCommand(NetDestroyPlayerCommandMsg *msg); ///< Do what needs to be done when we need to destroy a player. void endOfGameCheck(); ///< Checks to see if its ok to leave this game. If it is, send the apropriate command to the game logic. - Bool timeForNewFrame(); ConnectionManager *m_conMgr; ///< The connection manager object @@ -218,7 +218,6 @@ class Network : public NetworkInterface int64_t m_nextFrameTime; ///< When did we execute the last frame? For slugging the GameLogic... - Bool m_frameDataReady; ///< Is the frame data for the next frame ready to be executed by TheGameLogic? Bool m_isStalling; // CRC info @@ -278,8 +277,8 @@ NetworkInterface *NetworkInterface::createNetwork() Network::Network() { m_checkCRCsThisFrame = FALSE; - m_didSelfSlug = FALSE; - m_frameDataReady = FALSE; + m_perfCountFreq = 0; + m_isStalling = FALSE; m_sawCRCMismatch = FALSE; m_conMgr = nullptr; @@ -342,8 +341,7 @@ void Network::init() m_runAhead = min(max(30, MIN_RUNAHEAD), MAX_FRAMES_AHEAD/2); ///< @todo: don't hard-code the run-ahead. m_frameRate = 30; m_lastExecutionFrame = m_runAhead - 1; // subtract 1 since we're starting on frame 0 - m_lastFrameCompleted = m_runAhead - 1; // subtract 1 since we're starting on frame 0 - m_frameDataReady = FALSE; + m_lastFrameCompleted = m_runAhead - 1; // subtract 1 since were starting on frame 0 m_isStalling = FALSE; m_didSelfSlug = FALSE; m_dynamicBufferSize = 5; // Default value, will be adjustable later @@ -642,6 +640,12 @@ void Network::RelayCommandsToCommandList(UnsignedInt frame) { deleteInstance(netcmdlist); } +void Network::consumeFrameData(UnsignedInt logicFrame) { + if (!m_isStalling) { + RelayCommandsToCommandList(logicFrame); + } +} + /** * This is where network commands that need to be executed on the same frame should be executed. */ @@ -721,9 +725,7 @@ void Network::update() // 1. Take Commands off TheCommandList, hand them off to the ConnectionManager. // 2. Call ConnectionManager->update; // 3. Check to see if all the commands for the next frame are there. -// 4. If all commands are there, put that frame's commands on TheCommandList. // - m_frameDataReady = FALSE; m_isStalling = FALSE; #if defined(RTS_DEBUG) @@ -768,10 +770,6 @@ void Network::update() if (!m_isStalling) { m_conMgr->handleAllCommandsReady(); - if (timeForNewFrame()) { // This needs to come after any other pre-frame execution checks as this changes the timing variables. - RelayCommandsToCommandList(TheGameLogic->getFrame()); // Put the commands for the next frame on TheCommandList. - m_frameDataReady = TRUE; // Tell the GameEngine to run the commands for the new frame. - } } } @@ -808,63 +806,8 @@ void Network::endOfGameCheck() { #endif } } - -Bool Network::timeForNewFrame() { - // GeneralsX @bugfix GitHubCopilot 27/04/2026 Keep network frame pacing in nanoseconds on Linux - int64_t curTime; - #ifdef _WIN32 - QueryPerformanceCounter((LARGE_INTEGER *)&curTime); - #else - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - curTime = static_cast(ts.tv_sec) * 1000000000 + ts.tv_nsec; - #endif - int64_t frameDelay = m_perfCountFreq / m_frameRate; - - /* - * If we're pushing up against the edge of our run ahead, we should slow the framerate down a bit - * to avoid being frozen by spikes in network lag. This will happen if another user's computer is - * running too far behind us, so we need to slow down to let them catch up. - */ - if (m_conMgr != nullptr) { - Real cushion = m_conMgr->getMinimumCushion(); - Real runAheadPercentage = m_runAhead * (TheGlobalData->m_networkRunAheadSlack / (Real)100.0); // If we are at least 50% into our slack, we need to slow down. - if (cushion < runAheadPercentage) { - int64_t oldFrameDelay = frameDelay; - frameDelay += oldFrameDelay / 10; // temporarily decrease the frame rate by 20%. -// DEBUG_LOG(("Average cushion = %f, run ahead percentage = %f. Adjusting frameDelay from %I64d to %I64d", cushion, runAheadPercentage, oldFrameDelay, frameDelay)); - m_didSelfSlug = TRUE; -// } else { -// DEBUG_LOG(("Average cushion = %f, run ahead percentage = %f", cushion, runAheadPercentage)); - } - } - - // Check to see if we can run another frame. - if (curTime >= m_nextFrameTime) { -// DEBUG_LOG(("Allowing a new frame, frameDelay = %I64d, curTime - m_nextFrameTime = %I64d", frameDelay, curTime - m_nextFrameTime)); - -// if (m_nextFrameTime + frameDelay < curTime) { - if ((m_nextFrameTime + (2 * frameDelay)) < curTime) { - // If we get too far behind on our framerate we need to reset the nextFrameTime thing. - m_nextFrameTime = curTime; -// DEBUG_LOG(("Initializing m_nextFrameTime to %I64d", m_nextFrameTime)); - } else { - // Set the soonest possible starting time for the next frame. - m_nextFrameTime += frameDelay; -// DEBUG_LOG(("m_nextFrameTime = %I64d", m_nextFrameTime)); - } - - return TRUE; - } -// DEBUG_LOG(("Slowing down frame rate. frame rate = %d, frame delay = %I64d, curTime - m_nextFrameTime = %I64d", m_frameRate, frameDelay, curTime - m_nextFrameTime)); - return FALSE; -} - -/** - * Returns true if the game commands for the next frame have been put on the command list. - */ Bool Network::isFrameDataReady() { - return (m_frameDataReady || (m_localStatus == NETLOCALSTATUS_LEFT)); + return (!m_isStalling || (m_localStatus == NETLOCALSTATUS_LEFT)); } Bool Network::isStalling() diff --git a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp index 604d8118d9a..7ff434e024e 100644 --- a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp @@ -883,6 +883,11 @@ void GameEngine::update() // TheSuperHackers @info Ignores frozen time because the script engine needs updating in the logic update regardless. if (canUpdateGameLogic(FramePacer::IgnoreFrozenTime)) { + if (TheNetwork != nullptr) + { + TheNetwork->consumeFrameData(TheGameLogic->getFrame()); + } + TheGameLogic->UPDATE(); if (!TheFramePacer->isTimeFrozen()) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index e0d8dea6b2c..38d38f7a1c3 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -1046,6 +1046,11 @@ void GameEngine::update() // TheSuperHackers @info Ignores frozen time because the script engine needs updating in the logic update regardless. if (canUpdateGameLogic(FramePacer::IgnoreFrozenTime)) { + if (TheNetwork != nullptr) + { + TheNetwork->consumeFrameData(TheGameLogic->getFrame()); + } + TheGameLogic->UPDATE(); if (!TheFramePacer->isTimeFrozen()) From 5c98605d453f145cb44316058f23e29f66af5d3d Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Tue, 21 Jul 2026 18:01:12 -0300 Subject: [PATCH 3/7] fix(network): remove m_isStalling reset that broke dynamic buffer m_isStalling was being reset to FALSE on every update() loop, destroying its persistence and causing the logic to stall for exactly one frame every time the buffer hit zero instead of properly waiting for actualTargetBuffer frames to accumulate. --- Core/GameEngine/Source/GameNetwork/Network.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/Core/GameEngine/Source/GameNetwork/Network.cpp b/Core/GameEngine/Source/GameNetwork/Network.cpp index b6bd0e47366..457b4b5a251 100644 --- a/Core/GameEngine/Source/GameNetwork/Network.cpp +++ b/Core/GameEngine/Source/GameNetwork/Network.cpp @@ -726,7 +726,6 @@ void Network::update() // 2. Call ConnectionManager->update; // 3. Check to see if all the commands for the next frame are there. // - m_isStalling = FALSE; #if defined(RTS_DEBUG) if (m_networkOn == FALSE) { From 6d99c9111c4d3d717afafddca160ec54f04d8aeb Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Tue, 21 Jul 2026 19:49:15 -0300 Subject: [PATCH 4/7] fix(network): revert broken dual-pacing fixes Revert "fix(network): remove dual-pacing race condition causing desyncs" Revert "fix(network): remove m_isStalling reset that broke dynamic buffer" The pacing changes broke the lockstep deterministic netcode and its frame pacing, causing the game to run at accelerated speed, stutter, and fail to progress past the post-game screen. --- .../Include/GameNetwork/NetworkInterface.h | 1 - .../GameEngine/Source/GameNetwork/Network.cpp | 80 ++++++++++++++++--- .../GameEngine/Source/Common/GameEngine.cpp | 5 -- .../GameEngine/Source/Common/GameEngine.cpp | 5 -- 4 files changed, 69 insertions(+), 22 deletions(-) diff --git a/Core/GameEngine/Include/GameNetwork/NetworkInterface.h b/Core/GameEngine/Include/GameNetwork/NetworkInterface.h index 3554d508b89..94c037f3dc4 100644 --- a/Core/GameEngine/Include/GameNetwork/NetworkInterface.h +++ b/Core/GameEngine/Include/GameNetwork/NetworkInterface.h @@ -69,7 +69,6 @@ class NetworkInterface : public SubsystemInterface virtual void setDynamicBufferSize(UnsignedInt size) = 0; virtual UnsignedInt getDynamicBufferSize() = 0; virtual UnsignedInt getBufferedFramesAvailable() = 0; - virtual void consumeFrameData(UnsignedInt logicFrame) = 0; virtual UnsignedInt getFrameRate() = 0; ///< Get the current allowed frame rate. virtual UnsignedInt getPacketArrivalCushion() = 0; ///< Get the smallest packet arrival cushion since this was last called. diff --git a/Core/GameEngine/Source/GameNetwork/Network.cpp b/Core/GameEngine/Source/GameNetwork/Network.cpp index 457b4b5a251..1f4d0cc9cac 100644 --- a/Core/GameEngine/Source/GameNetwork/Network.cpp +++ b/Core/GameEngine/Source/GameNetwork/Network.cpp @@ -117,7 +117,6 @@ class Network : public NetworkInterface virtual void setDynamicBufferSize(UnsignedInt size) override { m_dynamicBufferSize = size; } virtual UnsignedInt getDynamicBufferSize() override { return m_dynamicBufferSize; } virtual UnsignedInt getBufferedFramesAvailable() override; - virtual void consumeFrameData(UnsignedInt logicFrame) override; virtual UnsignedInt getFrameRate() override { return m_frameRate; } virtual UnsignedInt getPacketArrivalCushion() override; ///< Returns the smallest packet arrival cushion since this was last called. @@ -199,6 +198,7 @@ class Network : public NetworkInterface void processRunAheadCommand(NetRunAheadCommandMsg *msg); ///< Do what needs to be done when we get a new run ahead command. void processDestroyPlayerCommand(NetDestroyPlayerCommandMsg *msg); ///< Do what needs to be done when we need to destroy a player. void endOfGameCheck(); ///< Checks to see if its ok to leave this game. If it is, send the apropriate command to the game logic. + Bool timeForNewFrame(); ConnectionManager *m_conMgr; ///< The connection manager object @@ -218,6 +218,7 @@ class Network : public NetworkInterface int64_t m_nextFrameTime; ///< When did we execute the last frame? For slugging the GameLogic... + Bool m_frameDataReady; ///< Is the frame data for the next frame ready to be executed by TheGameLogic? Bool m_isStalling; // CRC info @@ -277,8 +278,8 @@ NetworkInterface *NetworkInterface::createNetwork() Network::Network() { m_checkCRCsThisFrame = FALSE; - m_perfCountFreq = 0; - + m_didSelfSlug = FALSE; + m_frameDataReady = FALSE; m_isStalling = FALSE; m_sawCRCMismatch = FALSE; m_conMgr = nullptr; @@ -341,7 +342,8 @@ void Network::init() m_runAhead = min(max(30, MIN_RUNAHEAD), MAX_FRAMES_AHEAD/2); ///< @todo: don't hard-code the run-ahead. m_frameRate = 30; m_lastExecutionFrame = m_runAhead - 1; // subtract 1 since we're starting on frame 0 - m_lastFrameCompleted = m_runAhead - 1; // subtract 1 since were starting on frame 0 + m_lastFrameCompleted = m_runAhead - 1; // subtract 1 since we're starting on frame 0 + m_frameDataReady = FALSE; m_isStalling = FALSE; m_didSelfSlug = FALSE; m_dynamicBufferSize = 5; // Default value, will be adjustable later @@ -640,12 +642,6 @@ void Network::RelayCommandsToCommandList(UnsignedInt frame) { deleteInstance(netcmdlist); } -void Network::consumeFrameData(UnsignedInt logicFrame) { - if (!m_isStalling) { - RelayCommandsToCommandList(logicFrame); - } -} - /** * This is where network commands that need to be executed on the same frame should be executed. */ @@ -725,7 +721,10 @@ void Network::update() // 1. Take Commands off TheCommandList, hand them off to the ConnectionManager. // 2. Call ConnectionManager->update; // 3. Check to see if all the commands for the next frame are there. +// 4. If all commands are there, put that frame's commands on TheCommandList. // + m_frameDataReady = FALSE; + m_isStalling = FALSE; #if defined(RTS_DEBUG) if (m_networkOn == FALSE) { @@ -769,6 +768,10 @@ void Network::update() if (!m_isStalling) { m_conMgr->handleAllCommandsReady(); + if (timeForNewFrame()) { // This needs to come after any other pre-frame execution checks as this changes the timing variables. + RelayCommandsToCommandList(TheGameLogic->getFrame()); // Put the commands for the next frame on TheCommandList. + m_frameDataReady = TRUE; // Tell the GameEngine to run the commands for the new frame. + } } } @@ -805,8 +808,63 @@ void Network::endOfGameCheck() { #endif } } + +Bool Network::timeForNewFrame() { + // GeneralsX @bugfix GitHubCopilot 27/04/2026 Keep network frame pacing in nanoseconds on Linux + int64_t curTime; + #ifdef _WIN32 + QueryPerformanceCounter((LARGE_INTEGER *)&curTime); + #else + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + curTime = static_cast(ts.tv_sec) * 1000000000 + ts.tv_nsec; + #endif + int64_t frameDelay = m_perfCountFreq / m_frameRate; + + /* + * If we're pushing up against the edge of our run ahead, we should slow the framerate down a bit + * to avoid being frozen by spikes in network lag. This will happen if another user's computer is + * running too far behind us, so we need to slow down to let them catch up. + */ + if (m_conMgr != nullptr) { + Real cushion = m_conMgr->getMinimumCushion(); + Real runAheadPercentage = m_runAhead * (TheGlobalData->m_networkRunAheadSlack / (Real)100.0); // If we are at least 50% into our slack, we need to slow down. + if (cushion < runAheadPercentage) { + int64_t oldFrameDelay = frameDelay; + frameDelay += oldFrameDelay / 10; // temporarily decrease the frame rate by 20%. +// DEBUG_LOG(("Average cushion = %f, run ahead percentage = %f. Adjusting frameDelay from %I64d to %I64d", cushion, runAheadPercentage, oldFrameDelay, frameDelay)); + m_didSelfSlug = TRUE; +// } else { +// DEBUG_LOG(("Average cushion = %f, run ahead percentage = %f", cushion, runAheadPercentage)); + } + } + + // Check to see if we can run another frame. + if (curTime >= m_nextFrameTime) { +// DEBUG_LOG(("Allowing a new frame, frameDelay = %I64d, curTime - m_nextFrameTime = %I64d", frameDelay, curTime - m_nextFrameTime)); + +// if (m_nextFrameTime + frameDelay < curTime) { + if ((m_nextFrameTime + (2 * frameDelay)) < curTime) { + // If we get too far behind on our framerate we need to reset the nextFrameTime thing. + m_nextFrameTime = curTime; +// DEBUG_LOG(("Initializing m_nextFrameTime to %I64d", m_nextFrameTime)); + } else { + // Set the soonest possible starting time for the next frame. + m_nextFrameTime += frameDelay; +// DEBUG_LOG(("m_nextFrameTime = %I64d", m_nextFrameTime)); + } + + return TRUE; + } +// DEBUG_LOG(("Slowing down frame rate. frame rate = %d, frame delay = %I64d, curTime - m_nextFrameTime = %I64d", m_frameRate, frameDelay, curTime - m_nextFrameTime)); + return FALSE; +} + +/** + * Returns true if the game commands for the next frame have been put on the command list. + */ Bool Network::isFrameDataReady() { - return (!m_isStalling || (m_localStatus == NETLOCALSTATUS_LEFT)); + return (m_frameDataReady || (m_localStatus == NETLOCALSTATUS_LEFT)); } Bool Network::isStalling() diff --git a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp index 7ff434e024e..604d8118d9a 100644 --- a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp @@ -883,11 +883,6 @@ void GameEngine::update() // TheSuperHackers @info Ignores frozen time because the script engine needs updating in the logic update regardless. if (canUpdateGameLogic(FramePacer::IgnoreFrozenTime)) { - if (TheNetwork != nullptr) - { - TheNetwork->consumeFrameData(TheGameLogic->getFrame()); - } - TheGameLogic->UPDATE(); if (!TheFramePacer->isTimeFrozen()) diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index 38d38f7a1c3..e0d8dea6b2c 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -1046,11 +1046,6 @@ void GameEngine::update() // TheSuperHackers @info Ignores frozen time because the script engine needs updating in the logic update regardless. if (canUpdateGameLogic(FramePacer::IgnoreFrozenTime)) { - if (TheNetwork != nullptr) - { - TheNetwork->consumeFrameData(TheGameLogic->getFrame()); - } - TheGameLogic->UPDATE(); if (!TheFramePacer->isTimeFrozen()) From 12844c98ac0c9ed67a2751fb0296a6742e3be951 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Tue, 21 Jul 2026 19:49:44 -0300 Subject: [PATCH 5/7] fix(network): revert broken dynamic buffer feature Revert "feat(network): implement dynamic command buffer" Dynamic buffering is fundamentally incompatible with the engine's strict RunAhead lockstep model, as forcing the game to wait for frames that the other client hasn't been allowed to simulate yet causes a deadlock and massive stuttering. --- .../Include/GameNetwork/NetworkInterface.h | 6 --- .../GameSpy/StagingRoomGameInfo.cpp | 2 +- .../Source/GameNetwork/LANAPICallbacks.cpp | 2 +- .../GameEngine/Source/GameNetwork/Network.cpp | 51 +++++-------------- .../GameEngine/Source/Common/GameEngine.cpp | 34 +------------ .../GameEngine/Source/GameClient/InGameUI.cpp | 4 +- .../Source/GameNetwork/GameSpyGameInfo.cpp | 2 + .../W3DDevice/GameClient/W3DDisplay.cpp | 7 +-- .../GameEngine/Source/Common/GameEngine.cpp | 34 +------------ .../GameEngine/Source/GameClient/InGameUI.cpp | 4 +- .../Source/GameNetwork/GameSpyGameInfo.cpp | 2 + .../W3DDevice/GameClient/W3DDisplay.cpp | 7 +-- 12 files changed, 30 insertions(+), 125 deletions(-) diff --git a/Core/GameEngine/Include/GameNetwork/NetworkInterface.h b/Core/GameEngine/Include/GameNetwork/NetworkInterface.h index 94c037f3dc4..b137260ecfa 100644 --- a/Core/GameEngine/Include/GameNetwork/NetworkInterface.h +++ b/Core/GameEngine/Include/GameNetwork/NetworkInterface.h @@ -64,12 +64,6 @@ class NetworkInterface : public SubsystemInterface virtual void parseUserList( const GameInfo *game ) = 0; ///< Parse a userlist, creating connections virtual void startGame() = 0; ///< Sets the network game frame counter to -1 virtual UnsignedInt getRunAhead() = 0; ///< Get the current RunAhead value - - // GeneralsX @feature fbraz3 dynamic network buffer decoupling - virtual void setDynamicBufferSize(UnsignedInt size) = 0; - virtual UnsignedInt getDynamicBufferSize() = 0; - virtual UnsignedInt getBufferedFramesAvailable() = 0; - virtual UnsignedInt getFrameRate() = 0; ///< Get the current allowed frame rate. virtual UnsignedInt getPacketArrivalCushion() = 0; ///< Get the smallest packet arrival cushion since this was last called. diff --git a/Core/GameEngine/Source/GameNetwork/GameSpy/StagingRoomGameInfo.cpp b/Core/GameEngine/Source/GameNetwork/GameSpy/StagingRoomGameInfo.cpp index 9fa6f6d9197..8f4cf72c56f 100644 --- a/Core/GameEngine/Source/GameNetwork/GameSpy/StagingRoomGameInfo.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameSpy/StagingRoomGameInfo.cpp @@ -857,7 +857,7 @@ void GameSpyStagingRoom::launchGame() GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); msg->appendIntegerArgument(GAME_INTERNET); - // TheWritableGlobalData->m_useFpsLimit = false; // GeneralsX @bugfix Removed frame limiter override + TheWritableGlobalData->m_useFpsLimit = false; // Set the seeds InitRandom( getSeed() ); diff --git a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp index d119ce49a6d..2445271916f 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp @@ -255,7 +255,7 @@ void LANAPI::OnGameStart() GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); msg->appendIntegerArgument(GAME_LAN); - // TheWritableGlobalData->m_useFpsLimit = false; // GeneralsX @bugfix Removed frame limiter override + TheWritableGlobalData->m_useFpsLimit = false; // Set the seeds InitRandom( m_currentGame->getSeed() ); diff --git a/Core/GameEngine/Source/GameNetwork/Network.cpp b/Core/GameEngine/Source/GameNetwork/Network.cpp index 1f4d0cc9cac..f45ff04812f 100644 --- a/Core/GameEngine/Source/GameNetwork/Network.cpp +++ b/Core/GameEngine/Source/GameNetwork/Network.cpp @@ -113,11 +113,6 @@ class Network : public NetworkInterface virtual void setLocalAddress(UnsignedInt ip, UnsignedInt port) override; virtual UnsignedInt getRunAhead() override { return m_runAhead; } - - virtual void setDynamicBufferSize(UnsignedInt size) override { m_dynamicBufferSize = size; } - virtual UnsignedInt getDynamicBufferSize() override { return m_dynamicBufferSize; } - virtual UnsignedInt getBufferedFramesAvailable() override; - virtual UnsignedInt getFrameRate() override { return m_frameRate; } virtual UnsignedInt getPacketArrivalCushion() override; ///< Returns the smallest packet arrival cushion since this was last called. virtual Bool isFrameDataReady() override; @@ -214,8 +209,6 @@ class Network : public NetworkInterface // GeneralsX @bugfix BenderAI 13/02/2026 Use int64_t instead of __int64 (C99 standard, fighter19 pattern) int64_t m_perfCountFreq; ///< The frequency of the performance counter. - UnsignedInt m_dynamicBufferSize; - int64_t m_nextFrameTime; ///< When did we execute the last frame? For slugging the GameLogic... Bool m_frameDataReady; ///< Is the frame data for the next frame ready to be executed by TheGameLogic? @@ -346,7 +339,6 @@ void Network::init() m_frameDataReady = FALSE; m_isStalling = FALSE; m_didSelfSlug = FALSE; - m_dynamicBufferSize = 5; // Default value, will be adjustable later m_localStatus = NETLOCALSTATUS_PREGAME; @@ -597,16 +589,6 @@ Bool Network::AllCommandsReady(UnsignedInt frame) { return m_conMgr->allCommandsReady(frame);// && m_conMgr->allCRCsReady(frame); } -UnsignedInt Network::getBufferedFramesAvailable() { - UnsignedInt currentFrame = TheGameLogic->getFrame(); - UnsignedInt readyCount = 0; - // Limit the search so we don't loop forever if network is somehow way ahead - while(AllCommandsReady(currentFrame + readyCount) && readyCount < 100) { - readyCount++; - } - return readyCount; -} - /** * Take commands from the connection manager and put them on TheCommandList. * The commands need to be put on in the same order across all clients. @@ -748,31 +730,26 @@ void Network::update() endOfGameCheck(); } - UnsignedInt buffered = getBufferedFramesAvailable(); - UnsignedInt actualTargetBuffer = m_dynamicBufferSize; - - // GeneralsX @feature fbraz3 16/07/2026 Clamp target buffer by RunAhead so we don't stall permanently - if (actualTargetBuffer > m_runAhead) { - actualTargetBuffer = m_runAhead; - } - - if (m_isStalling) { - if (buffered >= actualTargetBuffer) { - m_isStalling = FALSE; - } - } else { - if (buffered == 0) { - m_isStalling = TRUE; - } - } - - if (!m_isStalling) { + if (AllCommandsReady(TheGameLogic->getFrame())) { // If all the commands are ready for the next frame... m_conMgr->handleAllCommandsReady(); +// DEBUG_LOG(("Network::update - frame %d is ready", TheGameLogic->getFrame())); if (timeForNewFrame()) { // This needs to come after any other pre-frame execution checks as this changes the timing variables. RelayCommandsToCommandList(TheGameLogic->getFrame()); // Put the commands for the next frame on TheCommandList. m_frameDataReady = TRUE; // Tell the GameEngine to run the commands for the new frame. } } + else { + // GeneralsX @bugfix GitHubCopilot 27/04/2026 Keep stall check in nanoseconds on Linux + int64_t curTime; + #ifdef _WIN32 + QueryPerformanceCounter((LARGE_INTEGER *)&curTime); + #else + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + curTime = static_cast(ts.tv_sec) * 1000000000 + ts.tv_nsec; + #endif + m_isStalling = curTime >= m_nextFrameTime; + } } void Network::liteupdate() { diff --git a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp index 604d8118d9a..6361bc18904 100644 --- a/Generals/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/Generals/Code/GameEngine/Source/Common/GameEngine.cpp @@ -768,44 +768,12 @@ Bool GameEngine::canUpdateNetworkGameLogic() { DEBUG_ASSERTCRASH(TheNetwork != nullptr, ("TheNetwork is null")); - // Accumulate real time to keep pace with the renderer, just like regular game logic - const Int logicTimeScaleFps = TheFramePacer->getActualLogicTimeScaleFps(0); - if (logicTimeScaleFps > 0 && logicTimeScaleFps != RenderFpsPreset::UncappedFpsValue) - { - m_logicTimeAccumulator += TheFramePacer->getUpdateTime(); - } - if (TheNetwork->isFrameDataReady()) { // Important: The Network is definitely no longer stalling. TheFramePacer->setGameHalted(false); - if (logicTimeScaleFps <= 0) - { - return false; // Game logic is halted/frozen. - } - - if (logicTimeScaleFps == RenderFpsPreset::UncappedFpsValue) - { - return true; // Game logic steps map exactly 1-to-1 with the render frames. - } - - const Real logicTimeStepSeconds = TheFramePacer->getLogicTimeStepSeconds(0); - if (m_logicTimeAccumulator >= logicTimeStepSeconds) - { - m_logicTimeAccumulator -= logicTimeStepSeconds; - return true; - } - } - else - { - // Network is stalling. Cap the accumulated time so that when the network catches up, - // the game logic only speeds up for a brief period (1 second cap) to consume the buffer. - const Real maxAccumulatedTime = 1.0f; - if (m_logicTimeAccumulator > maxAccumulatedTime) - { - m_logicTimeAccumulator = maxAccumulatedTime; - } + return true; } return false; diff --git a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp index b433862a7d1..e969bc91be2 100644 --- a/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -6006,12 +6006,12 @@ void InGameUI::updateRenderFpsString() void InGameUI::drawNetworkLatency(Int &x, Int &y) { - const UnsignedInt networkLatencyFrames = TheNetwork->getBufferedFramesAvailable(); + const UnsignedInt networkLatencyFrames = TheNetwork->getRunAhead(); if (networkLatencyFrames != m_lastNetworkLatencyFrames) { UnicodeString latencyStr; - latencyStr.format(L"(Buf: %u) ", networkLatencyFrames); + latencyStr.format(L"%u", networkLatencyFrames); m_networkLatencyString->setText(latencyStr); m_lastNetworkLatencyFrames = networkLatencyFrames; } diff --git a/Generals/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp b/Generals/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp index b2e7d0cc792..708f90ce5cc 100644 --- a/Generals/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp +++ b/Generals/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp @@ -560,6 +560,8 @@ void GameSpyLaunchGame() GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); msg->appendIntegerArgument(GAME_INTERNET); + TheGlobalData->m_useFpsLimit = false; + // Set the random seed InitGameLogicRandom( TheGameSpyGame->getSeed() ); DEBUG_LOG(("InitGameLogicRandom( %d )", TheGameSpyGame->getSeed())); diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 3202fb6ccdc..13c61210fa8 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -1241,11 +1241,8 @@ void W3DDisplay::gatherDebugStats() } #endif - if (TheNetwork != nullptr) { - UnicodeString bufStr; - bufStr.format(L" (Buf: %d)", TheNetwork->getBufferedFramesAvailable()); - unibuffer.concat(bufStr); - } + fpsString.format( L"FPS: %.2f", fps); + m_benchmarkDisplayString->setText( fpsString ); Int polyPerFrame = Debug_Statistics::Get_DX8_Polygons(); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp index e0d8dea6b2c..bc4a3550bf9 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GameEngine.cpp @@ -931,44 +931,12 @@ Bool GameEngine::canUpdateNetworkGameLogic() { DEBUG_ASSERTCRASH(TheNetwork != nullptr, ("TheNetwork is null")); - // Accumulate real time to keep pace with the renderer, just like regular game logic - const Int logicTimeScaleFps = TheFramePacer->getActualLogicTimeScaleFps(0); - if (logicTimeScaleFps > 0 && logicTimeScaleFps != RenderFpsPreset::UncappedFpsValue) - { - m_logicTimeAccumulator += TheFramePacer->getUpdateTime(); - } - if (TheNetwork->isFrameDataReady()) { // Important: The Network is definitely no longer stalling. TheFramePacer->setGameHalted(false); - if (logicTimeScaleFps <= 0) - { - return false; // Game logic is halted/frozen. - } - - if (logicTimeScaleFps == RenderFpsPreset::UncappedFpsValue) - { - return true; // Game logic steps map exactly 1-to-1 with the render frames. - } - - const Real logicTimeStepSeconds = TheFramePacer->getLogicTimeStepSeconds(0); - if (m_logicTimeAccumulator >= logicTimeStepSeconds) - { - m_logicTimeAccumulator -= logicTimeStepSeconds; - return true; - } - } - else - { - // Network is stalling. Cap the accumulated time so that when the network catches up, - // the game logic only speeds up for a brief period (1 second cap) to consume the buffer. - const Real maxAccumulatedTime = 1.0f; - if (m_logicTimeAccumulator > maxAccumulatedTime) - { - m_logicTimeAccumulator = maxAccumulatedTime; - } + return true; } return false; diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index 814c0b49cac..d8cc749800c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -6141,12 +6141,12 @@ void InGameUI::updateRenderFpsString() void InGameUI::drawNetworkLatency(Int &x, Int &y) { - const UnsignedInt networkLatencyFrames = TheNetwork->getBufferedFramesAvailable(); + const UnsignedInt networkLatencyFrames = TheNetwork->getRunAhead(); if (networkLatencyFrames != m_lastNetworkLatencyFrames) { UnicodeString latencyStr; - latencyStr.format(L"(Buf: %u) ", networkLatencyFrames); + latencyStr.format(L"%u", networkLatencyFrames); m_networkLatencyString->setText(latencyStr); m_lastNetworkLatencyFrames = networkLatencyFrames; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp index 5053fd5a9fc..b8386b756e2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp @@ -560,6 +560,8 @@ void GameSpyLaunchGame() GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); msg->appendIntegerArgument(GAME_INTERNET); + TheGlobalData->m_useFpsLimit = false; + // Set the random seed InitGameLogicRandom( TheGameSpyGame->getSeed() ); DEBUG_LOG(("InitGameLogicRandom( %d )", TheGameSpyGame->getSeed())); diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 62194244220..2974a4c2611 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -1313,11 +1313,8 @@ void W3DDisplay::gatherDebugStats() } #endif - if (TheNetwork != nullptr) { - UnicodeString bufStr; - bufStr.format(L" (Buf: %d)", TheNetwork->getBufferedFramesAvailable()); - unibuffer.concat(bufStr); - } + fpsString.format( L"FPS: %.2f", fps); + m_benchmarkDisplayString->setText( fpsString ); Int polyPerFrame = Debug_Statistics::Get_DX8_Polygons(); From 6b5c365791558dcd35729494bdecc6726f2d70f8 Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Tue, 21 Jul 2026 19:54:54 -0300 Subject: [PATCH 6/7] feat(network): display network buffer size in HUD Adds getBufferedFramesAvailable() to NetworkInterface to allow the HUD to display how many frames are currently buffered and ready to simulate (e.g., 'FPS: 30.00 (Buf: 3)'). This doesn't affect the actual simulation pacing. --- .../GameEngine/Include/GameNetwork/NetworkInterface.h | 1 + Core/GameEngine/Source/GameNetwork/Network.cpp | 11 +++++++++++ .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 6 +++++- .../Source/W3DDevice/GameClient/W3DDisplay.cpp | 6 +++++- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/Core/GameEngine/Include/GameNetwork/NetworkInterface.h b/Core/GameEngine/Include/GameNetwork/NetworkInterface.h index b137260ecfa..fe8ad376095 100644 --- a/Core/GameEngine/Include/GameNetwork/NetworkInterface.h +++ b/Core/GameEngine/Include/GameNetwork/NetworkInterface.h @@ -64,6 +64,7 @@ class NetworkInterface : public SubsystemInterface virtual void parseUserList( const GameInfo *game ) = 0; ///< Parse a userlist, creating connections virtual void startGame() = 0; ///< Sets the network game frame counter to -1 virtual UnsignedInt getRunAhead() = 0; ///< Get the current RunAhead value + virtual UnsignedInt getBufferedFramesAvailable() = 0; virtual UnsignedInt getFrameRate() = 0; ///< Get the current allowed frame rate. virtual UnsignedInt getPacketArrivalCushion() = 0; ///< Get the smallest packet arrival cushion since this was last called. diff --git a/Core/GameEngine/Source/GameNetwork/Network.cpp b/Core/GameEngine/Source/GameNetwork/Network.cpp index f45ff04812f..eb5b1b58516 100644 --- a/Core/GameEngine/Source/GameNetwork/Network.cpp +++ b/Core/GameEngine/Source/GameNetwork/Network.cpp @@ -113,6 +113,7 @@ class Network : public NetworkInterface virtual void setLocalAddress(UnsignedInt ip, UnsignedInt port) override; virtual UnsignedInt getRunAhead() override { return m_runAhead; } + virtual UnsignedInt getBufferedFramesAvailable() override; virtual UnsignedInt getFrameRate() override { return m_frameRate; } virtual UnsignedInt getPacketArrivalCushion() override; ///< Returns the smallest packet arrival cushion since this was last called. virtual Bool isFrameDataReady() override; @@ -589,6 +590,16 @@ Bool Network::AllCommandsReady(UnsignedInt frame) { return m_conMgr->allCommandsReady(frame);// && m_conMgr->allCRCsReady(frame); } +UnsignedInt Network::getBufferedFramesAvailable() { + UnsignedInt currentFrame = TheGameLogic->getFrame(); + UnsignedInt readyCount = 0; + // Limit the search so we don't loop forever if network is somehow way ahead + while(AllCommandsReady(currentFrame + readyCount) && readyCount < 100) { + readyCount++; + } + return readyCount; +} + /** * Take commands from the connection manager and put them on TheCommandList. * The commands need to be put on in the same order across all clients. diff --git a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 13c61210fa8..857d7fe08f8 100644 --- a/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -1241,7 +1241,11 @@ void W3DDisplay::gatherDebugStats() } #endif - fpsString.format( L"FPS: %.2f", fps); + if (TheNetwork != nullptr) { + fpsString.format( L"FPS: %.2f (Buf: %d)", fps, TheNetwork->getBufferedFramesAvailable() ); + } else { + fpsString.format( L"FPS: %.2f", fps); + } m_benchmarkDisplayString->setText( fpsString ); Int polyPerFrame = Debug_Statistics::Get_DX8_Polygons(); diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp index 2974a4c2611..5978e1832bf 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp @@ -1313,7 +1313,11 @@ void W3DDisplay::gatherDebugStats() } #endif - fpsString.format( L"FPS: %.2f", fps); + if (TheNetwork != nullptr) { + fpsString.format( L"FPS: %.2f (Buf: %d)", fps, TheNetwork->getBufferedFramesAvailable() ); + } else { + fpsString.format( L"FPS: %.2f", fps); + } m_benchmarkDisplayString->setText( fpsString ); Int polyPerFrame = Debug_Statistics::Get_DX8_Polygons(); From a6d5a90e5ea3d80e03bf0726e6ff8c83109f56df Mon Sep 17 00:00:00 2001 From: Felipe Keller Braz Date: Tue, 21 Jul 2026 20:03:51 -0300 Subject: [PATCH 7/7] fix(network): remove FPS limiter override in multiplayer This restores the frame rate limiter (e.g. 30 FPS or 60 FPS) when playing multiplayer. Previously, the engine uncapped the FPS in GameSpy/LAN lobbies, causing it to render at 500+ FPS, burning GPU/CPU without actually speeding up the network simulation (which is locked to 30 Hz). By leaving m_useFpsLimit true, the engine uses the FPS cap from the options. --- .../Source/GameNetwork/GameSpy/StagingRoomGameInfo.cpp | 2 +- Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp | 2 +- Generals/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp | 2 +- .../Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Core/GameEngine/Source/GameNetwork/GameSpy/StagingRoomGameInfo.cpp b/Core/GameEngine/Source/GameNetwork/GameSpy/StagingRoomGameInfo.cpp index 8f4cf72c56f..b217b8b93e1 100644 --- a/Core/GameEngine/Source/GameNetwork/GameSpy/StagingRoomGameInfo.cpp +++ b/Core/GameEngine/Source/GameNetwork/GameSpy/StagingRoomGameInfo.cpp @@ -857,7 +857,7 @@ void GameSpyStagingRoom::launchGame() GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); msg->appendIntegerArgument(GAME_INTERNET); - TheWritableGlobalData->m_useFpsLimit = false; + // TheWritableGlobalData->m_useFpsLimit = false; // GeneralsX @bugfix fbraz3 21/07/2026 Keep FPS limiter active in multiplayer to avoid rendering at uncapped 500+ FPS // Set the seeds InitRandom( getSeed() ); diff --git a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp index 2445271916f..d1ec177a2a8 100644 --- a/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp +++ b/Core/GameEngine/Source/GameNetwork/LANAPICallbacks.cpp @@ -255,7 +255,7 @@ void LANAPI::OnGameStart() GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); msg->appendIntegerArgument(GAME_LAN); - TheWritableGlobalData->m_useFpsLimit = false; + // TheWritableGlobalData->m_useFpsLimit = false; // GeneralsX @bugfix fbraz3 21/07/2026 Keep FPS limiter active in multiplayer to avoid rendering at uncapped 500+ FPS // Set the seeds InitRandom( m_currentGame->getSeed() ); diff --git a/Generals/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp b/Generals/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp index 708f90ce5cc..c150e674fea 100644 --- a/Generals/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp +++ b/Generals/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp @@ -560,7 +560,7 @@ void GameSpyLaunchGame() GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); msg->appendIntegerArgument(GAME_INTERNET); - TheGlobalData->m_useFpsLimit = false; + // TheGlobalData->m_useFpsLimit = false; // GeneralsX @bugfix fbraz3 21/07/2026 Keep FPS limiter active in multiplayer to avoid rendering at uncapped 500+ FPS // Set the random seed InitGameLogicRandom( TheGameSpyGame->getSeed() ); diff --git a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp index b8386b756e2..e9c211c8258 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameNetwork/GameSpyGameInfo.cpp @@ -560,7 +560,7 @@ void GameSpyLaunchGame() GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_NEW_GAME ); msg->appendIntegerArgument(GAME_INTERNET); - TheGlobalData->m_useFpsLimit = false; + // TheGlobalData->m_useFpsLimit = false; // GeneralsX @bugfix fbraz3 21/07/2026 Keep FPS limiter active in multiplayer to avoid rendering at uncapped 500+ FPS // Set the random seed InitGameLogicRandom( TheGameSpyGame->getSeed() );