Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9c726ac
Decouple pitch/roll rate damping from render update
bobtista Jan 4, 2026
ed52d50
Replicate to generals
bobtista Jan 4, 2026
879e415
Decouple wheel suspension offset from render update
bobtista Jan 4, 2026
f823f92
Replicate to generals
bobtista Jan 4, 2026
96f7b80
Decouple missile wobble and thrust roll from render update
bobtista Jan 4, 2026
3bf7db2
Replicate to generals
bobtista Jan 4, 2026
e0dfd9b
Decouple hover/wings spring-damper physics from render update
bobtista Jan 4, 2026
6eef2fe
Replicate to generals
bobtista Jan 4, 2026
362af18
Decouple remaining Drawable physics from render update
bobtista Jan 4, 2026
297f0c4
Replicate to generals
bobtista Jan 4, 2026
4ada4d0
Fix additional missed physics timing issues
bobtista Jan 4, 2026
c8abfb6
Replicate to generals
bobtista Jan 4, 2026
bfc118c
Decouple decal opacity fade from render update
bobtista Jan 4, 2026
379ea76
Replicate to generals
bobtista Jan 4, 2026
f504556
Decouple drawable fade timing from render update
bobtista Jan 4, 2026
cbaa530
Replicate to generals
bobtista Jan 4, 2026
6c822d9
Decouple airborne frame counter from render update
bobtista Jan 4, 2026
f86991a
Replicate to generals
bobtista Jan 4, 2026
7dcd285
Calculate physics at fixed logic rate with interpolation to restore w…
bobtista Jan 6, 2026
4461fd6
docs(drawable): Document xfer version 9 in version history
bobtista Jun 29, 2026
5a8c9db
Replicate to generals
bobtista Jun 29, 2026
d3c5b8d
Fix interpolation to span a full logic frame and remove render time s…
bobtista Jul 19, 2026
40bf8d3
Replicate to generals
bobtista Aug 14, 2026
7be7c8f
refactor(drawable): Restore untouched physics lines left over from re…
bobtista Aug 14, 2026
bbfe20f
bugfix(drawable): Preserve fade progress when saving
bobtista Aug 30, 2026
e7a1573
bugfix(drawable): Replicate fade save fix to Generals
bobtista Aug 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions Generals/Code/GameEngine/Include/GameClient/Drawable.h
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,13 @@ class Drawable : public Thing,
Real m_totalYaw; ///< Current total yaw for this frame
Real m_totalZ;

PhysicsXformInfo() : m_totalPitch(0), m_totalRoll(0), m_totalYaw(0), m_totalZ(0) { }
Real m_prevTotalPitch;
Real m_prevTotalRoll;
Real m_prevTotalYaw;
Real m_prevTotalZ;

PhysicsXformInfo() : m_totalPitch(0), m_totalRoll(0), m_totalYaw(0), m_totalZ(0),
m_prevTotalPitch(0), m_prevTotalRoll(0), m_prevTotalYaw(0), m_prevTotalZ(0) { }
};

Bool calcPhysicsXform(PhysicsXformInfo& info);
Expand Down Expand Up @@ -687,7 +693,7 @@ class Drawable : public Thing,
FADING_OUT
};
FadingMode m_fadeMode;
UnsignedInt m_timeElapsedFade; ///< for how many frames have i been fading
Real m_timeElapsedFade; ///< for how long have i been fading (in 30fps-equivalent frames)
UnsignedInt m_timeToFade; ///< how slowly am I fading

UnsignedInt m_shroudClearFrame; ///< Last frame the local player saw this drawable "OBJECTSHROUD_CLEAR"
Expand Down
52 changes: 42 additions & 10 deletions Generals/Code/GameEngine/Source/GameClient/Drawable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1147,7 +1147,9 @@ void Drawable::updateDrawable()
Real numer = (m_fadeMode == FADING_IN) ? (m_timeElapsedFade) : (m_timeToFade-m_timeElapsedFade);

setDrawableOpacity(numer/(Real)m_timeToFade);
++m_timeElapsedFade;
// TheSuperHackers @tweak Drawable fade is now decoupled from the render update.
const Real fadeTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio();
m_timeElapsedFade += fadeTimeScale;

if (m_timeElapsedFade > m_timeToFade)
m_fadeMode = FADING_NONE;
Expand All @@ -1165,7 +1167,9 @@ void Drawable::updateDrawable()
{
//LERP
(*dm)->setTerrainDecalOpacity(m_decalOpacity);
m_decalOpacity += m_decalOpacityFadeRate;
// TheSuperHackers @tweak Decal opacity fade is now decoupled from the render update.
const Real decalFadeTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio();
m_decalOpacity += m_decalOpacityFadeRate * decalFadeTimeScale;
}
//---------------

Expand Down Expand Up @@ -1349,17 +1353,32 @@ void Drawable::applyPhysicsXform(Matrix3D* mtx)
{
if (m_physicsXform != nullptr)
{
// TheSuperHackers @tweak Update the physics transform on every WW Sync only.
// All calculations are originally catered to a 30 fps logic step.
// TheSuperHackers @tweak Run physics on logic frames only, interpolate for rendering.
// This provides stable physics at any framerate without numerical integration issues.
if (WW3D::Get_Sync_Frame_Time() != 0)
{
m_physicsXform->m_prevTotalPitch = m_physicsXform->m_totalPitch;
m_physicsXform->m_prevTotalRoll = m_physicsXform->m_totalRoll;
m_physicsXform->m_prevTotalYaw = m_physicsXform->m_totalYaw;
m_physicsXform->m_prevTotalZ = m_physicsXform->m_totalZ;

calcPhysicsXform(*m_physicsXform);
}

mtx->Translate(0.0f, 0.0f, m_physicsXform->m_totalZ);
mtx->Rotate_Y( m_physicsXform->m_totalPitch );
mtx->Rotate_X( -m_physicsXform->m_totalRoll );
mtx->Rotate_Z( m_physicsXform->m_totalYaw );
// Interpolate between previous and current state based on fractional sync time.
// The fractional sync time accumulates in logic time, so a full logic step is always MSEC_PER_LOGICFRAME_REAL.
const Real fractionalMs = (Real)WW3D::Get_Fractional_Sync_Milliseconds();
const Real t = clamp(0.0f, fractionalMs / MSEC_PER_LOGICFRAME_REAL, 1.0f);

const Real interpPitch = m_physicsXform->m_prevTotalPitch + t * (m_physicsXform->m_totalPitch - m_physicsXform->m_prevTotalPitch);
const Real interpRoll = m_physicsXform->m_prevTotalRoll + t * (m_physicsXform->m_totalRoll - m_physicsXform->m_prevTotalRoll);
const Real interpYaw = m_physicsXform->m_prevTotalYaw + t * (m_physicsXform->m_totalYaw - m_physicsXform->m_prevTotalYaw);
const Real interpZ = m_physicsXform->m_prevTotalZ + t * (m_physicsXform->m_totalZ - m_physicsXform->m_prevTotalZ);

mtx->Translate(0.0f, 0.0f, interpZ);
mtx->Rotate_Y( interpPitch );
mtx->Rotate_X( -interpRoll );
mtx->Rotate_Z( interpYaw );
}
}

Expand Down Expand Up @@ -4845,6 +4864,7 @@ void Drawable::xferDrawableModules( Xfer *xfer )
* 6: Added m_ambientSoundEnabledFromScript flag (Added in Zero Hour)
* 7: Save the customize ambient sound info (Added in Zero Hour)
* 8: TheSuperHackers @bugfix Removed m_prevTintStatus because loading its value is unnecessary and undesirable
* 9: TheSuperHackers @tweak Changed m_timeElapsedFade from UnsignedInt to Real for frame-rate independent fading
*/
// ------------------------------------------------------------------------------------------------
void Drawable::xfer( Xfer *xfer )
Expand All @@ -4856,7 +4876,7 @@ void Drawable::xfer( Xfer *xfer )
#elif RETAIL_COMPATIBLE_XFER_SAVE
const XferVersion currentVersion = 7;
#else
const XferVersion currentVersion = 8;
const XferVersion currentVersion = 9;
#endif
XferVersion version = currentVersion;
xfer->xferVersion( &version, currentVersion );
Expand Down Expand Up @@ -5030,7 +5050,19 @@ void Drawable::xfer( Xfer *xfer )
xfer->xferUser( &m_fadeMode, sizeof( FadingMode ) );

// time elapsed fade
xfer->xferUnsignedInt( &m_timeElapsedFade );
if (version >= 9)
{
xfer->xferReal( &m_timeElapsedFade );
}
else
{
UnsignedInt timeElapsedFadeFrames = static_cast<UnsignedInt>(m_timeElapsedFade);
xfer->xferUnsignedInt( &timeElapsedFadeFrames );
if (xfer->getXferMode() == XFER_LOAD)
{
m_timeElapsedFade = static_cast<Real>(timeElapsedFadeFrames);
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

// time to fade
xfer->xferUnsignedInt( &m_timeToFade );
Expand Down
10 changes: 8 additions & 2 deletions GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,13 @@ class Drawable : public Thing,
Real m_totalYaw; ///< Current total yaw for this frame
Real m_totalZ;

PhysicsXformInfo() : m_totalPitch(0), m_totalRoll(0), m_totalYaw(0), m_totalZ(0) { }
Real m_prevTotalPitch;
Real m_prevTotalRoll;
Real m_prevTotalYaw;
Real m_prevTotalZ;

PhysicsXformInfo() : m_totalPitch(0), m_totalRoll(0), m_totalYaw(0), m_totalZ(0),
m_prevTotalPitch(0), m_prevTotalRoll(0), m_prevTotalYaw(0), m_prevTotalZ(0) { }
};

Bool calcPhysicsXform(PhysicsXformInfo& info);
Expand Down Expand Up @@ -687,7 +693,7 @@ class Drawable : public Thing,
FADING_OUT
};
FadingMode m_fadeMode;
UnsignedInt m_timeElapsedFade; ///< for how many frames have i been fading
Real m_timeElapsedFade; ///< for how long have i been fading (in 30fps-equivalent frames)
UnsignedInt m_timeToFade; ///< how slowly am I fading

UnsignedInt m_shroudClearFrame; ///< Last frame the local player saw this drawable "OBJECTSHROUD_CLEAR"
Expand Down
52 changes: 42 additions & 10 deletions GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1149,7 +1149,9 @@ void Drawable::updateDrawable()
Real numer = (m_fadeMode == FADING_IN) ? (m_timeElapsedFade) : (m_timeToFade-m_timeElapsedFade);

setDrawableOpacity(numer/(Real)m_timeToFade);
++m_timeElapsedFade;
// TheSuperHackers @tweak Drawable fade is now decoupled from the render update.
const Real fadeTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio();
m_timeElapsedFade += fadeTimeScale;

if (m_timeElapsedFade > m_timeToFade)
m_fadeMode = FADING_NONE;
Expand All @@ -1167,7 +1169,9 @@ void Drawable::updateDrawable()
{
//LERP
(*dm)->setTerrainDecalOpacity(m_decalOpacity);
m_decalOpacity += m_decalOpacityFadeRate;
// TheSuperHackers @tweak Decal opacity fade is now decoupled from the render update.
const Real decalFadeTimeScale = TheFramePacer->getActualLogicTimeScaleOverFpsRatio();
m_decalOpacity += m_decalOpacityFadeRate * decalFadeTimeScale;
}
//---------------

