Skip to content

fix(network): fix multiplayer uncapped FPS and add buffer HUD#230

Merged
fbraz3 merged 7 commits into
mainfrom
feature/dynamic-network-buffer
Jul 22, 2026
Merged

fix(network): fix multiplayer uncapped FPS and add buffer HUD#230
fbraz3 merged 7 commits into
mainfrom
feature/dynamic-network-buffer

Conversation

@fbraz3

@fbraz3 fbraz3 commented Jul 21, 2026

Copy link
Copy Markdown
Owner

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)

  • FPS Limiter Fix: Removed legacy m_useFpsLimit = false overrides from GameSpyGameInfo.cpp, StagingRoomGameInfo.cpp, and LANAPICallbacks.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.
  • Network HUD: Added getBufferedFramesAvailable() to NetworkInterface and updated W3DDisplay.cpp to show the actual buffer usage in the debug HUD formatted as FPS: 60.00 (Buf: X). This is purely visual and does not affect simulation pacing.

These changes apply to both GeneralsMD (Zero Hour) and Generals (Base Game) binaries.

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.
fbraz3 added 6 commits July 21, 2026 17:34
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.
@fbraz3 fbraz3 changed the title feat(network): implement dynamic command buffer fix(network): fix multiplayer uncapped FPS and add buffer HUD Jul 22, 2026
@fbraz3

fbraz3 commented Jul 22, 2026

Copy link
Copy Markdown
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
fbraz3 marked this pull request as ready for review July 22, 2026 00:09
@fbraz3
fbraz3 merged commit 468d6e7 into main Jul 22, 2026
8 checks passed
@fbraz3
fbraz3 deleted the feature/dynamic-network-buffer branch July 22, 2026 00:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant