Skip to content

performance(Drawable): Adjusts Turret Positioning, Recoil and Muzzle for Model Draw to Update Only when Necessary - #2046

Open
IamInnocent3X wants to merge 8 commits into
TheSuperHackers:mainfrom
IamInnocent3X:W3DModelDrawPerfImprovements
Open

performance(Drawable): Adjusts Turret Positioning, Recoil and Muzzle for Model Draw to Update Only when Necessary#2046
IamInnocent3X wants to merge 8 commits into
TheSuperHackers:mainfrom
IamInnocent3X:W3DModelDrawPerfImprovements

Conversation

@IamInnocent3X

Copy link
Copy Markdown

Modifies the GameLogic to tell handleClientTurretPositioning() and handleClientRecoil() function in W3DModelDraw to only update when Necessary.

@Skyaero42 Skyaero42 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.

Any indication on the performance gain?

This may need extensive testing for mismatching.

//-------------------------------------------------------------------------------------------------
void W3DModelDraw::setNeedUpdateTurretPositioning(Bool set)
{
m_needUpdateTurretPosition = set; // A simple function with the dangers of an atomic bomb, misuse and it'll cause desync

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 not necessary.

if( xfer->getXferMode() == XFER_LOAD && m_subObjectVec.empty() == FALSE )
updateSubObjects();

#if !RETAIL_COMPATIBLE_CRC

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Does this require a xfer version change?

Comment thread GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp Outdated
@IamInnocent3X

IamInnocent3X commented Jan 2, 2026

Copy link
Copy Markdown
Author

Any indication on the performance gain?

I am not sure, but the devs does mention it being not a major problem.

Line 2494 @todo fix me someday (srj)

handleClientRecoil function are majority just For Loop Checks, each time checks with for loops based on how many Maximum Weapons are in game, (In this case, 3), and checks for each Weapon Slot whether there are any recoil to handle. If it does, it handles the Model Bone Recoil and handles their Muzzle from showing. Every Barrel Bone present will require checks.

handleClientTurretPositioning however, is O(n) based. It calls for all Turrets for each Weapon Slot to update Every Frame regardless if there are any changes. For each turret in the runtime it calls for AIUpdateInterface to get their Turret Pitch and Angle, then Transforms the Matrix up to 2 Times (One for Angle, One for Pitch). This is called every frame.

The changes just add a go/stop variable at the beginning of both functions, (m_doHandleRecoil, and m_needUpdateTurretPosition) to inform W3DModelDraw to allow the calling of the respective functions.

@stephanmeesters

Copy link
Copy Markdown

I think this can be simplified by removing the changes to xfer and instead forcing a recompute right after you load the savegame, like this (do test it):

diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp
  @@ void Object::loadPostProcess()
   	if( m_xferContainedByID != INVALID_ID )
   		m_containedBy = TheGameLogic->findObjectByID(m_xferContainedByID);
   	else
   		m_containedBy = NULL;
  +
  +	setNeedUpdateTurretPositioning(TRUE);
diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp b/GeneralsMD/Code/GameEngineDevice/
  Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp
  @@ void W3DModelDraw::loadPostProcess( void )
   	// extend base class
   	DrawModule::loadPostProcess();
  +
  +	m_needUpdateTurretPosition = TRUE;
  +	m_doHandleRecoil = TRUE;

This may change the CRC right after loading a savegame but I don't see how that would be a problem

@IamInnocent3X

Copy link
Copy Markdown
Author

I think this can be simplified by removing the changes to xfer and instead forcing a recompute right after you load the savegame, like this (do test it):

diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp
  @@ void Object::loadPostProcess()
   	if( m_xferContainedByID != INVALID_ID )
   		m_containedBy = TheGameLogic->findObjectByID(m_xferContainedByID);
   	else
   		m_containedBy = NULL;
  +
  +	setNeedUpdateTurretPositioning(TRUE);
diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp b/GeneralsMD/Code/GameEngineDevice/
  Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp
  @@ void W3DModelDraw::loadPostProcess( void )
   	// extend base class
   	DrawModule::loadPostProcess();
  +
  +	m_needUpdateTurretPosition = TRUE;
  +	m_doHandleRecoil = TRUE;

This may change the CRC right after loading a savegame but I don't see how that would be a problem

This change seems logical, tested on my end from loading a retail compatible save and it seems fine. I'll adjust and update my commit.

@Caball009 Caball009 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.

I don't think the turret positioning implementation is right.

I built a couple of firebases. I moved the camera somewhere else and then back to the firebases and the turrets snap to a certain angle. It looks like a purely visual thing, though, because it didn't cause a mismatch with a client that didn't have PR.

@IamInnocent3X

IamInnocent3X commented Jun 30, 2026

Copy link
Copy Markdown
Author

Alright, thanks for reporting. Pushed an update, it should fix it.

Edit: Nevermind, the issue still persist, just less noticable. I'm looking into it.

@IamInnocent3X
IamInnocent3X force-pushed the W3DModelDrawPerfImprovements branch from 3ea78f3 to 3e96ab1 Compare June 30, 2026 09:24
@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces a guard mechanism to skip handleClientTurretPositioning() and handleClientRecoil() when no turret or recoil animation is actively changing, reducing unnecessary bone updates each frame.

  • Adds m_needUpdateTurretPosition / m_lastNeedUpdateTurretPosition to W3DModelDraw with a one-trailing-frame pattern so bones settle to their final position before updates are gated off; TurretAI state machine sites (AimTurret, RecenterTurret, IdleScanState, setTurretTargetObject/Position, setTurretEnabled) now call Object::setNeedUpdateTurretPositioning to drive the flag.
  • Adds m_doHandleRecoil to W3DModelDraw, reset to FALSE at the top of handleClientRecoil() and re-armed to TRUE by any barrel still in RECOIL_START/RECOIL/SETTLE state, ensuring the function exits early only when every barrel is truly idle.
  • Three new if statements place their body on the same line as the condition, violating the team's style rule (rule 16b9b669); these are flagged as suggestions.

Confidence Score: 5/5

  • This PR is safe to merge. The optimization is well-scoped: updates only skip when every turret and every barrel is idle, and a one-trailing-frame mechanism ensures bones settle to their final position before gating off.
  • The flag propagation chain (TurretAI → Object → Drawable → W3DModelDraw) is correctly wired, the recoil accumulation pattern covers all active barrel states, and the change-detection dedup at the Object layer avoids redundant virtual dispatch. The only findings are style nits on three new if-statements.
  • No files require special attention beyond the three same-line if-statement style fixes already suggested.

Important Files Changed

Filename Overview
Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp Core of the optimization: adds dual-flag early-exit to handleClientTurretPositioning (one trailing frame to settle bones) and a reset-then-accumulate pattern for m_doHandleRecoil in handleClientRecoil. The recoil logic correctly covers RECOIL_START/RECOIL (via fall-through) and SETTLE states. Two style violations (same-line if bodies) in the new code.
GeneralsMD/Code/GameEngine/Source/GameLogic/AI/TurretAI.cpp Inserts setNeedUpdateTurretPositioning calls in every state that causes bone movement: setTurretTargetObject, setTurretTargetPosition, recenterTurret, setTurretEnabled, TurretAIAimTurretState, TurretAIRecenterTurretState, and TurretAIIdleScanState. One style rule violation (same-line if in setTurretEnabled).
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp Adds m_turretNeedPositioning (initialized FALSE) with a change-detection gate so the drawable is only notified when the boolean actually flips, avoiding redundant per-frame virtual dispatch through the draw module chain.
GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp Adds setNeedUpdateTurretPositioning that fans out to all draw modules via ObjectDrawInterface, guarded by the null-check on di. Straightforward delegation, no issues.
GeneralsMD/Code/GameEngine/Include/Common/DrawModule.h Adds setNeedUpdateTurretPositioning as a pure virtual on ObjectDrawInterface. Only W3DModelDraw inherits this interface, so no other concrete class is broken.
Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DModelDraw.h Adds the three new member booleans and the virtual override declaration. Clean header change.
GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h Adds m_turretNeedPositioning member and setNeedUpdateTurretPositioning declaration. Aligned with surrounding member declarations.
GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h Adds setNeedUpdateTurretPositioning declaration. Clean header change, no issues.

Sequence Diagram

sequenceDiagram
    participant TAI as TurretAI State Machine
    participant Obj as Object
    participant Drw as Drawable
    participant MDraw as W3DModelDraw

    Note over TAI,MDraw: Turret starts moving (e.g. new target)
    TAI->>Obj: setNeedUpdateTurretPositioning(TRUE)
    Obj->>Obj: m_turretNeedPositioning FALSE→TRUE (changed)
    Obj->>Drw: setNeedUpdateTurretPositioning(TRUE)
    Drw->>MDraw: setNeedUpdateTurretPositioning(TRUE)
    MDraw->>MDraw: "m_needUpdateTurretPosition=true, m_lastNeedUpdateTurretPosition=true"

    loop Each client frame (turret moving)
        MDraw->>MDraw: "handleClientTurretPositioning()<br/>m_need=true → runs update"
    end

    Note over TAI,MDraw: Turret reaches target angle
    TAI->>Obj: setNeedUpdateTurretPositioning(FALSE)
    Obj->>Obj: m_turretNeedPositioning TRUE→FALSE (changed)
    Obj->>Drw: setNeedUpdateTurretPositioning(FALSE)
    Drw->>MDraw: setNeedUpdateTurretPositioning(FALSE)
    MDraw->>MDraw: "m_needUpdateTurretPosition=false<br/>(m_last stays true)"

    MDraw->>MDraw: "handleClientTurretPositioning()<br/>m_need=false, m_last=true → runs trailing update<br/>sets m_last=false"

    MDraw->>MDraw: "handleClientTurretPositioning()<br/>m_need=false, m_last=false → early return ✓"

    Note over TAI,MDraw: Weapon fires — recoil path
    TAI->>MDraw: handleWeaponFireFX()
    MDraw->>MDraw: "m_doHandleRecoil=TRUE, state=RECOIL_START"

    loop Each frame while barrel active
        MDraw->>MDraw: "handleClientRecoil()<br/>reset m_doHandleRecoil=FALSE<br/>barrel in RECOIL→sets TRUE<br/>barrel in SETTLE→sets TRUE"
    end

    MDraw->>MDraw: "handleClientRecoil()<br/>SETTLE→IDLE: m_doHandleRecoil stays FALSE<br/>Next frame: early return ✓"
Loading

Reviews (5): Last reviewed commit: "Updated handleRecoil" | Re-trigger Greptile

Comment on lines +2426 to +2429
if(!m_needUpdateTurretPosition && !m_lastNeedUpdateTurretPosition)
return;

m_lastNeedUpdateTurretPosition = m_needUpdateTurretPosition;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Single Object flag spans all turret slots

m_needUpdateTurretPosition is one Bool for the entire object, but handleClientTurretPositioning loops over MAX_TURRETS. Any path that clears the flag (e.g. a two-turret unit where RecenterTurretState finishes for slot 1) suppresses updates for every other turret slot. A per-slot flag array, or a reference-counted active-turret counter, would prevent one turret's quiescence from silencing a sibling that is still rotating.

Prompt To Fix With AI
This is a comment left during a code review.
Path: GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp
Line: 2426-2429

Comment:
**Single Object flag spans all turret slots**

`m_needUpdateTurretPosition` is one `Bool` for the entire object, but `handleClientTurretPositioning` loops over `MAX_TURRETS`. Any path that clears the flag (e.g. a two-turret unit where `RecenterTurretState` finishes for slot 1) suppresses updates for every other turret slot. A per-slot flag array, or a reference-counted active-turret counter, would prevent one turret's quiescence from silencing a sibling that is still rotating.

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

@IamInnocent3X

IamInnocent3X commented Jun 30, 2026

Copy link
Copy Markdown
Author

Alright, thanks for reporting. Pushed an update, it should fix it.

Edit: Nevermind, the issue still persist, just less noticable. I'm looking into it.

Ignore the above, the issue is linked towards my GameClient Drawable Iteration Issue in another PR. That being said, without that Iteration feature even before the fix attempt, I cannot replicate the bug by moving the camera either by scrolling or clicking from the minimap. @Caball009 , able to send a video on replicating it?

@Caball009

Copy link
Copy Markdown

Here you go.

firebase_turret_angle_snap.mp4

@IamInnocent3X

IamInnocent3X commented Jul 1, 2026

Copy link
Copy Markdown
Author

Thank you, I have reattempted the fix as TurretAI's function expands beyond AIUpdate. I have also tested the fix on the current updated version of the SH Repo. Should be fine now.

@Caball009

Copy link
Copy Markdown

Nice, I'll try to make some time to test it later.

Can you rebase this branch?

@IamInnocent3X
IamInnocent3X force-pushed the W3DModelDrawPerfImprovements branch from 955c9e1 to b402a9b Compare July 6, 2026 07:34
@IamInnocent3X

Copy link
Copy Markdown
Author

Nice, I'll try to make some time to test it later.

Can you rebase this branch?

It's done. Sorry for the wait.

@IamInnocent3X
IamInnocent3X force-pushed the W3DModelDrawPerfImprovements branch from b402a9b to ab42a7e Compare July 6, 2026 07:38

@Skyaero42 Skyaero42 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.

It's done. Sorry for the wait.

It's not. Issue still persist. Please test your changes before submitting them

@IamInnocent3X

Copy link
Copy Markdown
Author

It's done. Sorry for the wait.

It's not. Issue still persist. Please test your changes before submitting them

Sorry about that. It should be fixed now.

https://youtu.be/riLCfKjc_fU

Comment thread GeneralsReplays
{
const W3DModelDrawModuleData* d = getW3DModelDrawModuleData();
if (!(m_curState->m_validStuff & ModelConditionInfo::BARRELS_VALID))
if (!(m_curState->m_validStuff & ModelConditionInfo::BARRELS_VALID) || !m_doHandleRecoil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Needs a null guard for m_curState

@IamInnocent3X
IamInnocent3X force-pushed the W3DModelDrawPerfImprovements branch from ae2357e to 0815b77 Compare August 28, 2026 04:27
@stephanmeesters

Copy link
Copy Markdown

I think the current solution can be bug prone and hard to maintain because if you forget to set the update flag at some point then you can break visuals in a suble way. The extra complexity can be worth it if the performance benefit is there but we don’t have raw numbers yet.

Perhaps use the Tracy profiler to get measurements of the handleClientRecoil and handleClientTurretPositioning functions (put a PROFILER_SECTION; at the top of the function and see project README how to enable profiler)

@IamInnocent3X

IamInnocent3X commented Aug 28, 2026

Copy link
Copy Markdown
Author

I think the current solution can be bug prone and hard to maintain because if you forget to set the update flag at some point then you can break visuals in a suble way. The extra complexity can be worth it if the performance benefit is there but we don’t have raw numbers yet.

Perhaps use the Tracy profiler to get measurements of the handleClientRecoil and handleClientTurretPositioning functions (put a PROFILER_SECTION; at the top of the function and see project README how to enable profiler)

Yeah, it would be hard if someone were to modify the direction of the code and building upon existing ones, I could see it becoming an issue towards oversight. Though if it could be further simplified...

One of the troublesome solution is to use define headers to separate the features, but it could further complicate things.

Though the profiler will be very useful to identify whether the tradeoffs would be worth it for this and #2044

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.

5 participants