Expand Down Expand Up @@ -1351,17 +1355,32 @@ void Drawable::applyPhysicsXform(Matrix3D* mtx)
{
if (m_physicsXform != nullptr)
{
// TheSuperHackers @tweak Update the physics transform on every WW Sync only.
// All calculations are originally catered to a 30 fps logic step.
// TheSuperHackers @tweak Run physics on logic frames only, interpolate for rendering.
// This provides stable physics at any framerate without numerical integration issues.
if (WW3D::Get_Sync_Frame_Time() != 0)
{
m_physicsXform->m_prevTotalPitch = m_physicsXform->m_totalPitch;
m_physicsXform->m_prevTotalRoll = m_physicsXform->m_totalRoll;
m_physicsXform->m_prevTotalYaw = m_physicsXform->m_totalYaw;
m_physicsXform->m_prevTotalZ = m_physicsXform->m_totalZ;

calcPhysicsXform(*m_physicsXform);
}

mtx->Translate(0.0f, 0.0f, m_physicsXform->m_totalZ);
mtx->Rotate_Y( m_physicsXform->m_totalPitch );
mtx->Rotate_X( -m_physicsXform->m_totalRoll );
mtx->Rotate_Z( m_physicsXform->m_totalYaw );
// Interpolate between previous and current state based on fractional sync time.
// The fractional sync time accumulates in logic time, so a full logic step is always MSEC_PER_LOGICFRAME_REAL.
const Real fractionalMs = (Real)WW3D::Get_Fractional_Sync_Milliseconds();
const Real t = clamp(0.0f, fractionalMs / MSEC_PER_LOGICFRAME_REAL, 1.0f);

const Real interpPitch = m_physicsXform->m_prevTotalPitch + t * (m_physicsXform->m_totalPitch - m_physicsXform->m_prevTotalPitch);
const Real interpRoll = m_physicsXform->m_prevTotalRoll + t * (m_physicsXform->m_totalRoll - m_physicsXform->m_prevTotalRoll);
const Real interpYaw = m_physicsXform->m_prevTotalYaw + t * (m_physicsXform->m_totalYaw - m_physicsXform->m_prevTotalYaw);
const Real interpZ = m_physicsXform->m_prevTotalZ + t * (m_physicsXform->m_totalZ - m_physicsXform->m_prevTotalZ);

mtx->Translate(0.0f, 0.0f, interpZ);
mtx->Rotate_Y( interpPitch );
mtx->Rotate_X( -interpRoll );
mtx->Rotate_Z( interpYaw );
}
}

Expand Down Expand Up @@ -4850,6 +4869,7 @@ void Drawable::xferDrawableModules( Xfer *xfer )
* 6: Added m_ambientSoundEnabledFromScript flag (Added in Zero Hour)
* 7: Save the customize ambient sound info (Added in Zero Hour)
* 8: TheSuperHackers @bugfix Removed m_prevTintStatus because loading its value is unnecessary and undesirable
* 9: TheSuperHackers @tweak Changed m_timeElapsedFade from UnsignedInt to Real for frame-rate independent fading
*/
// ------------------------------------------------------------------------------------------------
void Drawable::xfer( Xfer *xfer )
Expand All @@ -4861,7 +4881,7 @@ void Drawable::xfer( Xfer *xfer )
#elif RETAIL_COMPATIBLE_XFER_SAVE
const XferVersion currentVersion = 7;
#else
const XferVersion currentVersion = 8;
const XferVersion currentVersion = 9;
#endif
XferVersion version = currentVersion;
xfer->xferVersion( &version, currentVersion );
Expand Down Expand Up @@ -5035,7 +5055,19 @@ void Drawable::xfer( Xfer *xfer )
xfer->xferUser( &m_fadeMode, sizeof( FadingMode ) );

// time elapsed fade
xfer->xferUnsignedInt( &m_timeElapsedFade );
if (version >= 9)
{
xfer->xferReal( &m_timeElapsedFade );
}
else
{
UnsignedInt timeElapsedFadeFrames = static_cast<UnsignedInt>(m_timeElapsedFade);
xfer->xferUnsignedInt( &timeElapsedFadeFrames );
if (xfer->getXferMode() == XFER_LOAD)
{
m_timeElapsedFade = static_cast<Real>(timeElapsedFadeFrames);
}
}

// time to fade
xfer->xferUnsignedInt( &m_timeToFade );
Expand Down
Loading