diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt index 494d544f73..ceb8762557 100644 --- a/Core/GameEngine/CMakeLists.txt +++ b/Core/GameEngine/CMakeLists.txt @@ -39,6 +39,7 @@ set(GAMEENGINE_SRC # Include/Common/Errors.h Include/Common/file.h Include/Common/FileSystem.h + Include/Common/FrameRateLimit.h # Include/Common/FunctionLexicon.h Include/Common/GameAudio.h # Include/Common/GameCommon.h @@ -569,6 +570,7 @@ set(GAMEENGINE_SRC # Source/Common/DamageFX.cpp # Source/Common/Dict.cpp # Source/Common/DiscreteCircle.cpp + Source/Common/FrameRateLimit.cpp # Source/Common/GameEngine.cpp # Source/Common/GameLOD.cpp # Source/Common/GameMain.cpp diff --git a/Core/GameEngine/Include/Common/FrameRateLimit.h b/Core/GameEngine/Include/Common/FrameRateLimit.h new file mode 100644 index 0000000000..2ddc9136c0 --- /dev/null +++ b/Core/GameEngine/Include/Common/FrameRateLimit.h @@ -0,0 +1,70 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#pragma once + +class FrameRateLimit +{ +public: + FrameRateLimit(); + + Real wait(UnsignedInt maxFps); + +private: + LARGE_INTEGER m_freq; + LARGE_INTEGER m_start; +}; + + +enum FpsValueChange +{ + FpsValueChange_Increase, + FpsValueChange_Decrease, +}; + + +class RenderFpsPreset +{ +public: + enum CPP_11(: UnsignedInt) + { + UncappedFpsValue = 1000, + }; + + static UnsignedInt getNextFpsValue(UnsignedInt value); + static UnsignedInt getPrevFpsValue(UnsignedInt value); + static UnsignedInt changeFpsValue(UnsignedInt value, FpsValueChange change); + +private: + static const UnsignedInt s_fpsValues[]; +}; + + +class LogicTimeScaleFpsPreset +{ +public: + enum CPP_11(: UnsignedInt) + { + StepFpsValue = 5, + }; + + static UnsignedInt getNextFpsValue(UnsignedInt value); + static UnsignedInt getPrevFpsValue(UnsignedInt value); + static UnsignedInt changeFpsValue(UnsignedInt value, FpsValueChange change); +}; + diff --git a/Core/GameEngine/Source/Common/FrameRateLimit.cpp b/Core/GameEngine/Source/Common/FrameRateLimit.cpp new file mode 100644 index 0000000000..71fb213d65 --- /dev/null +++ b/Core/GameEngine/Source/Common/FrameRateLimit.cpp @@ -0,0 +1,126 @@ +/* +** Command & Conquer Generals Zero Hour(tm) +** Copyright 2025 TheSuperHackers +** +** This program is free software: you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation, either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program. If not, see . +*/ + +#include "PreRTS.h" +#include "Common/FrameRateLimit.h" + + +FrameRateLimit::FrameRateLimit() +{ + QueryPerformanceFrequency(&m_freq); + QueryPerformanceCounter(&m_start); +} + +Real FrameRateLimit::wait(UnsignedInt maxFps) +{ + LARGE_INTEGER tick; + QueryPerformanceCounter(&tick); + double elapsedSeconds = static_cast(tick.QuadPart - m_start.QuadPart) / m_freq.QuadPart; + const double targetSeconds = 1.0 / maxFps; + const double sleepSeconds = targetSeconds - elapsedSeconds - 0.002; // leave ~2ms for spin wait + + if (sleepSeconds > 0.0) + { + // Non busy wait with Munkee sleep + DWORD dwMilliseconds = static_cast(sleepSeconds * 1000); + Sleep(dwMilliseconds); + } + + // Busy wait for remaining time + do + { + QueryPerformanceCounter(&tick); + elapsedSeconds = static_cast(tick.QuadPart - m_start.QuadPart) / m_freq.QuadPart; + } + while (elapsedSeconds < targetSeconds); + + m_start = tick; + return (Real)elapsedSeconds; +} + + +const UnsignedInt RenderFpsPreset::s_fpsValues[] = { + 30, 50, 56, 60, 65, 70, 72, 75, 80, 85, 90, 100, 110, 120, 144, 240, 480, UncappedFpsValue }; + +static_assert(LOGICFRAMES_PER_SECOND <= 30, "Min FPS values need to be revisited!"); + +UnsignedInt RenderFpsPreset::getNextFpsValue(UnsignedInt value) +{ + const Int first = 0; + const Int last = ARRAY_SIZE(s_fpsValues) - 1; + for (Int i = first; i < last; ++i) + { + if (value >= s_fpsValues[i] && value < s_fpsValues[i + 1]) + { + return s_fpsValues[i + 1]; + } + } + return s_fpsValues[last]; +} + +UnsignedInt RenderFpsPreset::getPrevFpsValue(UnsignedInt value) +{ + const Int first = 0; + const Int last = ARRAY_SIZE(s_fpsValues) - 1; + for (Int i = last; i > first; --i) + { + if (value <= s_fpsValues[i] && value > s_fpsValues[i - 1]) + { + return s_fpsValues[i - 1]; + } + } + return s_fpsValues[first]; +} + +UnsignedInt RenderFpsPreset::changeFpsValue(UnsignedInt value, FpsValueChange change) +{ + switch (change) + { + default: + case FpsValueChange_Increase: return getNextFpsValue(value); + case FpsValueChange_Decrease: return getPrevFpsValue(value); + } +} + + +UnsignedInt LogicTimeScaleFpsPreset::getNextFpsValue(UnsignedInt value) +{ + return value + StepFpsValue; +} + +UnsignedInt LogicTimeScaleFpsPreset::getPrevFpsValue(UnsignedInt value) +{ + if (value - StepFpsValue < LOGICFRAMES_PER_SECOND) + { + return LOGICFRAMES_PER_SECOND; + } + else + { + return value - StepFpsValue; + } +} + +UnsignedInt LogicTimeScaleFpsPreset::changeFpsValue(UnsignedInt value, FpsValueChange change) +{ + switch (change) + { + default: + case FpsValueChange_Increase: return getNextFpsValue(value); + case FpsValueChange_Decrease: return getPrevFpsValue(value); + } +} diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h index 02e9c39e4e..86f524551e 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GameEngine.h @@ -34,8 +34,6 @@ #include "Common/SubsystemInterface.h" #include "Common/GameType.h" -#define DEFAULT_MAX_FPS 45 - // forward declarations class AudioManager; class GameLogic; @@ -72,8 +70,20 @@ class GameEngine : public SubsystemInterface virtual void execute( void ); /**< The "main loop" of the game engine. It will not return until the game exits. */ - virtual void setFramesPerSecondLimit( Int fps ); ///< Set the maximum rate engine updates are allowed to occur - virtual Int getFramesPerSecondLimit( void ); ///< Get maxFPS. Not inline since it is called from another lib. + + virtual void setFramesPerSecondLimit( Int fps ); ///< Set the max render and engine update fps. + virtual Int getFramesPerSecondLimit( void ); ///< Get the max render and engine update fps. + Real getUpdateTime(); ///< Get the last engine update delta time. + Real getUpdateFps(); ///< Get the last engine update fps. + + virtual void setLogicTimeScaleFps( Int fps ); ///< Set the logic time scale fps and therefore scale the simulation time. Is capped by the max render fps and does not apply to network matches. + virtual Int getLogicTimeScaleFps(); ///< Get the raw logic time scale fps value. + virtual void enableLogicTimeScale( Bool enable ); ///< Enable the logic time scale setup. If disabled, the simulation time scale is bound to the render frame time or network update time. + virtual Bool isLogicTimeScaleEnabled(); ///< Check whether the logic time scale setup is enabled. + Int getActualLogicTimeScaleFps(); ///< Get the real logic time scale fps, depending on the max render fps, network state and enabled state. + Real getActualLogicTimeScaleRatio(); ///< Get the real logic time scale ratio, depending on the max render fps, network state and enabled state. + Real getActualLogicTimeScaleOverFpsRatio(); ///< Get the real logic time scale over render fps ratio, used to scale down steps in render updates to match logic updates. + virtual void setQuitting( Bool quitting ); ///< set quitting status virtual Bool getQuitting(void); ///< is app getting ready to quit. @@ -100,9 +110,15 @@ class GameEngine : public SubsystemInterface virtual ParticleSystemManager* createParticleSystemManager( void ) = 0; virtual AudioManager *createAudioManager( void ) = 0; ///< Factory for Audio Manager - Int m_maxFPS; ///< Maximum frames per second allowed + Int m_maxFPS; ///< Maximum frames per second for rendering + Int m_logicTimeScaleFPS; ///< Maximum frames per second for logic time scale + + Real m_updateTime; ///< Last engine update delta time + Real m_logicTimeAccumulator; ///< Frame time accumulated towards submitting a new logic frame + Bool m_quitting; ///< true when we need to quit the game Bool m_isActive; ///< app has OS focus. + Bool m_enableLogicTimeScale; }; inline void GameEngine::setQuitting( Bool quitting ) { m_quitting = quitting; } diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index e34faf16d2..9bfeee47b3 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -241,6 +241,10 @@ class GameMessage : public MemoryPoolObject MSG_META_HELP, ///< bring up help screen #endif + MSG_META_INCREASE_MAX_RENDER_FPS, ///< TheSuperHackers @feature Increase the max render fps + MSG_META_DECREASE_MAX_RENDER_FPS, ///< TheSuperHackers @feature Decrease the max render fps + MSG_META_INCREASE_LOGIC_TIME_SCALE, ///< TheSuperHackers @feature Increase the logic time scale + MSG_META_DECREASE_LOGIC_TIME_SCALE, ///< TheSuperHackers @feature Decrease the logic time scale MSG_META_TOGGLE_LOWER_DETAILS, ///< toggles graphics options to crappy mode instantly MSG_META_TOGGLE_CONTROL_BAR, ///< show/hide controlbar diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h index 3fcc01a078..7b889b9c0b 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Display.h @@ -109,6 +109,7 @@ class Display : public SubsystemInterface virtual void drawViews( void ); ///< Render all views of the world virtual void updateViews ( void ); ///< Updates state of world views + virtual void stepViews(); ///< Update views for every fixed time step virtual VideoBuffer* createVideoBuffer( void ) = 0; ///< Create a video buffer that can be used for this display @@ -118,6 +119,7 @@ class Display : public SubsystemInterface virtual Bool isClippingEnabled( void ) = 0; virtual void enableClipping( Bool onoff ) = 0; + virtual void step() {}; ///< Do one fixed time step virtual void draw( void ); ///< Redraw the entire display virtual void setTimeOfDay( TimeOfDay tod ) = 0; ///< Set the time of day for this display virtual void createLightPulse( const Coord3D *pos, const RGBColor *color, Real innerRadius,Real attenuationWidth, diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h index 72248a9e04..634b78545a 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/GameClient.h @@ -99,6 +99,8 @@ class GameClient : public SubsystemInterface, virtual void setFrame( UnsignedInt frame ) { m_frame = frame; } ///< Set the GameClient's internal frame number virtual void registerDrawable( Drawable *draw ); ///< Given a drawable, register it with the GameClient and give it a unique ID + void step(); ///< Do one fixed time step + void updateHeadless(); void addDrawableToLookupTable( Drawable *draw ); ///< add drawable ID to hash lookup table diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/MetaEvent.h b/GeneralsMD/Code/GameEngine/Include/GameClient/MetaEvent.h index 1d1a19dea3..cb358886cc 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/MetaEvent.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/MetaEvent.h @@ -69,6 +69,24 @@ static const LookupListRec CategoryListName[] = // KeyDefType; this is extremely important to maintain! enum MappableKeyType CPP_11(: Int) { + // keypad keys ---------------------------------------------------------------- + MK_KP0 = KEY_KP0, + MK_KP1 = KEY_KP1, + MK_KP2 = KEY_KP2, + MK_KP3 = KEY_KP3, + MK_KP4 = KEY_KP4, + MK_KP5 = KEY_KP5, + MK_KP6 = KEY_KP6, + MK_KP7 = KEY_KP7, + MK_KP8 = KEY_KP8, + MK_KP9 = KEY_KP9, + MK_KPDEL = KEY_KPDEL, + MK_KPSTAR = KEY_KPSTAR, + MK_KPMINUS = KEY_KPMINUS, + MK_KPPLUS = KEY_KPPLUS, + MK_KPENTER = KEY_KPENTER, + MK_KPSLASH = KEY_KPSLASH, + MK_ESC = KEY_ESC, MK_BACKSPACE = KEY_BACKSPACE, MK_ENTER = KEY_ENTER, @@ -122,16 +140,6 @@ enum MappableKeyType CPP_11(: Int) MK_8 = KEY_8, MK_9 = KEY_9, MK_0 = KEY_0, - MK_KP1 = KEY_KP1, - MK_KP2 = KEY_KP2, - MK_KP3 = KEY_KP3, - MK_KP4 = KEY_KP4, - MK_KP5 = KEY_KP5, - MK_KP6 = KEY_KP6, - MK_KP7 = KEY_KP7, - MK_KP8 = KEY_KP8, - MK_KP9 = KEY_KP9, - MK_KP0 = KEY_KP0, MK_MINUS = KEY_MINUS, MK_EQUAL = KEY_EQUAL, MK_LBRACKET = KEY_LBRACKET, @@ -153,13 +161,30 @@ enum MappableKeyType CPP_11(: Int) MK_PGDN = KEY_PGDN, MK_INS = KEY_INS, MK_DEL = KEY_DEL, - MK_KPSLASH = KEY_KPSLASH, MK_NONE = KEY_NONE }; static const LookupListRec KeyNames[] = { + // keypad keys ---------------------------------------------------------------- + { "KEY_KP0", MK_KP0 }, + { "KEY_KP1", MK_KP1 }, + { "KEY_KP2", MK_KP2 }, + { "KEY_KP3", MK_KP3 }, + { "KEY_KP4", MK_KP4 }, + { "KEY_KP5", MK_KP5 }, + { "KEY_KP6", MK_KP6 }, + { "KEY_KP7", MK_KP7 }, + { "KEY_KP8", MK_KP8 }, + { "KEY_KP9", MK_KP9 }, + { "KEY_KPDEL", MK_KPDEL }, + { "KEY_KPSTAR", MK_KPSTAR }, + { "KEY_KPMINUS", MK_KPMINUS }, + { "KEY_KPPLUS", MK_KPPLUS }, + { "KEY_KPENTER", MK_KPENTER }, + { "KEY_KPSLASH", MK_KPSLASH }, + { "KEY_ESC", MK_ESC }, { "KEY_BACKSPACE", MK_BACKSPACE }, { "KEY_ENTER", MK_ENTER }, @@ -213,16 +238,6 @@ static const LookupListRec KeyNames[] = { "KEY_8", MK_8 }, { "KEY_9", MK_9 }, { "KEY_0", MK_0 }, - { "KEY_KP1", MK_KP1 }, - { "KEY_KP2", MK_KP2 }, - { "KEY_KP3", MK_KP3 }, - { "KEY_KP4", MK_KP4 }, - { "KEY_KP5", MK_KP5 }, - { "KEY_KP6", MK_KP6 }, - { "KEY_KP7", MK_KP7 }, - { "KEY_KP8", MK_KP8 }, - { "KEY_KP9", MK_KP9 }, - { "KEY_KP0", MK_KP0 }, { "KEY_MINUS", MK_MINUS }, { "KEY_EQUAL", MK_EQUAL }, { "KEY_LBRACKET", MK_LBRACKET }, @@ -244,7 +259,6 @@ static const LookupListRec KeyNames[] = { "KEY_PGDN", MK_PGDN }, { "KEY_INS", MK_INS }, { "KEY_DEL", MK_DEL }, - { "KEY_KPSLASH", MK_KPSLASH }, { "KEY_NONE", MK_NONE }, { NULL, 0 } // keep this last! }; @@ -301,7 +315,9 @@ enum CommandUsableInType CPP_11(: Int) COMMANDUSABLE_NONE = 0, COMMANDUSABLE_SHELL = (1 << 0), - COMMANDUSABLE_GAME = (1 << 1) + COMMANDUSABLE_GAME = (1 << 1), + + COMMANDUSABLE_EVERYWHERE = COMMANDUSABLE_SHELL | COMMANDUSABLE_GAME, }; static const char* TheCommandUsableInNames[] = diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/View.h b/GeneralsMD/Code/GameEngine/Include/GameClient/View.h index 8afbd829a0..a72a360c4c 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/View.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/View.h @@ -184,8 +184,7 @@ class View : public Snapshot virtual void setZoom(Real z) { } virtual Real getHeightAboveGround() { return m_heightAboveGround; } virtual void setHeightAboveGround(Real z) { m_heightAboveGround = z; } - virtual void zoomIn( void ); ///< Zoom in, closer to the ground, limit to min - virtual void zoomOut( void ); ///< Zoom out, farther away from the ground, limit to max + virtual void zoom( Real height ); ///< Zoom in/out, closer to the ground, limit to min, or farther away from the ground, limit to max virtual void setZoomToDefault( void ) { } ///< Set zoom to default value virtual Real getMaxZoom( void ) { return m_maxZoom; } ///< return max zoom value virtual void setOkToAdjustHeight( Bool val ) { m_okToAdjustHeight = val; } ///< Set this to adjust camera height @@ -211,6 +210,7 @@ class View : public Snapshot virtual void drawView( void ) = 0; ///< Render the world visible in this view. virtual void updateView(void) = 0; ///m_useFpsLimit == %d)", fps, TheGlobalData->m_useFpsLimit)); m_maxFPS = fps; } +//------------------------------------------------------------------------------------------------- +Int GameEngine::getFramesPerSecondLimit( void ) +{ + return m_maxFPS; +} + +//------------------------------------------------------------------------------------------------- +Real GameEngine::getUpdateTime() +{ + return m_updateTime; +} + +//------------------------------------------------------------------------------------------------- +Real GameEngine::getUpdateFps() +{ + return 1.0f / m_updateTime; +} + +//------------------------------------------------------------------------------------------------- +void GameEngine::setLogicTimeScaleFps( Int fps ) +{ + m_logicTimeScaleFPS = fps; +} + +//------------------------------------------------------------------------------------------------- +Int GameEngine::getLogicTimeScaleFps() +{ + return m_logicTimeScaleFPS; +} + +//------------------------------------------------------------------------------------------------- +void GameEngine::enableLogicTimeScale( Bool enable ) +{ + m_enableLogicTimeScale = enable; +} + +//------------------------------------------------------------------------------------------------- +Bool GameEngine::isLogicTimeScaleEnabled() +{ + return m_enableLogicTimeScale; +} + +//------------------------------------------------------------------------------------------------- +Int GameEngine::getActualLogicTimeScaleFps( void ) +{ + if (TheNetwork != NULL) + { + return TheNetwork->getFrameRate(); + } + else + { + const Bool enabled = isLogicTimeScaleEnabled(); + const Int logicTimeScaleFps = getLogicTimeScaleFps(); + const Int maxFps = getFramesPerSecondLimit(); + + if (!enabled || logicTimeScaleFps >= maxFps) + { + return getFramesPerSecondLimit(); + } + else + { + return logicTimeScaleFps; + } + } +} + +//------------------------------------------------------------------------------------------------- +Real GameEngine::getActualLogicTimeScaleRatio() +{ + return (Real)getActualLogicTimeScaleFps() / LOGICFRAMES_PER_SECONDS_REAL; +} + +//------------------------------------------------------------------------------------------------- +Real GameEngine::getActualLogicTimeScaleOverFpsRatio() +{ + // TheSuperHackers @info Clamps ratio to min 1, because the logic + // frame rate is (typically) capped by the render frame rate. + return min(1.0f, (Real)getActualLogicTimeScaleFps() / getUpdateFps()); +} + /** ----------------------------------------------------------------------------------------------- * Initialize the game engine by initializing the GameLogic and GameClient. */ @@ -358,7 +437,6 @@ void GameEngine::init() char Buf[256];////////////////////////////////////////////////////////////////////// #endif////////////////////////////////////////////////////////////////////////////// - m_maxFPS = DEFAULT_MAX_FPS; TheSubsystemList = MSGNEW("GameEngineSubsystem") SubsystemInterfaceList; @@ -824,9 +902,49 @@ void GameEngine::update( void ) TheGameLogic->preUpdate(); - if ((TheNetwork == NULL && !TheGameLogic->isGamePaused()) || (TheNetwork && TheNetwork->isFrameDataReady())) + if (TheNetwork != NULL) { - TheGameLogic->UPDATE(); + if (TheNetwork->isFrameDataReady()) + { + TheGameClient->step(); + TheGameLogic->UPDATE(); + } + } + else + { + if (!TheGameLogic->isGamePaused()) + { + const Bool enabled = isLogicTimeScaleEnabled(); + const Int logicTimeScaleFps = getLogicTimeScaleFps(); + const Int maxRenderFps = getFramesPerSecondLimit(); + +#if defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE) + Bool useFastMode = TheGlobalData->m_TiVOFastMode; +#else //always allow this cheat key if we're in a replay game. + Bool useFastMode = TheGlobalData->m_TiVOFastMode && TheGameLogic->isInReplayGame(); +#endif + + if (useFastMode || !enabled || logicTimeScaleFps >= maxRenderFps) + { + // Logic time scale is uncapped or larger equal Render FPS. Update straight away. + TheGameClient->step(); + TheGameLogic->UPDATE(); + } + else + { + // TheSuperHackers @tweak xezon 06/08/2025 + // The logic time step is now decoupled from the render update. + const Real targetFrameTime = 1.0f / logicTimeScaleFps; + m_logicTimeAccumulator += min(m_updateTime, targetFrameTime); + + if (m_logicTimeAccumulator >= targetFrameTime) + { + m_logicTimeAccumulator -= targetFrameTime; + TheGameClient->step(); + TheGameLogic->UPDATE(); + } + } + } } } // end perfGather @@ -842,8 +960,8 @@ extern HWND ApplicationHWnd; */ void GameEngine::execute( void ) { + FrameRateLimit* frameRateLimit = new FrameRateLimit(); - DWORD prevTime = timeGetTime(); #if defined(RTS_DEBUG) DWORD startTime = timeGetTime() / 1000; #endif @@ -913,39 +1031,31 @@ void GameEngine::execute( void ) } // perf { - - if (TheTacticalView->getTimeMultiplier()<=1 && !TheScriptEngine->isTimeFast()) { + Bool allowFpsLimit = TheTacticalView->getTimeMultiplier()<=1 && !TheScriptEngine->isTimeFast(); - // I'm disabling this in internal because many people need alt-tab capability. If you happen to be + // I'm disabling this in debug because many people need alt-tab capability. If you happen to be // doing performance tuning, please just change this on your local system. -MDC #if defined(RTS_DEBUG) - ::Sleep(1); // give everyone else a tiny time slice. + if (allowFpsLimit) + ::Sleep(1); // give everyone else a tiny time slice. #endif #if defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE) - if ( ! TheGlobalData->m_TiVOFastMode ) - #else //always allow this cheatkey if we're in a replaygame. - if ( ! (TheGlobalData->m_TiVOFastMode && TheGameLogic->isInReplayGame())) + allowFpsLimit &= !(!TheGameLogic->isGamePaused() && TheGlobalData->m_TiVOFastMode); + #else //always allow this cheat key if we're in a replay game. + allowFpsLimit &= !(!TheGameLogic->isGamePaused() && TheGlobalData->m_TiVOFastMode && TheGameLogic->isInReplayGame()); #endif - { - // limit the framerate - DWORD now = timeGetTime(); - DWORD limit = (1000.0f/m_maxFPS)-1; - while (TheGlobalData->m_useFpsLimit && (now - prevTime) < limit) - { - ::Sleep(0); - now = timeGetTime(); - } - //Int slept = now - prevTime; - //DEBUG_LOG(("delayed %d",slept)); - - prevTime = now; - - } - - } + { + // TheSuperHackers @bugfix xezon 05/08/2025 Re-implements the frame rate limiter + // with higher resolution counters to cap the frame rate more accurately to the desired limit. + allowFpsLimit &= TheGlobalData->m_useFpsLimit; + const Int maxFps = allowFpsLimit ? getFramesPerSecondLimit() : INT_MAX; + m_updateTime = frameRateLimit->wait(maxFps); + } + + } } } // perfgather for execute_loop @@ -961,6 +1071,7 @@ void GameEngine::execute( void ) } + delete frameRateLimit; } /** ----------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp index 3c1f04476f..d7b5970958 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp @@ -346,6 +346,10 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t) CASE_LABEL(MSG_META_HELP) #endif + CASE_LABEL(MSG_META_INCREASE_MAX_RENDER_FPS) + CASE_LABEL(MSG_META_DECREASE_MAX_RENDER_FPS) + CASE_LABEL(MSG_META_INCREASE_LOGIC_TIME_SCALE) + CASE_LABEL(MSG_META_DECREASE_LOGIC_TIME_SCALE) CASE_LABEL(MSG_META_TOGGLE_LOWER_DETAILS) CASE_LABEL(MSG_META_TOGGLE_CONTROL_BAR) CASE_LABEL(MSG_META_BEGIN_PATH_BUILD) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Display.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Display.cpp index 53de8f7b7b..c319db509d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Display.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Display.cpp @@ -131,6 +131,14 @@ void Display::updateViews( void ) } +void Display::stepViews( void ) +{ + + for( View *v = m_viewList; v; v = v->getNextView() ) + v->stepView(); + +} + /// Redraw the entire display void Display::draw( void ) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp index 0448be6d79..76228beb56 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GameClient.cpp @@ -773,6 +773,11 @@ void GameClient::update( void ) } } // end update +void GameClient::step() +{ + TheDisplay->step(); +} + void GameClient::updateHeadless() { // TheSuperHackers @info helmutbuhler 03/05/2025 diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index e841924b50..90691380d5 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -1883,26 +1883,30 @@ void InGameUI::update( void ) layout->runUpdate(); } - //Handle keyboard camera rotations - if( m_cameraRotatingLeft && !m_cameraRotatingRight ) + if (m_cameraRotatingLeft || m_cameraRotatingRight || m_cameraZoomingIn || m_cameraZoomingOut) { - //Keyboard rotate left - TheTacticalView->setAngle( TheTacticalView->getAngle() - TheGlobalData->m_keyboardCameraRotateSpeed ); - } - if( m_cameraRotatingRight && !m_cameraRotatingLeft ) - { - //Keyboard rotate right - TheTacticalView->setAngle( TheTacticalView->getAngle() + TheGlobalData->m_keyboardCameraRotateSpeed ); - } - if( m_cameraZoomingIn && !m_cameraZoomingOut ) - { - //Keyboard zoom in - TheTacticalView->zoomIn(); - } - if( m_cameraZoomingOut && !m_cameraZoomingIn ) - { - //Keyboard zoom out - TheTacticalView->zoomOut(); + // TheSuperHackers @tweak The camera rotation and zoom are now decoupled from the render update. + const Real fpsRatio = 30.0f / TheGameEngine->getUpdateFps(); + const Real rotateAngle = TheGlobalData->m_keyboardCameraRotateSpeed * fpsRatio; + const Real zoomHeight = 10.0f * fpsRatio; + + if( m_cameraRotatingLeft && !m_cameraRotatingRight ) + { + TheTacticalView->setAngle( TheTacticalView->getAngle() - rotateAngle ); + } + else if( m_cameraRotatingRight && !m_cameraRotatingLeft ) + { + TheTacticalView->setAngle( TheTacticalView->getAngle() + rotateAngle ); + } + + if( m_cameraZoomingIn && !m_cameraZoomingOut ) + { + TheTacticalView->zoom( -zoomHeight ); + } + else if( m_cameraZoomingOut && !m_cameraZoomingIn ) + { + TheTacticalView->zoom( +zoomHeight ); + } } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index 23e1f89344..2c38f0b37b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -32,6 +32,7 @@ #include "Common/AudioAffect.h" #include "Common/ActionManager.h" +#include "Common/FrameRateLimit.h" #include "Common/GameAudio.h" #include "Common/GameEngine.h" #include "Common/GameType.h" @@ -187,6 +188,91 @@ Bool hasThingsInProduction(PlayerType playerType) #endif // defined(RTS_DEBUG) || defined(_ALLOW_DEBUG_CHEATS_IN_RELEASE) +bool changeMaxRenderFps(FpsValueChange change) +{ + UnsignedInt maxRenderFps = TheGameEngine->getFramesPerSecondLimit(); + maxRenderFps = RenderFpsPreset::changeFpsValue(maxRenderFps, change); + + TheGameEngine->setFramesPerSecondLimit(maxRenderFps); + TheWritableGlobalData->m_useFpsLimit = (maxRenderFps != RenderFpsPreset::UncappedFpsValue); + + UnicodeString message; + + if (TheWritableGlobalData->m_useFpsLimit) + { + message = TheGameText->FETCH_OR_SUBSTITUTE_FORMAT("GUI:SetMaxRenderFps", L"Max Render FPS is %u", maxRenderFps); + } + else + { + message = TheGameText->FETCH_OR_SUBSTITUTE_FORMAT("GUI:SetUncappedRenderFps", L"Max Render FPS is uncapped"); + } + + TheInGameUI->messageNoFormat(message); + + return true; +} + +bool changeLogicTimeScale(FpsValueChange change) +{ + if (TheNetwork != NULL) + return false; + + const UnsignedInt maxRenderFps = TheGameEngine->getFramesPerSecondLimit(); + UnsignedInt maxRenderRemainder = LogicTimeScaleFpsPreset::StepFpsValue; + maxRenderRemainder -= maxRenderFps % LogicTimeScaleFpsPreset::StepFpsValue; + maxRenderRemainder %= LogicTimeScaleFpsPreset::StepFpsValue; + + UnsignedInt logicTimeScaleFps = TheGameEngine->getLogicTimeScaleFps(); + // Set the value to the max render fps value plus a bit when time scale is + // disabled. This ensures that the time scale does not re-enable with a + // 'surprise' value. + if (!TheGameEngine->isLogicTimeScaleEnabled()) + { + logicTimeScaleFps = maxRenderFps + maxRenderRemainder; + } + // Ceil the value at the max render fps value plus a bit so that the next fps + // value decrease would undercut the max render fps at the correct step value. + // Example: render fps 72 -> logic value ceiled to 75 -> decreased to 70. + logicTimeScaleFps = min(logicTimeScaleFps, maxRenderFps + maxRenderRemainder); + logicTimeScaleFps = LogicTimeScaleFpsPreset::changeFpsValue(logicTimeScaleFps, change); + + // Set value before potentially disabling it. + if (TheGameEngine->isLogicTimeScaleEnabled()) + { + TheGameEngine->setLogicTimeScaleFps(logicTimeScaleFps); + } + + TheGameEngine->enableLogicTimeScale(logicTimeScaleFps < maxRenderFps); + + // Set value after potentially enabling it. + if (TheGameEngine->isLogicTimeScaleEnabled()) + { + TheGameEngine->setLogicTimeScaleFps(logicTimeScaleFps); + } + + logicTimeScaleFps = TheGameEngine->getLogicTimeScaleFps(); + const UnsignedInt actualLogicTimeScaleFps = TheGameEngine->getActualLogicTimeScaleFps(); + const Real actualLogicTimeScaleRatio = TheGameEngine->getActualLogicTimeScaleRatio(); + + UnicodeString message; + + if (TheGameEngine->isLogicTimeScaleEnabled()) + { + message = TheGameText->FETCH_OR_SUBSTITUTE_FORMAT("GUI:SetLogicTimeScaleFps", L"Logic Time Scale FPS is %u (actual %u, ratio %.02f)", + logicTimeScaleFps, actualLogicTimeScaleFps, actualLogicTimeScaleRatio); + } + else + { + message = TheGameText->FETCH_OR_SUBSTITUTE_FORMAT("GUI:SetUncappedLogicTimeScaleFps", L"Logic Time Scale FPS is uncapped (actual %u, ratio %.02f)", + actualLogicTimeScaleFps, actualLogicTimeScaleRatio); + } + + TheInGameUI->messageNoFormat(message); + + return true; +} + + static Bool isSystemMessage( const GameMessage *msg ); enum{ DROPPED_MAX_PARTICLE_COUNT = 1000}; @@ -3185,6 +3271,46 @@ GameMessageDisposition CommandTranslator::translateGameMessage(const GameMessage disp = DESTROY_MESSAGE; break; + //----------------------------------------------------------------------------------------- + case GameMessage::MSG_META_INCREASE_MAX_RENDER_FPS: + { + if (changeMaxRenderFps(FpsValueChange_Increase)) + { + disp = DESTROY_MESSAGE; + } + break; + } + + //----------------------------------------------------------------------------------------- + case GameMessage::MSG_META_DECREASE_MAX_RENDER_FPS: + { + if (changeMaxRenderFps(FpsValueChange_Decrease)) + { + disp = DESTROY_MESSAGE; + } + break; + } + + //----------------------------------------------------------------------------------------- + case GameMessage::MSG_META_INCREASE_LOGIC_TIME_SCALE: + { + if (changeLogicTimeScale(FpsValueChange_Increase)) + { + disp = DESTROY_MESSAGE; + } + break; + } + + //----------------------------------------------------------------------------------------- + case GameMessage::MSG_META_DECREASE_LOGIC_TIME_SCALE: + { + if (changeLogicTimeScale(FpsValueChange_Decrease)) + { + disp = DESTROY_MESSAGE; + } + break; + } + //----------------------------------------------------------------------------------------- case GameMessage::MSG_META_TOGGLE_LOWER_DETAILS: { diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/LookAtXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/LookAtXlat.cpp index 2dbfdf474d..e11440494f 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/LookAtXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/LookAtXlat.cpp @@ -29,6 +29,7 @@ #include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine #include "Common/GameType.h" +#include "Common/GameEngine.h" #include "Common/MessageStream.h" #include "Common/Player.h" #include "Common/PlayerList.h" @@ -366,15 +367,19 @@ GameMessageDisposition LookAtTranslator::translateGameMessage(const GameMessage Int spin = msg->getArgument( 1 )->integer; + // TheSuperHackers @tweak The camera zoom is now decoupled from the render update. + const Real fpsRatio = 30.0f / TheGameEngine->getUpdateFps(); + const Real zoomHeight = 10.0f * fpsRatio; + if (spin > 0) { for ( ; spin > 0; spin--) - TheTacticalView->zoomIn(); + TheTacticalView->zoom( -zoomHeight ); } else { for ( ;spin < 0; spin++ ) - TheTacticalView->zoomOut(); + TheTacticalView->zoom( +zoomHeight ); } } @@ -408,7 +413,7 @@ GameMessageDisposition LookAtTranslator::translateGameMessage(const GameMessage // The scaling is based on the current logic rate, this provides a consistent scroll speed at all GameClient FPS // This also fixes scrolling within replays when fast forwarding due to the uncapped FPS // When the FPS is in excess of the expected frame rate, the ratio will reduce the offset of the cameras movement - const Real logicToFpsRatio = TheGlobalData->m_framesPerSecondLimit / TheDisplay->getCurrentFPS(); + const Real logicToFpsRatio = 30.0f / TheGameEngine->getUpdateFps(); switch (m_scrollType) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp index e8d15503a6..7b854dece5 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/MetaEvent.cpp @@ -151,6 +151,10 @@ static const LookupListRec GameMessageMetaTypeNames[] = { "PLACE_BEACON", GameMessage::MSG_META_PLACE_BEACON }, { "DELETE_BEACON", GameMessage::MSG_META_REMOVE_BEACON }, { "OPTIONS", GameMessage::MSG_META_OPTIONS }, + { "INCREASE_MAX_RENDER_FPS", GameMessage::MSG_META_INCREASE_MAX_RENDER_FPS }, + { "DECREASE_MAX_RENDER_FPS", GameMessage::MSG_META_DECREASE_MAX_RENDER_FPS }, + { "INCREASE_LOGIC_TIME_SCALE", GameMessage::MSG_META_INCREASE_LOGIC_TIME_SCALE }, + { "DECREASE_LOGIC_TIME_SCALE", GameMessage::MSG_META_DECREASE_LOGIC_TIME_SCALE }, { "TOGGLE_LOWER_DETAILS", GameMessage::MSG_META_TOGGLE_LOWER_DETAILS }, { "TOGGLE_CONTROL_BAR", GameMessage::MSG_META_TOGGLE_CONTROL_BAR }, { "BEGIN_PATH_BUILD", GameMessage::MSG_META_BEGIN_PATH_BUILD }, @@ -722,6 +726,50 @@ MetaMapRec *MetaMap::getMetaMapRec(GameMessage::Type t) // TheSuperHackers @info A default mapping for MSG_META_SELECT_ALL_AIRCRAFT would be useful for Generals // but is not recommended, because it will cause key mapping conflicts with original game languages. + { + // Is useful for Generals and Zero Hour. + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_INCREASE_MAX_RENDER_FPS); + if (map->m_key == MK_NONE) + { + map->m_key = MK_KPPLUS; + map->m_transition = DOWN; + map->m_modState = CTRL; + map->m_usableIn = COMMANDUSABLE_EVERYWHERE; + } + } + { + // Is useful for Generals and Zero Hour. + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_DECREASE_MAX_RENDER_FPS); + if (map->m_key == MK_NONE) + { + map->m_key = MK_KPMINUS; + map->m_transition = DOWN; + map->m_modState = CTRL; + map->m_usableIn = COMMANDUSABLE_EVERYWHERE; + } + } + { + // Is useful for Generals and Zero Hour. + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_INCREASE_LOGIC_TIME_SCALE); + if (map->m_key == MK_NONE) + { + map->m_key = MK_KPPLUS; + map->m_transition = DOWN; + map->m_modState = SHIFT_CTRL; + map->m_usableIn = COMMANDUSABLE_EVERYWHERE; + } + } + { + // Is useful for Generals and Zero Hour. + MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_DECREASE_LOGIC_TIME_SCALE); + if (map->m_key == MK_NONE) + { + map->m_key = MK_KPMINUS; + map->m_transition = DOWN; + map->m_modState = SHIFT_CTRL; + map->m_usableIn = COMMANDUSABLE_EVERYWHERE; + } + } { // Is mostly useful for Generals. MetaMapRec *map = TheMetaMap->getMetaMapRec(GameMessage::MSG_META_TOGGLE_FAST_FORWARD_REPLAY); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/View.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/View.cpp index e9ca5dbbf6..58b615b06b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/View.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/View.cpp @@ -125,14 +125,9 @@ View *View::prependViewToList( View *list ) return this; } -void View::zoomIn( void ) +void View::zoom( Real height ) { - setHeightAboveGround(getHeightAboveGround() - 10.0f); -} - -void View::zoomOut( void ) -{ - setHeightAboveGround(getHeightAboveGround() + 10.0f); + setHeightAboveGround(getHeightAboveGround() + height); } /** diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h index 18539a53e2..80c07119da 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h @@ -81,6 +81,7 @@ class W3DDisplay : public Display virtual Bool isClippingEnabled( void ) { return m_isClippedEnabled; } virtual void enableClipping( Bool onoff ) { m_isClippedEnabled = onoff; } + virtual void step(); ///< Do one fixed time step virtual void draw( void ); ///< redraw the entire display /// @todo Replace these light management routines with a LightManager singleton @@ -162,6 +163,7 @@ class W3DDisplay : public Display void calculateTerrainLOD(void); ///< Calculate terrain LOD. void renderLetterBox(UnsignedInt time); ///< draw letter box border void updateAverageFPS(void); ///< figure out the average fps over the last 30 frames. + static Bool isTimeFrozen(); Byte m_initialized; ///< TRUE when system is initialized LightClass *m_myLight[LightEnvironmentClass::MAX_LIGHTS]; ///< light hack for now diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h index 76eaa651d2..b9242dda21 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DView.h @@ -61,8 +61,8 @@ typedef struct Real cameraAngle[MAX_WAYPOINTS+2]; // Camera Angle; Int timeMultiplier[MAX_WAYPOINTS+2]; // Time speedup factor. Real groundHeight[MAX_WAYPOINTS+1]; // Ground height. - Int totalTimeMilliseconds; // Num of ms to do this movement. - Int elapsedTimeMilliseconds; // Time since start. + Real totalTimeMilliseconds; // Num of ms to do this movement. + Real elapsedTimeMilliseconds; // Time since start. Real totalDistance; // Total length of paths. Real curSegDistance; // How far we are along the current seg. Int shutter; @@ -140,6 +140,7 @@ class W3DView : public View, public SubsystemInterface virtual void reset( void ); virtual void drawView( void ); ///< draw this view virtual void updateView(void); ///isTimeFrozen() && !TheTacticalView->isCameraMovementFinished()) + return true; + + if (TheScriptEngine->isTimeFrozenDebug()) + return true; + + if (TheScriptEngine->isTimeFrozenScript()) + return true; + + if (TheGameLogic->isGamePaused()) + return true; + + return false; +} + +// TheSuperHackers @tweak xezon 12/08/2025 The WW3D Sync is no longer tied +// to the render update, but is advanced separately for every fixed time step. +void W3DDisplay::step() +{ + // TheSuperHackers @info This will wrap in 1205 hours at 30 fps logic step. + static UnsignedInt syncTime = 0; + + extern HWND ApplicationHWnd; + if (ApplicationHWnd && ::IsIconic(ApplicationHWnd)) { + return; + } + + if (TheGlobalData->m_headless) + return; + + Bool freezeTime = isTimeFrozen(); + + if (!freezeTime) + { + syncTime += (UnsignedInt)TheW3DFrameLengthInMsec; + + if (TheScriptEngine->isTimeFast()) + { + return; + } + } + + WW3D::Sync( syncTime ); + + stepViews(); +} + //DECLARE_PERF_TIMER(BigAssRenderLoop) // W3DDisplay::draw =========================================================== @@ -1690,7 +1739,6 @@ Int W3DDisplay::getLastFrameDrawCalls() void W3DDisplay::draw( void ) { //USE_PERF_TIMER(W3DDisplay_draw) - static UnsignedInt syncTime = 0; extern HWND ApplicationHWnd; if (ApplicationHWnd && ::IsIconic(ApplicationHWnd)) { @@ -1735,10 +1783,6 @@ void W3DDisplay::draw( void ) TheInGameUI->message( UnicodeString( L"-stats is running, at interval: %d." ), TheGlobalData->m_statsInterval ); } } - - - - #endif // compute debug statistics for display later @@ -1779,21 +1823,19 @@ void W3DDisplay::draw( void ) // //PredictiveLODOptimizerClass::Optimize_LODs( 5000 ); - Bool freezeTime = TheTacticalView->isTimeFrozen() && !TheTacticalView->isCameraMovementFinished(); - freezeTime = freezeTime || TheScriptEngine->isTimeFrozenDebug() || TheScriptEngine->isTimeFrozenScript(); - freezeTime = freezeTime || TheGameLogic->isGamePaused(); + Bool freezeTime = isTimeFrozen(); // hack to let client spin fast in network games but still do effects at the same pace. -MDC static UnsignedInt lastFrame = ~0; - freezeTime = freezeTime || (lastFrame == TheGameClient->getFrame()); + freezeTime = freezeTime || (TheNetwork != NULL && lastFrame == TheGameClient->getFrame()); lastFrame = TheGameClient->getFrame(); /// @todo: I'm assuming the first view is our main 3D view. W3DView *primaryW3DView=(W3DView *)getFirstView(); + if (!freezeTime && TheScriptEngine->isTimeFast()) { primaryW3DView->updateCameraMovements(); // Update camera motion effects. - syncTime += TheW3DFrameLengthInMsec; return; } @@ -1822,21 +1864,9 @@ void W3DDisplay::draw( void ) } } - if (!freezeTime) - { - /// @todo Decouple framerate from timestep - // for now, use constant time steps to avoid animations running independent of framerate - syncTime += TheW3DFrameLengthInMsec; - // allow W3D to update its internals - // WW3D::Sync( GetTickCount() ); - } - WW3D::Sync( syncTime ); - - // Fast & Frozen time limits the time to 33 fps. - Int minTime = 30; - static Int prevTime = timeGetTime(), now; - + static Int now; now=timeGetTime(); + if (TheTacticalView->getTimeMultiplier()>1) { static Int timeMultiplierCounter = 1; @@ -1846,28 +1876,9 @@ void W3DDisplay::draw( void ) timeMultiplierCounter = TheTacticalView->getTimeMultiplier(); // limit the framerate, because while fast time is on, the game logic is running as fast as it can. } - else - { - now = timeGetTime(); - prevTime = now - minTime; // do the first frame immediately. - } - do { - { - if(TheGlobalData->m_loadScreenRender != TRUE) - { - - // limit the framerate - while(TheGlobalData->m_useFpsLimit && (now - prevTime) < minTime-1) - { - now = timeGetTime(); - } - prevTime = now; - } - } - // update all views of the world - recomputes data which will affect drawing if (DX8Wrapper::_Get_D3D_Device8() && (DX8Wrapper::_Get_D3D_Device8()->TestCooperativeLevel()) == D3D_OK) { //Checking if we have the device before updating views because the heightmap crashes otherwise while diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp index ae9ab7f442..6629fb9c64 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DView.cpp @@ -38,6 +38,7 @@ // USER INCLUDES ////////////////////////////////////////////////////////////////////////////////// #include "Common/BuildAssistant.h" +#include "Common/GameEngine.h" #include "Common/GlobalData.h" #include "Common/Module.h" #include "Common/RandomValue.h" @@ -100,7 +101,7 @@ // 30 fps -Int TheW3DFrameLengthInMsec = 1000/LOGICFRAMES_PER_SECOND; // default is 33msec/frame == 30fps. but we may change it depending on sys config. +Real TheW3DFrameLengthInMsec = MSEC_PER_LOGICFRAME_REAL; // default is 33msec/frame == 30fps. but we may change it depending on sys config. static const Int MAX_REQUEST_CACHE_SIZE = 40; // Any size larger than 10, or examine code below for changes. jkmcd. static const Real DRAWABLE_OVERSCAN = 75.0f; ///< 3D world coords of how much to overscan in the 3D screen region @@ -408,7 +409,9 @@ void W3DView::buildCameraTransform( Matrix3D *transform ) transform->Look_At( sourcePos, targetPos, 0 ); //WST 11/12/2002 New camera shaker system - CameraShakerSystem.Timestep(1.0f/30.0f); + // TheSuperHackers @tweak The camera shaker is now decoupled from the render update. + const Real logicTimeScaleOverFpsRatio = TheGameEngine->getActualLogicTimeScaleOverFpsRatio(); + CameraShakerSystem.Timestep(TheW3DFrameLengthInMsec * logicTimeScaleOverFpsRatio); CameraShakerSystem.Update_Camera_Shaker(sourcePos, &m_shakerAngles); transform->Rotate_X(m_shakerAngles.X); transform->Rotate_Y(m_shakerAngles.Y); @@ -1053,7 +1056,9 @@ Bool W3DView::updateCameraMovements() didUpdate = true; } else if (m_doingMoveCameraOnWaypointPath) { m_previousLookAtPosition = *getPosition(); - moveAlongWaypointPath(TheW3DFrameLengthInMsec); + // TheSuperHackers @tweak The scripted camera movement is now decoupled from the render update. + const Real logicTimeScaleOverFpsRatio = TheGameEngine->getActualLogicTimeScaleOverFpsRatio(); + moveAlongWaypointPath(TheW3DFrameLengthInMsec * logicTimeScaleOverFpsRatio); didUpdate = true; } if (m_doingScriptedCameraLock) @@ -1077,6 +1082,50 @@ void W3DView::updateView(void) UPDATE(); } +// TheSuperHackers @tweak xezon 12/08/2025 The drawable update is no longer tied to the +// render update, but it advanced separately for every fixed time step. This ensures that +// things like vehicle wheels no longer spin too fast on high frame rates or keep spinning +// on game pause. +// The camera shaker is also no longer tied to the render update. The shake does sharp shakes +// on every fixed time step, and is not intended to have linear interpolation during the +// render update. +void W3DView::stepView() +{ + // + // Process camera shake + // + if (m_shakeIntensity > 0.01f) + { + m_shakeOffset.x = m_shakeIntensity * m_shakeAngleCos; + m_shakeOffset.y = m_shakeIntensity * m_shakeAngleSin; + + // fake a stiff spring/damper + const Real dampingCoeff = 0.75f; + m_shakeIntensity *= dampingCoeff; + + // spring is so "stiff", it pulls 180 degrees opposite each frame + m_shakeAngleCos = -m_shakeAngleCos; + m_shakeAngleSin = -m_shakeAngleSin; + } + else + { + m_shakeIntensity = 0.0f; + m_shakeOffset.x = 0.0f; + m_shakeOffset.y = 0.0f; + } + + if (TheScriptEngine->isTimeFast()) { + return; // don't draw - makes it faster :) jba. + } + + Region3D axisAlignedRegion; + getAxisAlignedViewRegion(axisAlignedRegion); + + // render all of the visible Drawables + /// @todo this needs to use a real region partition or something + TheGameClient->iterateDrawablesInRegion( &axisAlignedRegion, drawDrawable, this ); +} + //DECLARE_PERF_TIMER(W3DView_updateView) void W3DView::update(void) { @@ -1289,29 +1338,11 @@ void W3DView::update(void) } // // Process camera shake - /// @todo Make this framerate-independent // if (m_shakeIntensity > 0.01f) { - m_shakeOffset.x = m_shakeIntensity * m_shakeAngleCos; - m_shakeOffset.y = m_shakeIntensity * m_shakeAngleSin; - - // fake a stiff spring/damper - const Real dampingCoeff = 0.75f; - m_shakeIntensity *= dampingCoeff; - - // spring is so "stiff", it pulls 180 degrees opposite each frame - m_shakeAngleCos = -m_shakeAngleCos; - m_shakeAngleSin = -m_shakeAngleSin; - recalcCamera = true; } - else - { - m_shakeIntensity = 0.0f; - m_shakeOffset.x = 0.0f; - m_shakeOffset.y = 0.0f; - } //Process New C3 Camera Shaker system if (CameraShakerSystem.IsCameraShaking()) @@ -1380,14 +1411,6 @@ void W3DView::update(void) // Give the terrain a chance to refresh animaing (Seismic) regions, if any. TheTerrainVisual->updateSeismicSimulations(); #endif - - Region3D axisAlignedRegion; - getAxisAlignedViewRegion(axisAlignedRegion); - - // render all of the visible Drawables - /// @todo this needs to use a real region partition or something - if (WW3D::Get_Frame_Time()) //make sure some time actually elapsed - TheGameClient->iterateDrawablesInRegion( &axisAlignedRegion, drawDrawable, this ); } //------------------------------------------------------------------------------------------------- @@ -3092,7 +3115,7 @@ void W3DView::pitchCameraOneFrame(void) // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ -void W3DView::moveAlongWaypointPath(Int milliseconds) +void W3DView::moveAlongWaypointPath(Real milliseconds) { m_mcwpInfo.elapsedTimeMilliseconds += milliseconds; if (TheGlobalData->m_disableCameraMovement) { diff --git a/GeneralsMD/Code/Tools/GUIEdit/Source/GUIEdit.cpp b/GeneralsMD/Code/Tools/GUIEdit/Source/GUIEdit.cpp index 44d0353acc..65ba591a26 100644 --- a/GeneralsMD/Code/Tools/GUIEdit/Source/GUIEdit.cpp +++ b/GeneralsMD/Code/Tools/GUIEdit/Source/GUIEdit.cpp @@ -77,8 +77,6 @@ #include "W3DDevice/Common/W3DFunctionLexicon.h" #include "W3DDevice/GameClient/W3DGameWindowManager.h" -#include "W3DDevice/GameClient/W3DDisplay.h" -#include "W3DDevice/GameClient/W3DGameWindowManager.h" #include "W3DDevice/GameClient/W3DGameFont.h" #include "W3DDevice/GameClient/W3DDisplayStringManager.h" #include "GameClient/Keyboard.h" diff --git a/GeneralsMD/Code/Tools/WorldBuilder/src/wbview3d.cpp b/GeneralsMD/Code/Tools/WorldBuilder/src/wbview3d.cpp index 04ae90a499..4efb18e36a 100644 --- a/GeneralsMD/Code/Tools/WorldBuilder/src/wbview3d.cpp +++ b/GeneralsMD/Code/Tools/WorldBuilder/src/wbview3d.cpp @@ -183,6 +183,7 @@ class PlaceholderView : public View virtual void drawView( void ) {}; ///< Render the world visible in this view. virtual void updateView( void ) {}; ///< Render the world visible in this view. + virtual void stepView() {}; ///< Update view for every fixed time step virtual void setZoomLimited( Bool limit ) {} ///< limit the zoom height virtual Bool isZoomLimited( void ) { return TRUE; } ///< get status of zoom limit