Skip to content

bugfix(drawable): Decouple visual render from projectile's logic position#2598

Draft
githubawn wants to merge 6 commits intoTheSuperHackers:mainfrom
githubawn:bugfix/drawable-projectile-subframe-extrapolation
Draft

bugfix(drawable): Decouple visual render from projectile's logic position#2598
githubawn wants to merge 6 commits intoTheSuperHackers:mainfrom
githubawn:bugfix/drawable-projectile-subframe-extrapolation

Conversation

@githubawn
Copy link
Copy Markdown

@githubawn githubawn commented Apr 13, 2026

This PR fixes the visual "stutter" or "jitter" seen on fast-moving projectiles when playing at high framerates. While the game's internal logic updates at a fixed frequency, rendering often happens much faster, causing projectiles to appear to "step" across the screen rather than move smoothly.

Implemented a system that fills in the gaps between logic steps, allowing projectiles to match the user's framerate.
Created a unified way for the game's rendering system to communicate with various projectile types.

This applies to dumb and precalculated projectiles. Such as Nuke superweapon and Inferno Cannon. Particles that are attached to the projectiles and smart projectiles are outside of the scope of this PR.

Video is of 30 logic and 60 visual, notice the projectile moving with every step:
https://github.com/user-attachments/assets/fcdefce9-723d-4d3b-aeda-d60d1fcce5de

Todo:
replicate to generals

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 13, 2026

Greptile Summary

This PR decouples the visual render position of projectiles from the game logic position, implementing sub-frame extrapolation to smooth out motion on fast-moving dumb and precalculated projectiles (e.g. Nuke, Inferno Cannon) at high framerates. It introduces m_logicStepVelocity in each projectile module, a m_logicVelocityPtr cache in Drawable, and uses the logic time accumulator scaled by the logic FPS to compute a 0–1 alpha for linear extrapolation each render frame.

Confidence Score: 5/5

Safe to merge; all findings are minor P2 style/defensive issues that do not affect correctness in production builds.

The extrapolation algorithm is correct (alpha = accumulator x logicFPS gives a proper 0-1 fraction; velocity vectors represent the correct per-frame displacement). Both projectile modules properly initialize, track, and zero the velocity, and clean up the drawable pointer in onDelete. The two flagged issues are P2: the m_isModelDirty visibility change only affects debug builds with DIRTY_CONDITION_FLAGS defined, and the stale m_useExtrapolation flag after clearLogicVelocity is practically harmless since the drawable is always hidden before cleanup occurs.

GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h and GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp contain both flagged issues.

Important Files Changed

Filename Overview
GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h Adds m_visualExtrapolationMtx, m_useExtrapolation, m_logicVelocityPtr members and setLogicVelocity/clearLogicVelocity methods; the new public: section accidentally changes the access level of m_isModelDirty in DIRTY_CONDITION_FLAGS builds.
GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp Core extrapolation logic: applySubFrameExtrapolation (correct alpha = accumulator x logicFPS), getTransformMatrix now returns the visual matrix when extrapolating; setLogicVelocity(nullptr) omits resetting m_useExtrapolation, leaving getTransformMatrix returning a stale matrix after cleanup.
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp Adds m_logicStepVelocity tracking (zeroed each update, computed as next-step minus current position), wires setLogicVelocity on fire, and cleans up via onDelete; logic is correct.
GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/NeutronMissileUpdate.cpp Captures m_logicStepVelocity = m_vel at the end of doLaunch and doAttack, zeros it in DEAD state, and properly cleans up via onDelete; previously-empty onDelete now correctly chains to UpdateModule::onDelete.
GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h Exposes getLogicTimeAccumulator() accessor for the already-existing m_logicTimeAccumulator; straightforward and correct.
GeneralsMD/Code/GameEngine/Include/GameLogic/Module/UpdateModule.h Adds getProjectileLogicVelocity() virtual method with a default nullptr return to ProjectileUpdateInterface; non-breaking addition.
GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DumbProjectileBehavior.h Adds m_logicStepVelocity member, onDelete override, and getProjectileLogicVelocity override; clean declarations.
GeneralsMD/Code/GameEngine/Include/GameLogic/Module/NeutronMissileUpdate.h Adds m_logicStepVelocity member and getProjectileLogicVelocity override; clean declarations.

Sequence Diagram

sequenceDiagram
    participant Logic as Game Logic (Fixed-rate)
    participant Proj as DumbProjectile/NeutronMissile
    participant Draw as Drawable
    participant Eng as GameEngine (accumulator)
    participant Rend as Renderer

    Logic->>Proj: update()
    Proj->>Proj: advance position to flightPath[K]
    Proj->>Proj: m_logicStepVelocity = flightPath[K+1] - flightPath[K]
    Proj->>Draw: setLogicVelocity at launch time

    loop Each render frame (high FPS)
        Rend->>Eng: getLogicTimeAccumulator()
        Eng-->>Rend: elapsed time since last logic frame
        Rend->>Draw: updateDrawable()
        Draw->>Draw: alpha = accumulator x logicFPS (0 to 1)
        Draw->>Draw: visualMtx = logicPos + velocity x alpha
        Draw->>Draw: m_useExtrapolation = TRUE
        Rend->>Draw: getTransformMatrix()
        Draw-->>Rend: m_visualExtrapolationMtx (smoothed)
    end

    Logic->>Proj: update() next logic frame
    Proj->>Proj: Position snaps to flightPath[K+1], new velocity set
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h
Line: 743-749

Comment:
**`public:` section accidentally exposes `m_isModelDirty`**

Inserting the new `public:` specifier before `#ifdef DIRTY_CONDITION_FLAGS` silently changes `m_isModelDirty` from private to public in any build that defines `DIRTY_CONDITION_FLAGS`. A `private:` re-specifier is needed before the `#ifdef` block.

```suggestion
public:
	void setLogicVelocity(const Coord3D* velocity);
	static void clearLogicVelocity(Object* obj);

private:
#ifdef DIRTY_CONDITION_FLAGS
	Bool m_isModelDirty;				///< if true, must call replaceModelConditionState() before drawing or accessing drawmodule info
#endif
```

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

---

This is a comment left during a code review.
Path: GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp
Line: 5657-5660

Comment:
**`setLogicVelocity(nullptr)` leaves `m_useExtrapolation` stale**

When `clearLogicVelocity` is called (e.g. in `onDelete`), `m_logicVelocityPtr` is set to `nullptr` but `m_useExtrapolation` is not reset. Until the next `updateDrawable()` executes (which would skip `applySubFrameExtrapolation` and never clear the flag), any caller of `getTransformMatrix()` — audio positioning, collision proximity checks, etc. — will receive the old extrapolated matrix rather than the current logic position.

```suggestion
void Drawable::setLogicVelocity(const Coord3D* velocity) 
{ 
	m_logicVelocityPtr = velocity;
	if (velocity == nullptr)
		m_useExtrapolation = FALSE;
}
```

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

Reviews (6): Last reviewed commit: "added caching" | Re-trigger Greptile

@githubawn githubawn changed the title bugfix(drawable): Decouple projectile logic position from visual render update bugfix(drawable): Decouple the visual render from the projectile logic position Apr 13, 2026
@githubawn githubawn changed the title bugfix(drawable): Decouple the visual render from the projectile logic position bugfix(drawable): Decouple visual render from projectile's logic position Apr 13, 2026
Comment thread GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp
{

}
void Drawable::setLogicVelocity(const Coord3D* velocity)
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 think this is going into the right direction and this ideally becomes applicable to ALL drawable, not just projectiles. I am not a fan of this hand holding of requiring external users to set velocities. It makes caller code more complicated.

I suggest to try implement this more generic so that any movements are interpolated between real logic frames. To do that, try to have a recycled history of 3 to 5 transforms and then extrapolate them. This could work alright even with accelleration and braking. Maybe make the history length configurable per drawable, because for objects that have no accelleration, 2 points would be enough for velocity.

@xezon xezon added the Experimental Wear safety goggles in lab label Apr 13, 2026
@githubawn githubawn marked this pull request as draft April 14, 2026 11:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Experimental Wear safety goggles in lab

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants