Skip to content

tweak(gui): Decouple GUI transition and world animation timing from render update#2056

Open
bobtista wants to merge 7 commits into
TheSuperHackers:mainfrom
bobtista:bobtista/fix-gui-animation-timing
Open

tweak(gui): Decouple GUI transition and world animation timing from render update#2056
bobtista wants to merge 7 commits into
TheSuperHackers:mainfrom
bobtista:bobtista/fix-gui-animation-timing

Conversation

@bobtista
Copy link
Copy Markdown

@bobtista bobtista commented Jan 4, 2026

Summary

  • GUI window transitions now advance at a consistent rate regardless of frame rate
  • 2D world animation icons (healing, promotion, crate collection effects) rise at consistent speed regardless of frame rate

Changes

  • TransitionGroup::m_currentFrame changed from Int to Real to support fractional frame accumulation
  • TransitionGroup::update() scales frame advancement by TheFramePacer->getActualLogicTimeScaleOverFpsRatio()
  • InGameUI world animation Z-rise calculation now scales by the same time factor

Test plan

  • Verify GUI transitions (menu fades, button flashes) play at the same speed at 30fps and higher frame rates
  • Verify 2D world icons (healing icons, veteran stars, crate pickups) rise at consistent speed regardless of frame rate

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Feb 3, 2026

Greptile Summary

This PR decouples GUI transition state-machine advancement and world animation Z-rise from raw render frame count by introducing FramePacer-based scaling. The TransitionGroup change is well-implemented: a Real accumulator with a stepping loop replays every integer frame boundary, preserving all discrete states at both high and low fps. The InGameUI changes fix the too-fast rise at >30fps but use getActualLogicTimeScaleOverFpsRatio, which is capped at 1.0, leaving the <30fps case still frame-rate-dependent (the same gap that existed before the PR). The test plan explicitly scopes to 30fps+, so this is a known limitation rather than a regression.

Confidence Score: 5/5

Safe to merge — all findings are P2 observations about scope limitations, not regressions or new bugs.

Only P2 findings: the getActualLogicTimeScaleOverFpsRatio cap at 1.0 leaves sub-30fps behavior unchanged from before the PR (no regression), and the test plan explicitly limits scope to ≥30fps. No P0/P1 issues found.

Both InGameUI.cpp variants — if sub-30fps consistency is ever in scope, switching to getBaseOverUpdateFpsRatio() for Z-rise would be the fix.

Important Files Changed

Filename Overview
Core/GameEngine/Include/GameClient/GameWindowTransitions.h Changes m_currentFrame from Int to Real to support fractional frame accumulation; comment updated to clarify unit semantics.
Core/GameEngine/Source/GameClient/GUI/GameWindowTransitions.cpp Replaces single-frame increment with a getBaseOverUpdateFpsRatio()-scaled accumulator and a stepping loop that replays every integer frame boundary — correctly handles both low and high fps for discrete state-machine transitions.
Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp Z-rise now scaled by getActualLogicTimeScaleOverFpsRatio, which is capped at 1.0; corrects high-fps drift (>30fps) but leaves sub-30fps rise speed still frame-rate-dependent.
GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp Identical change to the Zero Hour variant — same cap-at-1.0 limitation applies for sub-30fps scenarios.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Render Frame] --> B[TransitionGroup::update]
    B --> C["timeScale = getBaseOverUpdateFpsRatio()\n(30 / renderFPS, floored at 5fps)"]
    C --> D["prevFrame = int(m_currentFrame)\nm_currentFrame += dirMul * timeScale\nnewFrame = int(m_currentFrame)"]
    D --> E{newFrame == prevFrame?}
    E -- Yes --> F[return early\nno state change]
    E -- No --> G["Step loop:\nfor frame = prevFrame+step to newFrame"]
    G --> H["tWin->update(frame)\nfor each TransitionWindow"]
    H --> I{isFinished?}
    I -- Yes --> J[break]
    I -- No --> G
    A --> K[InGameUI::updateAndDrawWorldAnimations]
    K --> L["zRiseTimeScale = getActualLogicTimeScaleOverFpsRatio()\nmin(1.0f, logicFPS / renderFPS)"]
    L --> M["wad->m_worldPos.z +=\nzRisePerSecond / LOGICFRAMES_PER_SECOND * zRiseTimeScale"]
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp:5680
**`getActualLogicTimeScaleOverFpsRatio` clamps at 1.0 — sub-30fps rise speed not fixed**

`getActualLogicTimeScaleOverFpsRatio` is implemented as `min(1.0f, logicFPS / renderFPS)`, so it never returns a value greater than 1.0. At render rates below `BaseFps` (30fps), the multiplier is locked to 1.0 and `updateAndDrawWorldAnimations` runs fewer than 30 times per second, so the net rise per second is still frame-rate-proportional (e.g., 15fps → 0.5× speed).

In contrast, `getBaseOverUpdateFpsRatio()` — used for GUI transitions — has no such cap (only a floor at 5fps) and would produce the correct compensating value (2.0 at 15fps) to keep the total rise consistent. The PR description says both systems use "the same time factor," but they use different methods with different bounds. If sub-30fps consistency is in scope, switching to `getBaseOverUpdateFpsRatio()` here would fully solve it.

### Issue 2 of 2
Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp:5507
**Same `getActualLogicTimeScaleOverFpsRatio` cap applies here**

Same issue as in `GeneralsMD`: `getActualLogicTimeScaleOverFpsRatio` clamps at 1.0, so z-rise is still frame-rate-dependent below 30fps. Consistent with the Zero Hour copy; worth resolving in both places together if sub-30fps behavior is addressed.

Reviews (5): Last reviewed commit: "fix(gui): Step every integer transition ..." | Re-trigger Greptile

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

6 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment thread Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowTransitions.cpp Outdated
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Feb 3, 2026

Additional Comments (1)

Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowTransitions.cpp
[P2] This file still uses NULL (TheTransitionHandler = NULL;). New code in this PR also adds FramePacer usage; consider switching to nullptr for null pointer literals to match the repo’s C++ style.

GameWindowTransitionsHandler *TheTransitionHandler = nullptr;

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: Generals/Code/GameEngine/Source/GameClient/GUI/GameWindowTransitions.cpp
Line: 62:62

Comment:
[P2] This file still uses `NULL` (`TheTransitionHandler = NULL;`). New code in this PR also adds `FramePacer` usage; consider switching to `nullptr` for null pointer literals to match the repo’s C++ style.

```suggestion
GameWindowTransitionsHandler *TheTransitionHandler = nullptr;
```

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

@xezon xezon added this to the Decouple logic and rendering milestone Feb 5, 2026
@Caball009
Copy link
Copy Markdown

I frequently find myself increasing the render frame rate at the menu because I find the window transitions too slow at 30 fps, and would probably like it better if they played close to ~1.75x the original speed. Is there anything this PR can do in that regard?

@ElTioRata
Copy link
Copy Markdown

I agree with Caball. It would be nice to have a setting to adjust GUI speed (x1.0... x1.25... x1.5x... x1.75... x2.00... and so on).

@xezon
Copy link
Copy Markdown

xezon commented Feb 23, 2026

Feature for scaling animation speed is unrelated to decoupling step and better be follow up change.

@xezon xezon added GUI For graphical user interface Minor Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour labels Feb 23, 2026
@githubawn
Copy link
Copy Markdown

Looks good.
Maybe fully skip animations under -quickstart in the follow-up, cleaner than adding explicit speed controls.

@bobtista bobtista force-pushed the bobtista/fix-gui-animation-timing branch from 276ed5c to e6cd525 Compare February 24, 2026 16:59
Comment thread Core/GameEngine/Include/GameClient/GameWindowTransitions.h
@Caball009
Copy link
Copy Markdown

There are still a couple of to-dos in the PR description.

Caball009
Caball009 previously approved these changes Mar 24, 2026
Copy link
Copy Markdown

@Caball009 Caball009 left a comment

Choose a reason for hiding this comment

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

Looks good to me.

I'm still in favor of making the float assignments explicit for m_currentFrame, but it makes no functional difference.

Copy link
Copy Markdown

@xezon xezon left a comment

Choose a reason for hiding this comment

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

I tested this and something appears to be broken.

Sometimes the UI just gets stuck. I was testing with long SSD load times because Visual Studio was parsing which bogs down the SSD.

After clicking Options menu

Image

After clicking Skirmish Menu

Image

After clicking Credits Menu

Image

@Caball009
Copy link
Copy Markdown

Sometimes the UI just gets stuck.

Is this something you can reproduce? I don't think I've seen anything like this during my testing.

@xezon
Copy link
Copy Markdown

xezon commented Mar 26, 2026

I ran into this several times when testing. My SSD was slow, but still it was unusual behavior. I can retest if this is not reproducible for others.

@Caball009
Copy link
Copy Markdown

I can retest if this is not reproducible for others.

Please do.

@Caball009
Copy link
Copy Markdown

I ran into this several times when testing. My SSD was slow, but still it was unusual behavior. I can retest if this is not reproducible for others.

Can you take a look at this? I cannot reproduce it, and this PR probably shouldn't be blocked from merging if you can't reproduce it either

@githubawn
Copy link
Copy Markdown

I replicated Xezon's bug by running this on a networked drive.
This would also probably break on SD installs.

The logic seems to fail on long IO stalls.

@githubawn
Copy link
Copy Markdown

githubawn commented Apr 10, 2026

Added a single sleep to replicate this, the menu won't even launch on this. But it does on main.

GameEngine.cpp

void GameEngine::execute()
{
#if defined(RTS_DEBUG)
	DWORD startTime = timeGetTime() / 1000;
#endif

	// pretty basic for now
	while( !m_quitting )
	{
		::Sleep(100);

30 will randomly bug out the menu after a few switches.

@Caball009 Caball009 dismissed their stale review April 10, 2026 20:26

Implementation needs changes.

@Caball009
Copy link
Copy Markdown

Caball009 commented Apr 12, 2026

Added a single sleep to replicate this, the menu won't even launch on this. But it does on main.

I just tried with the main branch + sleep and the menu doesn't load, so it doesn't seem like this PR is at fault.
Edit: I can confirm that main + sleep still works, but PR + sleep does not.

I suspect that the window transition code is too brittle to handle the sleep as it probably skips over hard-coded transitions.

@xezon
Copy link
Copy Markdown

xezon commented Apr 18, 2026

Yes the bug still happens. Got stuck here:

 	generalsv.exe!TransitionGroup::isFinished() Line 293	C++
>	generalsv.exe!GameLogic::startNewGame(bool saveGame) Line 1895	C++
 	generalsv.exe!GameLogic::update() Line 3108	C++
 	generalsv.exe!SubsystemInterface::UPDATE() Line 74	C++
 	generalsv.exe!GameEngine::update() Line 749	C++
 	generalsv.exe!Win32GameEngine::update() Line 94	C++
 	generalsv.exe!GameEngine::execute() Line 804	C++
 	generalsv.exe!GameMain() Line 59	C++
 	generalsv.exe!WinMain(HINSTANCE__ * hInstance, HINSTANCE__ * hPrevInstance, char * lpCmdLine, int nCmdShow) Line 874	C++
 	[Inline Frame] generalsv.exe!invoke_main() Line 102	C++
 	generalsv.exe!__scrt_common_main_seh() Line 288	C++
 	kernel32.dll!76385d49()	Unknown
 	[Frames below may be incorrect and/or missing, no symbols loaded for kernel32.dll]	
 	ntdll.dll!7767d83b()	Unknown
 	ntdll.dll!7767d7c1()	Unknown
image

I suspect it has something to do with FlashTransition::update where if the frame does not perfectly hit the expected value, the transition never finishes. This certainly is possible when the frame rate is not stable.

Comment thread Generals/Code/GameEngine/Source/GameClient/InGameUI.cpp Outdated
@Caball009
Copy link
Copy Markdown

@bobtista Can you take a look at the stuck menu issue?

it++;
}

if( isFinished() )
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This goes through the list a second time. This can probably be optimized by returning finished result in update function above and then use that to determine finished status.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Or just check isFinished for each transition group after each update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Gen Relates to Generals GUI For graphical user interface Minor Severity: Minor < Major < Critical < Blocker ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants