Skip to content

tweak: decouple GUI window-move animations from the render frame rate#2833

Open
bobtista wants to merge 2 commits into
TheSuperHackers:mainfrom
bobtista:bobtista/tweak/decouple-window-move-animation
Open

tweak: decouple GUI window-move animations from the render frame rate#2833
bobtista wants to merge 2 commits into
TheSuperHackers:mainfrom
bobtista:bobtista/tweak/decouple-window-move-animation

Conversation

@bobtista

Copy link
Copy Markdown

Addresses #2832, Similar to 2056, which decoupled the GameWindowTransitions.

Problem

Control bar, diplomacy/communicator panels slide animations go too fast when you increase render FPS

Approach

AnimateWindowManager::update() now paces itself by elapsed wall-clock time: it runs the number of base-rate (30 fps) steps matching the real time since the last update, carrying the fractional remainder between updates.

Wall-clock pacing is used here (rather than #2056's getBaseOverUpdateFpsRatio()) on purpose: AnimateWindowManager is pumped on two different clocks. The shell drives it through a fixed ~30 Hz wall-clock gate (Shell::update), while in-game callers drive it once per render frame. A render-frame ratio would double-decouple the already-gated shell path. Wall-clock self-pacing is correct for both with no per-call-site changes. A 6-step catch-up cap keeps a lon snapping an animation straight to its end.

TODO:

[x] Testing
[x] Replicate to Generals

@greptile-apps

greptile-apps Bot commented Jun 27, 2026

Copy link
Copy Markdown

Greptile Summary

This PR decouples AnimateWindowManager window-slide animations from the render frame rate by replacing the direct step() call in update() with a wall-clock-time accumulator that issues the correct number of base-rate (30 fps) steps per real-elapsed second.

  • Accumulator pattern: update() reads TheFramePacer->getUpdateTime(), clamps to 0.2 s, accumulates fractional 30-fps steps, and calls the new step() (the former update() body) that many times — keeping animation speed constant at any render FPS.
  • Catch-up cap: The 200 ms clamp (= 6 base-rate steps) prevents a long stall or alt-tab from snapping a panel directly to its end position.
  • Initialization: m_updateAccumulator is zeroed in the constructor, init(), and reset(), covering all lifecycle paths. Both the Generals/ and GeneralsMD/ trees are updated identically.

Confidence Score: 5/5

Safe to merge. The accumulator pattern is correctly implemented for the render-frame path, and both Generals/GeneralsMD trees are kept in sync.

The change is a straightforward accumulator extraction: the former update() body becomes step(), a float accumulator converts wall-clock deltas to discrete 30-fps ticks, the 0.2 s clamp prevents catch-up snapping, and the accumulator is zeroed in all three lifecycle methods. The approach correctly produces exactly one animation step per base-rate tick regardless of render FPS on the in-game path. No data corruption or crash paths are introduced.

No files require special attention.

Important Files Changed

Filename Overview
Generals/Code/GameEngine/Source/GameClient/GUI/AnimateWindowManager.cpp Adds wall-clock accumulator in update() and extracts step(); logic is correct for the render-frame path. Accumulator properly initialized across all lifecycle methods.
Generals/Code/GameEngine/Include/GameClient/AnimateWindowManager.h Adds private step() declaration and Real m_updateAccumulator member; no structural concerns.
GeneralsMD/Code/GameEngine/Source/GameClient/GUI/AnimateWindowManager.cpp Mirrors Generals/ changes exactly; same analysis applies.
GeneralsMD/Code/GameEngine/Include/GameClient/AnimateWindowManager.h Mirrors Generals/ header changes exactly; no concerns.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller as Render Frame or Shell Gate
    participant AWM as AnimateWindowManager
    participant FP as TheFramePacer
    participant Anim as ProcessAnimateWindow

    Caller->>AWM: update()
    AWM->>FP: getUpdateTime()
    FP-->>AWM: deltaSeconds
    AWM->>AWM: clamp to 0.2s max
    AWM->>AWM: "accumulator += deltaSeconds x 30"
    AWM->>AWM: "steps = floor(accumulator)"
    AWM->>AWM: "accumulator -= steps"
    loop Once per whole base-rate step
        AWM->>AWM: step()
        AWM->>Anim: updateAnimateWindow or reverseAnimateWindow
        Anim-->>AWM: finished or needsUpdate
    end
    AWM-->>Caller: return
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller as Render Frame or Shell Gate
    participant AWM as AnimateWindowManager
    participant FP as TheFramePacer
    participant Anim as ProcessAnimateWindow

    Caller->>AWM: update()
    AWM->>FP: getUpdateTime()
    FP-->>AWM: deltaSeconds
    AWM->>AWM: clamp to 0.2s max
    AWM->>AWM: "accumulator += deltaSeconds x 30"
    AWM->>AWM: "steps = floor(accumulator)"
    AWM->>AWM: "accumulator -= steps"
    loop Once per whole base-rate step
        AWM->>AWM: step()
        AWM->>Anim: updateAnimateWindow or reverseAnimateWindow
        Anim-->>AWM: finished or needsUpdate
    end
    AWM-->>Caller: return
Loading

Reviews (3): Last reviewed commit: "tweak: decouple GUI window-move animatio..." | Re-trigger Greptile

@xezon xezon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to use less AI code generation for better code

ProcessAnimateWindowSlideFromTopFast *m_slideFromTopFast; ///< holds the process in which the windows slide from the top,fast
ProcessAnimateWindow *getProcessAnimate( AnimTypes animType); ///< returns the process for the kind of animation we need.

void updateStep(); ///< Runs a single base-rate step of all registered window animations

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blank line after

ProcessAnimateWindowSlideFromTopFast *m_slideFromTopFast; ///< holds the process in which the windows slide from the top,fast
ProcessAnimateWindow *getProcessAnimate( AnimTypes animType); ///< returns the process for the kind of animation we need.

void updateStep(); ///< Runs a single base-rate step of all registered window animations

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

step() is simpler name.

m_updateAccumulator -= (Real)steps;

// Cap the catch-up burst so a long stall (load, alt-tab) cannot snap an animation instantly.
const Int maxSteps = 6;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 6 ?

void AnimateWindowManager::update()
{
const UnsignedInt now = timeGetTime();
if (m_lastUpdateTime == 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to get rid of this condition. Maybe set in init function?

ProcessAnimateWindowSlideFromTopFast *m_slideFromTopFast; ///< holds the process in which the windows slide from the top,fast
ProcessAnimateWindow *getProcessAnimate( AnimTypes animType); ///< returns the process for the kind of animation we need.

void updateStep(); ///< Runs a single base-rate step of all registered window animations

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not put function below member variables.

// render frame rate. The animation logic runs at the historic base rate; here we run the
// number of base-rate steps that match the elapsed wall-clock time, carrying the fractional
// remainder between updates. This is independent of how often update() is called, so callers
// pumped per render frame and callers throttled to a fixed rate both animate at the same speed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment is very long. Try simplify.

// pumped per render frame and callers throttled to a fixed rate both animate at the same speed.
void AnimateWindowManager::update()
{
const UnsignedInt now = timeGetTime();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid timeGetTime where possible. Use TheFramePacer->getUpdateTime()

@bobtista bobtista force-pushed the bobtista/tweak/decouple-window-move-animation branch from 76414cb to e5a23b5 Compare July 6, 2026 20:33
@bobtista bobtista force-pushed the bobtista/tweak/decouple-window-move-animation branch from e5a23b5 to e829b80 Compare July 6, 2026 20:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants