Skip to content

bugfix(smudge): Decouple smudge time step from render update#2484

Open
xezon wants to merge 6 commits intoTheSuperHackers:mainfrom
xezon:xezon/fix-smudge-logic-step
Open

bugfix(smudge): Decouple smudge time step from render update#2484
xezon wants to merge 6 commits intoTheSuperHackers:mainfrom
xezon:xezon/fix-smudge-logic-step

Conversation

@xezon
Copy link
Copy Markdown

@xezon xezon commented Mar 22, 2026

This change decouples the smudge time step from the render update. Smudge refers to the heat haze of the USA Microwave Tank. When the game is paused, the heat effect will now pause as well.

@xezon xezon added this to the Decouple logic and rendering milestone Mar 22, 2026
@xezon xezon added Minor Severity: Minor < Major < Critical < Blocker USA Affects USA faction ZH Relates to Zero Hour Rendering Is Rendering related Bug Something is not working right, typically is user facing labels Mar 22, 2026
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 22, 2026

Greptile Summary

This PR decouples the smudge (heat haze) time step from the render update, fixing issue #1694 where the effect continued animating while the game was paused. Previously, all smudge data creation happened inside doParticles() (render-rate); now it lives in ParticleSystemManager::update() (logic-rate). The render path is slimmed down to a visibility-marking pass (resetDraw + findSmudge + set m_draw = true) before the existing W3DSmudgeManager::render() draws only the flagged smudges.

Key changes:

  • Smudge gains an Identifier (void *) field and a m_draw flag; SmudgeSet backs its used-smudge collection with a std::hash_map keyed on Identifier, enabling O(1) particle-to-smudge lookup in the render path.
  • SmudgeSet::reset() now correctly clears m_usedSmudgeMap and resets m_usedSmudgeCount (fixing a pre-existing omission).
  • W3DSmudgeManager::render() uses a local copy of smudge->m_offset rather than writing back to the struct, preventing inter-frame corruption of the stored offset.
  • W3DSmudgeManager is correctly marked final.
  • The removed setSmudgeCountLastFrame() setter is replaced by a direct assignment inside render(), which is the only place that can accurately count visible smudges.

Confidence Score: 5/5

  • This PR is safe to merge; the decoupling logic is sound, data flows are correct, and a pre-existing reset bug is incidentally fixed.
  • All six changed files have been reviewed end-to-end. The logic/render split is implemented correctly: the logic path rebuilds the full smudge set each tick (reset + add), and the render path only marks visibility and draws. Pause behaviour is correct because update() is not called while paused, freezing offsets and positions. The local offset copy in render() prevents frame-to-frame mutation. The hash-map identifier scheme correctly uses the particle pointer as a stable per-frame key after a full reset(). No null dereferences, no UAF risks, no broken batch-render paths. Only minor pre-existing style inconsistencies remain.
  • No files require special attention.

Important Files Changed

Filename Overview
Core/GameEngine/Include/GameClient/Smudge.h Adds Identifier typedef (void *) and m_draw flag to Smudge; introduces a std::hash_map-backed index on SmudgeSet for O(1) particle lookup; updates SmudgeManager API to null-invalidate removed sets and exposes resetDraw/findSmudge.
Core/GameEngine/Source/GameClient/System/Smudge.cpp Implements the new hash-map-backed addSmudgeToSet, findSmudge, and resetDraw. Also fixes a pre-existing omission: SmudgeSet::reset() now correctly clears m_usedSmudgeMap and resets m_usedSmudgeCount.
Core/GameEngine/Source/GameClient/System/ParticleSys.cpp Adds smudge rebuilding to ParticleSystemManager::update() (logic-rate), decoupling it from the render path. Each update resets the smudge manager and repopulates it from current live particles, freezing the effect when the game is paused.
Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DSmudge.h Marks W3DSmudgeManager as final, a safe and correct addition with no functional change.
Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DSmudge.cpp Render loop now checks smudge->m_draw and skips invisible smudges; uses a local offset copy instead of mutating smudge->m_offset (good fix for data stability across frames); moves m_smudgeCountLastFrame assignment into render() instead of relying on the removed setSmudgeCountLastFrame setter.
GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DParticleSys.cpp Render path simplified: instead of creating smudge data, it now only calls resetDraw() then marks frustum-culled smudges as drawable via findSmudge(p). Smudge manager reset and cleanup calls correctly removed since they moved to the logic layer.

Sequence Diagram

sequenceDiagram
    participant Logic as ParticleSystemManager<br/>(update, logic-rate)
    participant SM as SmudgeManager
    participant SS as SmudgeSet
    participant Render as W3DParticleSystemManager<br/>(doParticles, render-rate)
    participant W3DSM as W3DSmudgeManager<br/>(render)

    Note over Logic,W3DSM: Each game logic tick (paused = skipped)
    Logic->>SM: reset()
    SM->>SS: reset() [clears map + list]
    Logic->>SM: addSmudgeSet()
    SM-->>Logic: SmudgeSet*
    loop For each smudge particle
        Logic->>SS: addSmudgeToSet(particle*)
        SS-->>Logic: Smudge* (m_draw=false)
        Logic->>Logic: set pos/offset/size/opacity
    end

    Note over Logic,W3DSM: Each render frame (always runs, even when paused)
    Render->>SM: resetDraw()
    SM->>SS: resetDraw() [all m_draw = false]
    loop For each smudge particle in frustum
        Render->>SM: findSmudge(particle*)
        SM->>SS: findSmudge via hash_map
        SS-->>SM: Smudge*
        SM-->>Render: Smudge*
        Render->>Render: smudge->m_draw = true
    end
    Render->>W3DSM: render(rinfo)
    loop For each smudge where m_draw==true
        W3DSM->>W3DSM: compute UV coords (local offset copy)
        W3DSM->>W3DSM: count++
    end
    W3DSM->>W3DSM: m_smudgeCountLastFrame = count
    W3DSM->>W3DSM: draw batches via DX8 vertex buffer
Loading

Reviews (6): Last reviewed commit: "Add assert to SmudgeSet::addSmudgeToSet" | Re-trigger Greptile

@xezon xezon force-pushed the xezon/fix-smudge-logic-step branch from 1c343f0 to eb58201 Compare March 23, 2026 19:26
const Coord3D *pos = p->getPosition();
Smudge *smudge = set->addSmudgeToSet(p);
smudge->m_pos.Set(pos->x, pos->y, pos->z);
smudge->m_offset.Set(GameClientRandomValueReal(-0.06f,0.06f), GameClientRandomValueReal(-0.03f,0.03f));
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It is strange that the Y component has a different value range. This needs some investigation.

Copy link
Copy Markdown
Author

@xezon xezon Mar 25, 2026

Choose a reason for hiding this comment

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

I tested this value and it is responsible for the texture offset. What we need to do is set GameClientRandomValueReal(-0.06f,0.06f) for both X and Y, which will resemble the look of the original.

For follow up change.

Test with offset GameClientRandomValueReal(-2,2)

shot_20260325_190057_2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks how i imagine the GAP generator in red alert would distort someone's view of an area.


struct SmudgeSet : public DLNodeClass<SmudgeSet>
#ifdef USING_STLPORT
namespace std
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 would remove the namespace usage considering this is within the header.
you also call std:: directly further down.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It needs to be in the std namespace for the hash map to automatically pick up without being an explicit template argument. Modern STL already provides it, but STLPort did not which is why it is written like this.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Oh wait i see it now, yeah this is fine, i for some reason read it as though it was a using namespace directive.

SmudgeSet *addSmudgeSet(); ///< add and return a new smudge set
void removeSmudgeSet(SmudgeSet *&smudgeSet); ///< remove and invalidate the given smudge set
Smudge *findSmudge(Smudge::Identifier identifier); ///< find the smudge from any smudge set
Int getSmudgeCountLastFrame() {return m_smudgeCountLastFrame;} ///<return number of smudges submitted last frame.
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 is not used by anything so could be removed.

Copy link
Copy Markdown

@Mauller Mauller 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 overall

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

Labels

Bug Something is not working right, typically is user facing Minor Severity: Minor < Major < Critical < Blocker Rendering Is Rendering related USA Affects USA faction ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

USA Microwave Heat Haze is not halting on game pause

2 participants