fix(network): fix multiplayer uncapped FPS and add buffer HUD#230
Merged
Conversation
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.
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.
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.
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.
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.
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.
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.
Owner
Author
|
Just to document: we had to rollback the 'dynamic buffer' implementation. The SAGE engine's lockstep architecture does not handle artificial stalling well. When we paused the simulation to buffer frames, we stopped sending our own commands, causing the other client to also stall. This led to a feedback loop resulting in massive stuttering, game speed acceleration when the packets finally flooded in, and deadlocks on the end-game screen. The final implementation here restores the original pacing, limits the FPS properly to save GPU/CPU in multiplayer, and adds a passive HUD element to monitor the buffer size. |
fbraz3
marked this pull request as ready for review
July 22, 2026 00:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR initially attempted to implement a dynamic command buffer to absorb network jitter, but this proved incompatible with the engine's strict lockstep mechanism, causing severe stuttering, fast-forwarding, and game lockups on the victory screen.
The pacing and dynamic buffer changes have been reverted, restoring the original stable network tick behavior.
Changes (Final Implementation)
m_useFpsLimit = falseoverrides fromGameSpyGameInfo.cpp,StagingRoomGameInfo.cpp, andLANAPICallbacks.cpp. Players can now use their preferred frame limit (e.g. 60 FPS) in multiplayer, preventing the engine from rendering at uncapped 500+ FPS while the logic ticks at 30Hz.getBufferedFramesAvailable()toNetworkInterfaceand updatedW3DDisplay.cppto show the actual buffer usage in the debug HUD formatted asFPS: 60.00 (Buf: X). This is purely visual and does not affect simulation pacing.These changes apply to both
GeneralsMD(Zero Hour) andGenerals(Base Game) binaries.