From 7d2eeb55a66d7647fc04420d08b35eb7ba0b56af Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Thu, 25 Jul 2024 04:29:02 +0200 Subject: [PATCH 01/34] Revert "Soft particles: Disable Particle Stage "softeningRadius" keyword" This reverts commit 2e0b093c79f787cf3d1d5f00e8149a1ac7c4554c so the "softeningRadius" keyword in Particle Stages, as defined by TheDarkMod, is supported. This breaks the game API! --- neo/framework/DeclParticle.cpp | 12 +++--------- neo/framework/DeclParticle.h | 4 +--- neo/renderer/Model_prt.cpp | 7 ++----- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/neo/framework/DeclParticle.cpp b/neo/framework/DeclParticle.cpp index 2f8b843f0..7cd4fcc11 100644 --- a/neo/framework/DeclParticle.cpp +++ b/neo/framework/DeclParticle.cpp @@ -410,10 +410,7 @@ idParticleStage *idDeclParticle::ParseParticleStage( idLexer &src ) { continue; } if ( !token.Icmp( "softeningRadius" ) ) { // #3878 soft particles - common->Warning( "Particle %s from %s has stage with \"softeningRadius\" attribute, which is currently ignored (we soften all suitable particles)\n", - this->GetName(), src.GetFileName() ); - // DG: disable this for now to avoid breaking the game ABI - //stage->softeningRadius = src.ParseFloat(); + stage->softeningRadius = src.ParseFloat(); continue; } @@ -740,9 +737,7 @@ idParticleStage::idParticleStage( void ) { hidden = false; boundsExpansion = 0.0f; bounds.Clear(); - // DG: disable softeningRadius for now to avoid breaking the game ABI - // (will always behave like if softeningRadius = -2.0f) - //softeningRadius = -2.0f; // -2 means "auto" - #3878 soft particles + softeningRadius = -2.0f; // -2 means "auto" - #3878 soft particles } /* @@ -816,8 +811,7 @@ void idParticleStage::Default() { randomDistribution = true; entityColor = false; cycleMsec = ( particleLife + deadTime ) * 1000; - // DG: disable softeningRadius for now to avoid breaking game ABI - //softeningRadius = -2.0f; // -2 means "auto" - #3878 soft particles + softeningRadius = -2.0f; // -2 means "auto" - #3878 soft particles } /* diff --git a/neo/framework/DeclParticle.h b/neo/framework/DeclParticle.h index edc2cb051..8819cef45 100644 --- a/neo/framework/DeclParticle.h +++ b/neo/framework/DeclParticle.h @@ -202,9 +202,7 @@ class idParticleStage { This is more flexible even when not using soft particles, as modelDepthHack can be turned off for specific stages to stop them poking through walls. */ - // DG: disable this for now because it breaks the game DLL's ABI (re-enable in dhewm3 1.6.0 or 2.0.0) - // (this header is part of the SDK) - //float softeningRadius; + float softeningRadius; }; diff --git a/neo/renderer/Model_prt.cpp b/neo/renderer/Model_prt.cpp index 09d2c4aba..a603d5f7f 100644 --- a/neo/renderer/Model_prt.cpp +++ b/neo/renderer/Model_prt.cpp @@ -310,14 +310,11 @@ void idRenderModelPrt::SetSofteningRadii() for ( int i = 0; i < particleSystem->stages.Num(); ++i ) { const idParticleStage* ps = particleSystem->stages[i]; - // DG: for now softeningRadius isn't configurable to avoid breaking the game DLL's ABI - // => always behave like if ps->softeningRadius == -2, which means "auto" - // (doesn't make a difference, so far only TDM particles set the softeningRadius) - /* if ( ps->softeningRadius > -2.0f ) // User has specified a setting + if ( ps->softeningRadius > -2.0f ) // User has specified a setting { softeningRadii[i] = ps->softeningRadius; } - else */ if ( ps->orientation == POR_VIEW ) // Only view-aligned particle stages qualify for softening + else if ( ps->orientation == POR_VIEW ) // Only view-aligned particle stages qualify for softening { float diameter = Max( ps->size.from, ps->size.to ); float scale = Max( ps->aspect.from, ps->aspect.to ); From 2f1b2b312ed56df12001a553f8acb74f23c0867c Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Thu, 27 Jun 2024 16:37:55 +0200 Subject: [PATCH 02/34] Configurable FPS: Add com_gameHz, use for USERCMD_HZ and USERCMD_MSEC The CVar com_gameHz can be used to set the desired framerate, so we're no longer stuck to hardcoded 60fps. To minimize the amount of code that needs changes, USERCMD_HZ and USERCMD_MSEC are now #defines to values holding the corresponding values based on the current com_gameHz value. The game DLLs (need to) have their own variables for this, and those are set through the new idGame::SetGameHz() (yes, this means the game API will be broken; I guess that's unavoidable for this feature anyway). This is just the first step, there's more code (that currently assumes 16ms ticks) that will be adjusted in additional commits, and the scripts also need a similar hack for their GAME_FPS, GAME_FRAMETIME and CHAINGUN_FIRE_SKIPFRAMES #defines. --- neo/d3xp/Game_local.cpp | 10 ++++++++ neo/d3xp/Game_local.h | 8 ++++++ neo/d3xp/physics/Force_Drag.cpp | 2 ++ neo/framework/Common.cpp | 44 +++++++++++++++++++++++++++++---- neo/framework/Common.h | 6 +++++ neo/framework/Game.h | 3 +++ neo/framework/UsercmdGen.h | 12 +++++++-- neo/game/Game_local.cpp | 8 ++++++ neo/game/Game_local.h | 11 ++++++++- neo/game/physics/Force_Drag.cpp | 2 ++ neo/sound/snd_local.h | 4 ++- 11 files changed, 101 insertions(+), 9 deletions(-) diff --git a/neo/d3xp/Game_local.cpp b/neo/d3xp/Game_local.cpp index 51c113ba2..57456a5b6 100644 --- a/neo/d3xp/Game_local.cpp +++ b/neo/d3xp/Game_local.cpp @@ -5043,3 +5043,13 @@ idGameLocal::GetMapLoadingGUI =============== */ void idGameLocal::GetMapLoadingGUI( char gui[ MAX_STRING_CHARS ] ) { } + +// DG: Added for configurable framerate +void idGameLocal::SetGameHz( float hz, float frametime, float ticScaleFactor ) +{ + gameHz = hz; + gameMsec = frametime; + msec = frametime; // TODO: maybe msec must be scaled + // TODO: adjust this->fast and this->slow + gameTicScale = ticScaleFactor; +} diff --git a/neo/d3xp/Game_local.h b/neo/d3xp/Game_local.h index f9a969352..94b80876a 100644 --- a/neo/d3xp/Game_local.h +++ b/neo/d3xp/Game_local.h @@ -300,6 +300,11 @@ class idGameLocal : public idGame { int time; // in msec int msec; // time since last update in milliseconds + // DG: added for configurable framerate + int gameMsec; // length of one frame/tic in milliseconds - TODO: make float? + int gameHz; // current gameHz value (tic-rate, FPS) + float gameTicScale; // gameHz/60 factor to multiply delays in tics (that assume 60fps) with + int vacuumAreaNum; // -1 if level doesn't have any outside areas gameType_t gameType; @@ -397,6 +402,9 @@ class idGameLocal : public idGame { virtual void GetMapLoadingGUI( char gui[ MAX_STRING_CHARS ] ); + // DG: Added for configurable framerate + virtual void SetGameHz( float hz, float frametime, float ticScaleFactor ); + // ---------------------- Public idGameLocal Interface ------------------- void Printf( const char *fmt, ... ) const id_attribute((format(printf,2,3))); diff --git a/neo/d3xp/physics/Force_Drag.cpp b/neo/d3xp/physics/Force_Drag.cpp index f8682a11e..581a039a8 100644 --- a/neo/d3xp/physics/Force_Drag.cpp +++ b/neo/d3xp/physics/Force_Drag.cpp @@ -33,6 +33,8 @@ If you have questions concerning this license or the applicable additional terms #include "physics/Force_Drag.h" +#include "Game_local.h" + CLASS_DECLARATION( idForce, idForce_Drag ) END_CLASS diff --git a/neo/framework/Common.cpp b/neo/framework/Common.cpp index 95d11fb20..e121ee7d6 100644 --- a/neo/framework/Common.cpp +++ b/neo/framework/Common.cpp @@ -109,13 +109,20 @@ idCVar com_dbgServerAdr( "com_dbgServerAdr", "localhost", CVAR_SYSTEM | CVAR_ARC idCVar com_product_lang_ext( "com_product_lang_ext", "1", CVAR_INTEGER | CVAR_SYSTEM | CVAR_ARCHIVE, "Extension to use when creating language files." ); +// DG: the next block is for configurable framerate +idCVar com_gameHz( "com_gameHz", "60", CVAR_INTEGER | CVAR_ARCHIVE | CVAR_SYSTEM, "Frames per second the game runs at", 10, 480 ); // TODO: make it float? make it default to 62.5? +// the next three values will be set based on com_gameHz +int com_gameHzVal = 60; +int com_gameFrameTime = 16; // length of one frame in msec, 1000 / com_gameHz +float com_gameTicScale = 1.0f; // com_gameHzVal/60.0f, multiply stuff assuming one tic is 16ms with this + // com_speeds times int time_gameFrame; int time_gameDraw; int time_frontend; // renderSystem frontend time int time_backend; // renderSystem backend time -int com_frameTime; // time for the current frame in milliseconds +int com_frameTime; // time for the current frame in milliseconds - TODO: DG: make it double? int com_frameNumber; // variable frame number volatile int com_ticNumber; // 60 hz tics int com_editors; // currently opened editor(s) @@ -219,6 +226,8 @@ class idCommonLocal : public idCommon { void PrintLoadingMessage( const char *msg ); void FilterLangList( idStrList* list, idStr lang ); + void UpdateGameHz(); // DG: for configurable framerate + bool com_fullyInitialized; bool com_refreshOnPrint; // update the screen every print for dmap int com_errorEntered; // 0, ERP_DROP, etc @@ -2439,12 +2448,17 @@ void idCommonLocal::Frame( void ) { } } + // DG: for configurable framerate + if ( com_gameHz.IsModified() ) { + UpdateGameHz(); + } + eventLoop->RunEventLoop(); // DG: prepare new ImGui frame - I guess this is a good place, as all new events should be available? D3::ImGuiHooks::NewFrame(); - com_frameTime = com_ticNumber * USERCMD_MSEC; + com_frameTime = com_ticNumber * USERCMD_MSEC; // TODO: can we continue using 60hz tics and still run at higher fps? com_frameNumber vs com_ticNumber suggests that.. idAsyncNetwork::RunFrame(); @@ -2571,7 +2585,7 @@ idCommonLocal::Async ================= */ void idCommonLocal::Async( void ) { - int msec = Sys_Milliseconds(); + int msec = Sys_Milliseconds(); // TODO: make double? if ( !lastTicMsec ) { lastTicMsec = msec - USERCMD_MSEC; } @@ -2582,7 +2596,7 @@ void idCommonLocal::Async( void ) { return; } - int ticMsec = USERCMD_MSEC; + int ticMsec = USERCMD_MSEC; // TODO: make float? // the number of msec per tic can be varies with the timescale cvar float timescale = com_timescale.GetFloat(); @@ -2734,6 +2748,7 @@ void idCommonLocal::LoadGameDLL( void ) { // initialize the game object if ( game != NULL ) { + game->SetGameHz( com_gameHzVal, com_gameFrameTime, com_gameTicScale ); // DG: make sure it knows the ticrate game->Init(); } } @@ -3081,7 +3096,7 @@ void idCommonLocal::Init( int argc, char **argv ) { Sys_Error( "Error during initialization" ); } - async_timer = SDL_AddTimer(USERCMD_MSEC, AsyncTimer, NULL); + async_timer = SDL_AddTimer(USERCMD_MSEC, AsyncTimer, NULL); // TODO: maybe rework the whole async timer thing, might be too imprecise if (!async_timer) Sys_Error("Error while starting the async timer: %s", SDL_GetError()); @@ -3215,6 +3230,8 @@ void idCommonLocal::InitGame( void ) { // if any archived cvars are modified after this, we will trigger a writing of the config file cvarSystem->ClearModifiedFlags( CVAR_ARCHIVE ); + UpdateGameHz(); // DG: for configurable framerate + // init the user command input code usercmdGen->Init(); @@ -3328,6 +3345,23 @@ void idCommonLocal::ShutdownGame( bool reloading ) { fileSystem->Shutdown( reloading ); } +// DG: for configurable framerate +void idCommonLocal::UpdateGameHz() +{ + com_gameHz.ClearModified(); + com_gameHzVal = com_gameHz.GetInteger(); + // only rounding up the frame time a little bit, so for 144hz (6.94ms) it becomes 7ms, + // but for 60Hz (16.6667ms) it remains 16ms, like before + com_gameFrameTime = ( 1000.0f / com_gameHzVal ) + 0.1f; // TODO: idMath::Rint ? + com_gameTicScale = com_gameHzVal / 60.0f; // TODO: or / 62.5 ? + + Printf( "Running the game at com_gameHz = %dHz, frametime %dms\n", com_gameHzVal, com_gameFrameTime ); + + if ( game != NULL ) { + game->SetGameHz( com_gameHzVal, com_gameFrameTime, com_gameTicScale ); + } +} + // DG: below here are hacks to allow adding callbacks and exporting additional functions to the // Game DLL without breaking the ABI. See Common.h for longer explanation... diff --git a/neo/framework/Common.h b/neo/framework/Common.h index 3262e6cc0..00315b439 100644 --- a/neo/framework/Common.h +++ b/neo/framework/Common.h @@ -77,6 +77,12 @@ extern idCVar com_enableDebuggerServer; extern idCVar com_dbgClientAdr; extern idCVar com_dbgServerAdr; +// DG: the next block is for configurable framerate +extern idCVar com_gameHz; +extern int com_gameHzVal; +extern int com_gameFrameTime; // round(1000.0f / gameHzVal), I guess +extern float com_gameTicScale; // com_gameHzVal/60.0f, multiply stuff assuming one tic is 16ms with this + extern int time_gameFrame; // game logic time extern int time_gameDraw; // game present time extern int time_frontend; // renderer frontend time diff --git a/neo/framework/Game.h b/neo/framework/Game.h index af64d2c83..54711c6ad 100644 --- a/neo/framework/Game.h +++ b/neo/framework/Game.h @@ -197,6 +197,9 @@ class idGame { virtual bool DownloadRequest( const char *IP, const char *guid, const char *paks, char urls[ MAX_STRING_CHARS ] ) = 0; virtual void GetMapLoadingGUI( char gui[ MAX_STRING_CHARS ] ) = 0; + + // DG: Added for configurable framerate + virtual void SetGameHz( float hz, float frametime, float ticScaleFactor ) = 0; }; extern idGame * game; diff --git a/neo/framework/UsercmdGen.h b/neo/framework/UsercmdGen.h index c9e9ecaee..09dd5e9a1 100644 --- a/neo/framework/UsercmdGen.h +++ b/neo/framework/UsercmdGen.h @@ -37,8 +37,16 @@ If you have questions concerning this license or the applicable additional terms =============================================================================== */ -const int USERCMD_HZ = 60; // 60 frames per second -const int USERCMD_MSEC = 1000 / USERCMD_HZ; +// DG: The framerate/ticrate is now configurable. This hacks lets us continue using USERCMD_HZ/MSEC +//const int USERCMD_HZ = 60; // 60 frames per second +//const int USERCMD_MSEC = 1000 / USERCMD_HZ; +#ifdef GAME_DLL // in the game DLLs we can't access com_gameHzVal, so it's mirrored in idGameLocal + #define USERCMD_HZ gameLocal.gameHz + #define USERCMD_MSEC gameLocal.gameMsec +#else + #define USERCMD_HZ com_gameHzVal + #define USERCMD_MSEC com_gameFrameTime +#endif // usercmd_t->button bits const int BUTTON_ATTACK = BIT(0); diff --git a/neo/game/Game_local.cpp b/neo/game/Game_local.cpp index 4ca98d6f8..2a873190c 100644 --- a/neo/game/Game_local.cpp +++ b/neo/game/Game_local.cpp @@ -4443,3 +4443,11 @@ idGameLocal::GetMapLoadingGUI =============== */ void idGameLocal::GetMapLoadingGUI( char gui[ MAX_STRING_CHARS ] ) { } + +// DG: Added for configurable framerate +void idGameLocal::SetGameHz( float hz, float frametime, float ticScaleFactor ) +{ + gameHz = hz; + gameMsec = msec = frametime; + gameTicScale = ticScaleFactor; +} diff --git a/neo/game/Game_local.h b/neo/game/Game_local.h index df7456d1c..848f85e8f 100644 --- a/neo/game/Game_local.h +++ b/neo/game/Game_local.h @@ -272,7 +272,13 @@ class idGameLocal : public idGame { int framenum; int previousTime; // time in msec of last frame int time; // in msec - static const int msec = USERCMD_MSEC; // time since last update in milliseconds + //static const int msec = USERCMD_MSEC; // time since last update in milliseconds + + int msec; // time since last update in milliseconds - DG: just mirrors gameMsec (in d3xp it's modified for slowmo) + // DG: added for configurable framerate + int gameMsec; // length of one frame/tic in milliseconds - TODO: make float? + int gameHz; // current gameHz value (tic-rate, FPS) + float gameTicScale; // gameHz/60 factor to multiply delays in tics (that assume 60fps) with int vacuumAreaNum; // -1 if level doesn't have any outside areas @@ -338,6 +344,9 @@ class idGameLocal : public idGame { virtual bool DownloadRequest( const char *IP, const char *guid, const char *paks, char urls[ MAX_STRING_CHARS ] ); + // DG: Added for configurable framerate + virtual void SetGameHz( float hz, float frametime, float ticScaleFactor ); + // ---------------------- Public idGameLocal Interface ------------------- void Printf( const char *fmt, ... ) const id_attribute((format(printf,2,3))); diff --git a/neo/game/physics/Force_Drag.cpp b/neo/game/physics/Force_Drag.cpp index f8682a11e..581a039a8 100644 --- a/neo/game/physics/Force_Drag.cpp +++ b/neo/game/physics/Force_Drag.cpp @@ -33,6 +33,8 @@ If you have questions concerning this license or the applicable additional terms #include "physics/Force_Drag.h" +#include "Game_local.h" + CLASS_DECLARATION( idForce, idForce_Drag ) END_CLASS diff --git a/neo/sound/snd_local.h b/neo/sound/snd_local.h index d07ee0b3e..7a5f50c9c 100644 --- a/neo/sound/snd_local.h +++ b/neo/sound/snd_local.h @@ -97,7 +97,9 @@ typedef enum { } soundDemoCommand_t; const int SOUND_MAX_CHANNELS = 8; -const int SOUND_DECODER_FREE_DELAY = 1000 * MIXBUFFER_SAMPLES / USERCMD_MSEC; // four seconds +// DG: TODO: keep the next at the same value for ~4 seconds, no matter how many frames that is? +// (we don't play sound faster just because we're running at higher FPS, I hope..) +const int SOUND_DECODER_FREE_DELAY = 1000 * MIXBUFFER_SAMPLES / 16; //USERCMD_MSEC; // four seconds const int PRIMARYFREQ = 44100; // samples per second const float SND_EPSILON = 1.0f / 32768.0f; // if volume is below this, it will always multiply to zero From e08236038e55d994ba384df878985deb57a23d4a Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Thu, 27 Jun 2024 16:54:35 +0200 Subject: [PATCH 03/34] Merge fixes from dezo2's 144Hz branch adjusted to use gameLocal.gameTicScale instead of 60/USERCMD_HZ --- neo/d3xp/Game_local.cpp | 8 ++++---- neo/d3xp/Player.cpp | 25 ++++++++++++++++--------- neo/d3xp/physics/Physics_AF.h | 3 ++- neo/d3xp/physics/Physics_RigidBody.cpp | 8 +++++++- neo/game/Player.cpp | 20 ++++++++++++-------- neo/game/physics/Physics_AF.h | 3 ++- neo/game/physics/Physics_RigidBody.cpp | 8 +++++++- 7 files changed, 50 insertions(+), 25 deletions(-) diff --git a/neo/d3xp/Game_local.cpp b/neo/d3xp/Game_local.cpp index 57456a5b6..44d38fb3e 100644 --- a/neo/d3xp/Game_local.cpp +++ b/neo/d3xp/Game_local.cpp @@ -4913,10 +4913,10 @@ void idGameLocal::ComputeSlowMsec() { // do any necessary ramping if ( slowmoState == SLOWMO_STATE_RAMPUP ) { - delta = 4 - slowmoMsec; + delta = gameMsec * 0.25f - slowmoMsec; // DG: adjust to support com_gameHz if ( fabs( delta ) < g_slowmoStepRate.GetFloat() ) { - slowmoMsec = 4; + slowmoMsec = gameMsec * 0.25f; // DG: adjust to support com_gameHz (was 4 = 16/4) slowmoState = SLOWMO_STATE_ON; } else { @@ -4928,10 +4928,10 @@ void idGameLocal::ComputeSlowMsec() { } } else if ( slowmoState == SLOWMO_STATE_RAMPDOWN ) { - delta = 16 - slowmoMsec; + delta = gameMsec - slowmoMsec; // DG: adjust to support com_gameHz if ( fabs( delta ) < g_slowmoStepRate.GetFloat() ) { - slowmoMsec = 16; + slowmoMsec = gameMsec; // DG: adjust to support com_gameHz slowmoState = SLOWMO_STATE_OFF; if ( gameSoundWorld ) { gameSoundWorld->SetSlowmo( false ); diff --git a/neo/d3xp/Player.cpp b/neo/d3xp/Player.cpp index 837447979..0aa3c4307 100644 --- a/neo/d3xp/Player.cpp +++ b/neo/d3xp/Player.cpp @@ -1599,8 +1599,8 @@ void idPlayer::Init( void ) { // stamina always initialized to maximum stamina = pm_stamina.GetFloat(); - // air always initialized to maximum too - airTics = pm_airTics.GetFloat(); + // air always initialized to maximum too - DG: pm_airTics must be scaled for actual FPS from com_gameHz + airTics = idMath::Rint( pm_airTics.GetFloat() * gameLocal.gameTicScale ); airless = false; gibDeath = false; @@ -3511,12 +3511,14 @@ bool idPlayer::Give( const char *statname, const char *value ) { } } else if ( !idStr::Icmp( statname, "air" ) ) { - if ( airTics >= pm_airTics.GetInteger() ) { + // DG: pm_airTics must be scaled for actual FPS from com_gameHz + int airTicsCnt = idMath::Rint( pm_airTics.GetFloat() * gameLocal.gameTicScale ); + if ( airTics >= airTicsCnt ) { return false; } airTics += atoi( value ) / 100.0 * pm_airTics.GetInteger(); - if ( airTics > pm_airTics.GetInteger() ) { - airTics = pm_airTics.GetInteger(); + if ( airTics > airTicsCnt ) { + airTics = airTicsCnt; } #ifdef _D3XP } else if ( !idStr::Icmp( statname, "enviroTime" ) ) { @@ -6071,6 +6073,9 @@ void idPlayer::UpdateAir( void ) { return; } + // DG: pm_airTics must be scaled for actual FPS from com_gameHz + int airTicsCnt = idMath::Rint( pm_airTics.GetFloat() * gameLocal.gameTicScale ); + // see if the player is connected to the info_vacuum bool newAirless = false; @@ -6126,15 +6131,15 @@ void idPlayer::UpdateAir( void ) { } } airTics+=2; // regain twice as fast as lose - if ( airTics > pm_airTics.GetInteger() ) { - airTics = pm_airTics.GetInteger(); + if ( airTics > airTicsCnt ) { + airTics = airTicsCnt; } } airless = newAirless; if ( hud ) { - hud->SetStateInt( "player_air", 100 * airTics / pm_airTics.GetInteger() ); + hud->SetStateInt( "player_air", 100 * airTics / airTicsCnt ); } } @@ -7648,7 +7653,9 @@ bool idPlayer::CanGive( const char *statname, const char *value ) { return true; } else if ( !idStr::Icmp( statname, "air" ) ) { - if ( airTics >= pm_airTics.GetInteger() ) { + // DG: pm_airTics must be scaled for actual FPS from com_gameHz + int airTicsCnt = idMath::Rint( pm_airTics.GetFloat() * gameLocal.gameTicScale ); + if ( airTics >= airTicsCnt ) { return false; } return true; diff --git a/neo/d3xp/physics/Physics_AF.h b/neo/d3xp/physics/Physics_AF.h index 90b57c968..4c2eec088 100644 --- a/neo/d3xp/physics/Physics_AF.h +++ b/neo/d3xp/physics/Physics_AF.h @@ -382,7 +382,8 @@ class idAFConstraint_HingeSteering : public idAFConstraint { idAFConstraint_HingeSteering( void ); void Setup( idAFConstraint_Hinge *cc ); void SetSteerAngle( const float degrees ) { steerAngle = degrees; } - void SetSteerSpeed( const float speed ) { steerSpeed = speed; } + // DG: take com_gameHz into account + void SetSteerSpeed( const float speed ) { steerSpeed = speed * gameLocal.gameTicScale; } void SetEpsilon( const float e ) { epsilon = e; } bool Add( idPhysics_AF *phys, float invTimeStep ); virtual void Translate( const idVec3 &translation ); diff --git a/neo/d3xp/physics/Physics_RigidBody.cpp b/neo/d3xp/physics/Physics_RigidBody.cpp index 838065699..888c91520 100644 --- a/neo/d3xp/physics/Physics_RigidBody.cpp +++ b/neo/d3xp/physics/Physics_RigidBody.cpp @@ -38,7 +38,9 @@ If you have questions concerning this license or the applicable additional terms CLASS_DECLARATION( idPhysics_Base, idPhysics_RigidBody ) END_CLASS -const float STOP_SPEED = 10.0f; +// DG: moved STOP_SPEED into the functions using it, because it now +// depends on com_gameHz which can change at runtime +// const float STOP_SPEED = 10.0f; #undef RB_TIMINGS @@ -122,6 +124,8 @@ bool idPhysics_RigidBody::CollisionImpulse( const trace_t &collision, idVec3 &im impactInfo_t info; idEntity *ent; + const float STOP_SPEED = 10.0f * gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change + // get info from other entity involved ent = gameLocal.entities[collision.c.entityNum]; ent->GetImpactInfo( self, collision.c.id, collision.c.point, &info ); @@ -333,6 +337,8 @@ bool idPhysics_RigidBody::TestIfAtRest( void ) const { // linear velocity orthogonal to gravity direction v -= gv * gravityNormal; + const float STOP_SPEED = 10.0f * gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change + // if too much velocity orthogonal to gravity direction if ( v.Length() > STOP_SPEED ) { return false; diff --git a/neo/game/Player.cpp b/neo/game/Player.cpp index ec9b3ff8b..84740f6c6 100644 --- a/neo/game/Player.cpp +++ b/neo/game/Player.cpp @@ -1294,8 +1294,8 @@ void idPlayer::Init( void ) { // stamina always initialized to maximum stamina = pm_stamina.GetFloat(); - // air always initialized to maximum too - airTics = pm_airTics.GetFloat(); + // air always initialized to maximum too - DG: pm_airTics must be scaled for actual FPS for com_gameHz + airTics = idMath::Rint( pm_airTics.GetFloat() * gameLocal.gameTicScale ); airless = false; gibDeath = false; @@ -2903,12 +2903,14 @@ bool idPlayer::Give( const char *statname, const char *value ) { } } else if ( !idStr::Icmp( statname, "air" ) ) { - if ( airTics >= pm_airTics.GetInteger() ) { + // DG: pm_airTics must be scaled for actual FPS from com_gameHz + int airTicsCnt = idMath::Rint( pm_airTics.GetFloat() * gameLocal.gameTicScale ); + if ( airTics >= airTicsCnt ) { return false; } airTics += atoi( value ) / 100.0 * pm_airTics.GetInteger(); - if ( airTics > pm_airTics.GetInteger() ) { - airTics = pm_airTics.GetInteger(); + if ( airTics > airTicsCnt ) { + airTics = airTicsCnt; } } else { return inventory.Give( this, spawnArgs, statname, value, &idealWeapon, true ); @@ -5069,6 +5071,8 @@ void idPlayer::UpdateAir( void ) { // see if the player is connected to the info_vacuum bool newAirless = false; + // DG: pm_airTics must be scaled for actual FPS from com_gameHz + int airTicsCnt = idMath::Rint( pm_airTics.GetFloat() * gameLocal.gameTicScale ); if ( gameLocal.vacuumAreaNum != -1 ) { int num = GetNumPVSAreas(); @@ -5116,15 +5120,15 @@ void idPlayer::UpdateAir( void ) { } } airTics+=2; // regain twice as fast as lose - if ( airTics > pm_airTics.GetInteger() ) { - airTics = pm_airTics.GetInteger(); + if ( airTics > airTicsCnt ) { + airTics = airTicsCnt; } } airless = newAirless; if ( hud ) { - hud->SetStateInt( "player_air", 100 * airTics / pm_airTics.GetInteger() ); + hud->SetStateInt( "player_air", 100 * airTics / airTicsCnt ); } } diff --git a/neo/game/physics/Physics_AF.h b/neo/game/physics/Physics_AF.h index 90b57c968..4c2eec088 100644 --- a/neo/game/physics/Physics_AF.h +++ b/neo/game/physics/Physics_AF.h @@ -382,7 +382,8 @@ class idAFConstraint_HingeSteering : public idAFConstraint { idAFConstraint_HingeSteering( void ); void Setup( idAFConstraint_Hinge *cc ); void SetSteerAngle( const float degrees ) { steerAngle = degrees; } - void SetSteerSpeed( const float speed ) { steerSpeed = speed; } + // DG: take com_gameHz into account + void SetSteerSpeed( const float speed ) { steerSpeed = speed * gameLocal.gameTicScale; } void SetEpsilon( const float e ) { epsilon = e; } bool Add( idPhysics_AF *phys, float invTimeStep ); virtual void Translate( const idVec3 &translation ); diff --git a/neo/game/physics/Physics_RigidBody.cpp b/neo/game/physics/Physics_RigidBody.cpp index 838065699..888c91520 100644 --- a/neo/game/physics/Physics_RigidBody.cpp +++ b/neo/game/physics/Physics_RigidBody.cpp @@ -38,7 +38,9 @@ If you have questions concerning this license or the applicable additional terms CLASS_DECLARATION( idPhysics_Base, idPhysics_RigidBody ) END_CLASS -const float STOP_SPEED = 10.0f; +// DG: moved STOP_SPEED into the functions using it, because it now +// depends on com_gameHz which can change at runtime +// const float STOP_SPEED = 10.0f; #undef RB_TIMINGS @@ -122,6 +124,8 @@ bool idPhysics_RigidBody::CollisionImpulse( const trace_t &collision, idVec3 &im impactInfo_t info; idEntity *ent; + const float STOP_SPEED = 10.0f * gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change + // get info from other entity involved ent = gameLocal.entities[collision.c.entityNum]; ent->GetImpactInfo( self, collision.c.id, collision.c.point, &info ); @@ -333,6 +337,8 @@ bool idPhysics_RigidBody::TestIfAtRest( void ) const { // linear velocity orthogonal to gravity direction v -= gv * gravityNormal; + const float STOP_SPEED = 10.0f * gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change + // if too much velocity orthogonal to gravity direction if ( v.Length() > STOP_SPEED ) { return false; From edb0b8d7ec09e0660f209715d4d95713c20aadf6 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Wed, 26 Jun 2024 19:33:42 +0200 Subject: [PATCH 04/34] Merge dezo2's change in HUD scaling for d3xp grabber crosshair --- neo/d3xp/Player.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/neo/d3xp/Player.cpp b/neo/d3xp/Player.cpp index 0aa3c4307..0a19451b4 100644 --- a/neo/d3xp/Player.cpp +++ b/neo/d3xp/Player.cpp @@ -3231,9 +3231,14 @@ void idPlayer::DrawHUD( idUserInterface *_hud ) { if ( weapon.GetEntity()->GetGrabberState() == 1 || weapon.GetEntity()->GetGrabberState() == 2 ) { cursor->SetStateString( "grabbercursor", "1" ); cursor->SetStateString( "combatcursor", "0" ); + cursor->SetStateBool("scaleto43", false); // dezo2, unscaled + cursor->StateChanged(gameLocal.realClientTime); // dezo2, set state } else { cursor->SetStateString( "grabbercursor", "0" ); cursor->SetStateString( "combatcursor", "1" ); + cursor->SetStateBool("scaleto43", true); // dezo2, scaled + cursor->StateChanged(gameLocal.realClientTime); // dezo2, set state + } #endif From f0c9f4e63141ad350f711db3c0e3365e363b66ed Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Thu, 27 Jun 2024 04:12:44 +0200 Subject: [PATCH 05/34] Framerate-independent idPlayer::Move() and idAI::AdjustFlyingAngles() thanks to Github user tyuah8: https://github.com/dhewm/dhewm3/pull/584#issuecomment-2192836172 --- neo/d3xp/Player.cpp | 6 +++++- neo/d3xp/ai/AI.cpp | 11 +++++++++-- neo/game/Player.cpp | 6 +++++- neo/game/ai/AI.cpp | 8 ++++++-- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/neo/d3xp/Player.cpp b/neo/d3xp/Player.cpp index 0a19451b4..f716e00d5 100644 --- a/neo/d3xp/Player.cpp +++ b/neo/d3xp/Player.cpp @@ -7117,8 +7117,12 @@ void idPlayer::Move( void ) { if ( spectating ) { SetEyeHeight( newEyeOffset ); } else { + // DG: make this framerate-independent, code suggested by tyuah8 on Github + // https://en.wikipedia.org/wiki/Exponential_smoothing#Time_constant + const float tau = -16.0f / idMath::Log( pm_crouchrate.GetFloat() ); + const float a = 1.0f - idMath::Exp( -gameLocal.gameMsec / tau ); // smooth out duck height changes - SetEyeHeight( EyeHeight() * pm_crouchrate.GetFloat() + newEyeOffset * ( 1.0f - pm_crouchrate.GetFloat() ) ); + SetEyeHeight( EyeHeight() * (1.0f - a) + newEyeOffset * a ); } } diff --git a/neo/d3xp/ai/AI.cpp b/neo/d3xp/ai/AI.cpp index 887594b66..064a7e41a 100644 --- a/neo/d3xp/ai/AI.cpp +++ b/neo/d3xp/ai/AI.cpp @@ -2984,8 +2984,15 @@ void idAI::AdjustFlyingAngles( void ) { } } - fly_roll = fly_roll * 0.95f + roll * 0.05f; - fly_pitch = fly_pitch * 0.95f + pitch * 0.05f; + // DG: make this framerate-independent, code suggested by tyuah8 on Github + // https://en.wikipedia.org/wiki/Exponential_smoothing#Time_constant + static const float tau = -16.0f / idMath::Log( 0.95f ); + // TODO: use gameLocal.gameMsec instead, so it's not affected by slow motion? + // enemies turning slower in slowmo seems logical, but the original code + // just increased every tic and thus was independent of slowmo + const float a = 1.0f - idMath::Exp( -gameLocal.msec / tau ); + fly_roll = fly_roll * (1.0f - a) + roll * a; + fly_pitch = fly_pitch * (1.0f - a) + pitch * a; if ( flyTiltJoint != INVALID_JOINT ) { animator.SetJointAxis( flyTiltJoint, JOINTMOD_WORLD, idAngles( fly_pitch, 0.0f, fly_roll ).ToMat3() ); diff --git a/neo/game/Player.cpp b/neo/game/Player.cpp index 84740f6c6..d5656b301 100644 --- a/neo/game/Player.cpp +++ b/neo/game/Player.cpp @@ -6014,8 +6014,12 @@ void idPlayer::Move( void ) { if ( spectating ) { SetEyeHeight( newEyeOffset ); } else { + // DG: make this framerate-independent, code suggested by tyuah8 on Github + // https://en.wikipedia.org/wiki/Exponential_smoothing#Time_constant + const float tau = -16.0f / idMath::Log( pm_crouchrate.GetFloat() ); + const float a = 1.0f - idMath::Exp( -gameLocal.gameMsec / tau ); // smooth out duck height changes - SetEyeHeight( EyeHeight() * pm_crouchrate.GetFloat() + newEyeOffset * ( 1.0f - pm_crouchrate.GetFloat() ) ); + SetEyeHeight( EyeHeight() * (1.0f - a) + newEyeOffset * a ); } } diff --git a/neo/game/ai/AI.cpp b/neo/game/ai/AI.cpp index 2ac99486d..b025bd9ca 100644 --- a/neo/game/ai/AI.cpp +++ b/neo/game/ai/AI.cpp @@ -2896,8 +2896,12 @@ void idAI::AdjustFlyingAngles( void ) { } } - fly_roll = fly_roll * 0.95f + roll * 0.05f; - fly_pitch = fly_pitch * 0.95f + pitch * 0.05f; + // DG: make this framerate-independent, code suggested by tyuah8 on Github + // https://en.wikipedia.org/wiki/Exponential_smoothing#Time_constant + static const float tau = -16.0f / idMath::Log( 0.95f ); + const float a = 1.0f - idMath::Exp( -gameLocal.msec / tau ); + fly_roll = fly_roll * (1.0f - a) + roll * a; + fly_pitch = fly_pitch * (1.0f - a) + pitch * a; if ( flyTiltJoint != INVALID_JOINT ) { animator.SetJointAxis( flyTiltJoint, JOINTMOD_WORLD, idAngles( fly_pitch, 0.0f, fly_roll ).ToMat3() ); From 8911910ff05ca11c294299e0e01c9664f0af7de3 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Thu, 27 Jun 2024 18:13:01 +0200 Subject: [PATCH 06/34] Add hacks for replacing some #defines in scripts Note: A simpler approach would be replacing "#define GAME_FPS 60" and the similar defines for GAME_FRAMETIME and CHAINGUN_FIRE_SKIPFRAMES in the scripts with other values based on com_gameHz, for example to "#define GAME_FPS 144". However, that would have two disadvantages: 1. When changing com_gameHz, you'll have to reload the scripts 2. This changes the checksum of the scripts, so each time you change com_gameHz the checksum changes and your old savegames stop working. Luckily the scripts already have functions (that are implemented in the gamecode) that expose the FPS and FrameTime: sys.getTicksPerSecond() and sys.getFrameTime() So now we modify the #defines to call those functions instead. That will still break *old* savegames, but at least savegames made from now on will still work no matter what you set com_gameHz to. TODO: A problem with this approach is that the time sys.getFrameTime() returns is scaled when in slow motion, so I'll probably have to add another function for that This code is partly based on code from Stradex' "Add com_gameHz to play with higher HZ/FPS" PR: https://github.com/dhewm/dhewm3/pull/297 --- neo/idlib/Parser.cpp | 97 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/neo/idlib/Parser.cpp b/neo/idlib/Parser.cpp index dedf5cc01..70e32ef93 100644 --- a/neo/idlib/Parser.cpp +++ b/neo/idlib/Parser.cpp @@ -1083,6 +1083,19 @@ int idParser::Directive_undef( void ) { return true; } + +// DG: helperfunction for my hack in Directive_define() +static idToken* createToken(const char* str, int type, int subtype, int line) { + idToken* ret = new idToken(); + *ret = str; + ret->type = type; + ret->subtype = subtype; + ret->line = line; + ret->flags = 0; + ret->ClearTokenWhiteSpace(); + return ret; +} + /* ================ idParser::Directive_define @@ -1126,6 +1139,90 @@ int idParser::Directive_define( void ) { if ( !idParser::ReadLine( &token ) ) { return true; } + + // Stradex/DG: Hack to support com_gameHZ (configurable framerate instead of fixed 60fps): + // we replace GAME_FPS, GAME_FRAMETIME and CHAINGUN_FIRE_SKIPFRAMES #defines with script code + // NOTE: it's theoretically possible to just replace them with the current value instead + // of function calls ("#define GAME_FPS 144"), but that changes the script's checksum + // which makes loading savegames fail after changing com_gameHz + const char* defName = define->name; + int numHackTokens = 0; + idToken* hackTokens[12] = {}; + if ( idStr::Icmp(defName, "GAME_FPS") == 0 || idStr::Icmp(defName, "GAME_FRAMETIME") == 0 ) { + // FIXME: in d3xp, getFrameTime returns gameLocal.msec, which is modified for slowmo, which is not what we want + const char* funName = (idStr::Icmp(defName, "GAME_FPS") == 0) ? "getTicsPerSecond" : "getFrameTime"; + int line = token.line; + + // change "#define GAME_FPS 60" to "#define GAME_FPS sys.getTicsPerSecond()" + // (or equivalent for GAME_FRAMETIME and sys.getFrameTime()) + hackTokens[0] = createToken( "sys", TT_NAME, 3, line ); + hackTokens[1] = createToken( ".", TT_PUNCTUATION, P_REF, line ); + hackTokens[2] = createToken( funName, TT_NAME, strlen(funName), line ); // getTicsPerSecond or getFrameTime + hackTokens[3] = createToken( "(", TT_PUNCTUATION, P_PARENTHESESOPEN, line ); + hackTokens[4] = createToken( ")", TT_PUNCTUATION, P_PARENTHESESCLOSE, line ); + numHackTokens = 5; + } + else if ( idStr::Icmp(defName, "CHAINGUN_FIRE_SKIPFRAMES" ) == 0) { + int line = token.line; + + // change "#define CHAINGUN_FIRE_SKIPFRAMES 7" (or similar) to + // "#define CHAINGUN_FIRE_SKIPFRAMES int( ( 0.118644 * sys.getTicsPerSecond() ) ) + // where 0.118644 is the value of "factor" below (sth like 7/59) + // Note: Yes, it looks like there's a superfluous set of parenthesis, but without them + // it doesn't work. I guess that's a bug in the script compiler, but I have no + // motivation to debug that further.. + + float origVal = ( token.type == TT_NUMBER ) ? token.GetFloatValue() : 7.0f; + // should divide by 60, but with 59 it rounds up a bit, esp. for 144 fps the resulting + // value is better (in the script we clamp to int and don't round): 7/60 * 144 = 16.8 + // => converted to int that's 16, while 7/59 * 144 = 17.084 => 17 => closer to 16.8 + // (and for other common values like 60 or 120 or 200 or 240 it doesn't make a difference) + // Note that clamping to int is important, so the following script code works as before: + // "for( skip = 0; skip < CHAINGUN_FIRE_SKIPFRAMES; skip++ )" + // (if CHAINGUN_FIRE_SKIPFRAMES isn't 7 but 7.01, this runs 8 times instead of 7) + float factor = origVal / 59.0f; + char facStr[10]; + idStr::snPrintf( facStr, sizeof(facStr), "%f", factor ); + + // int( ( 0.118644 * sys.getTicsPerSecond() ) ) + hackTokens[0] = createToken( "int", TT_NAME, 3, line ); + hackTokens[1] = createToken( "(", TT_PUNCTUATION, P_PARENTHESESOPEN, line ); + + hackTokens[2] = createToken( "(", TT_PUNCTUATION, P_PARENTHESESOPEN, line ); + + hackTokens[3] = createToken( facStr, TT_NUMBER, TT_DECIMAL | TT_FLOAT | TT_DOUBLE_PRECISION, line ); + hackTokens[4] = createToken( "*", TT_PUNCTUATION, P_MUL, line ); + hackTokens[5] = createToken( "sys", TT_NAME, 3, line ); + hackTokens[6] = createToken( ".", TT_PUNCTUATION, P_REF, line ); + hackTokens[7] = createToken( "getTicsPerSecond", TT_NAME, strlen("getTicsPerSecond"), line ); + hackTokens[8] = createToken( "(", TT_PUNCTUATION, P_PARENTHESESOPEN, line ); + hackTokens[9] = createToken( ")", TT_PUNCTUATION, P_PARENTHESESCLOSE, line ); + + hackTokens[10] = createToken( ")", TT_PUNCTUATION, P_PARENTHESESCLOSE, line ); + + hackTokens[11] = createToken( ")", TT_PUNCTUATION, P_PARENTHESESCLOSE, line ); + numHackTokens = 12; + } + + if ( numHackTokens != 0 ) { + // skip rest of the line, inject our hackTokens instead and return + while ( idParser::ReadLine( &token ) ) + {}; + + define->tokens = hackTokens[0]; + last = hackTokens[0]; + + for( int i=1; i < numHackTokens; ++i ) { + t = hackTokens[i]; + last->next = t; + last = t; + } + last->next = NULL; + + return true; + } + // DG: END of Hack. + // if it is a define with parameters if ( token.WhiteSpaceBeforeToken() == 0 && token == "(" ) { // read the define parameters From 9f45b8ca001b5d4e98aedf08e0b7f96f7af296dd Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Thu, 27 Jun 2024 19:04:08 +0200 Subject: [PATCH 07/34] Add sys.getRawFrameTime() script event for GAME_FRAMETIME This fixes the issue of scaled frametimes in d3xp. As for some reason the events defined in C++ code must be defined again in scripts (script/doom_events.script), I had to add a little hack to the script compiler to inject the definition for getRawFrameTime() (it's done when the getFrameTime definition is parsed) --- neo/d3xp/script/Script_Compiler.cpp | 31 +++++++++++++++++++++++++++++ neo/d3xp/script/Script_Thread.cpp | 12 +++++++++++ neo/d3xp/script/Script_Thread.h | 1 + neo/game/script/Script_Compiler.cpp | 31 +++++++++++++++++++++++++++++ neo/game/script/Script_Thread.cpp | 12 +++++++++++ neo/game/script/Script_Thread.h | 1 + neo/idlib/Parser.cpp | 3 +-- 7 files changed, 89 insertions(+), 2 deletions(-) diff --git a/neo/d3xp/script/Script_Compiler.cpp b/neo/d3xp/script/Script_Compiler.cpp index 7f28d8e72..26822c07d 100644 --- a/neo/d3xp/script/Script_Compiler.cpp +++ b/neo/d3xp/script/Script_Compiler.cpp @@ -2516,6 +2516,37 @@ void idCompiler::ParseEventDef( idTypeDef *returnType, const char *name ) { // mark the parms as local func.locals = func.parmTotal; + + // DG: Hack: when "scriptEvent float getFrameTime()" is parsed, + // inject "scriptEvent float getRawFrameTime()" + if ( idStr::Cmp( name, "getFrameTime" ) == 0 + && gameLocal.program.FindType( "getRawFrameTime" ) == NULL ) + { + // NOTE: getRawFrameTime() has the same signature as getFrameTime() + // so its type settings can be copied, only the name must be changed + newtype.SetName( "getRawFrameTime" ); + + ev = idEventDef::FindEvent( "getRawFrameTime" ); + if ( ev == NULL ) { + Error( "Couldn't find Event getRawFrameTime!" ); + } + + type = gameLocal.program.AllocType( newtype ); + type->def = gameLocal.program.AllocDef( type, "getRawFrameTime", &def_namespace, true ); + + function_t &func2 = gameLocal.program.AllocFunction( type->def ); + func2.eventdef = ev; + func2.parmSize.SetNum( num ); + for( i = 0; i < num; i++ ) { + argType = newtype.GetParmType( i ); + func2.parmTotal += argType->Size(); + func2.parmSize[ i ] = argType->Size(); + } + + // mark the parms as local + func2.locals = func2.parmTotal; + } + // DG end } } diff --git a/neo/d3xp/script/Script_Thread.cpp b/neo/d3xp/script/Script_Thread.cpp index 7c6b020cd..cb06cf2c0 100644 --- a/neo/d3xp/script/Script_Thread.cpp +++ b/neo/d3xp/script/Script_Thread.cpp @@ -117,6 +117,7 @@ const idEventDef EV_Thread_RadiusDamage( "radiusDamage", "vEEEsf" ); const idEventDef EV_Thread_IsClient( "isClient", NULL, 'f' ); const idEventDef EV_Thread_IsMultiplayer( "isMultiplayer", NULL, 'f' ); const idEventDef EV_Thread_GetFrameTime( "getFrameTime", NULL, 'f' ); +const idEventDef EV_Thread_GetRawFrameTime( "getRawFrameTime", NULL, 'f' ); // DG: for com_gameHz (returns frametime, unlike getFrameTime() *not* scaled for slowmo) const idEventDef EV_Thread_GetTicsPerSecond( "getTicsPerSecond", NULL, 'f' ); const idEventDef EV_Thread_DebugLine( "debugLine", "vvvf" ); const idEventDef EV_Thread_DebugArrow( "debugArrow", "vvvdf" ); @@ -207,6 +208,7 @@ CLASS_DECLARATION( idClass, idThread ) EVENT( EV_Thread_IsClient, idThread::Event_IsClient ) EVENT( EV_Thread_IsMultiplayer, idThread::Event_IsMultiplayer ) EVENT( EV_Thread_GetFrameTime, idThread::Event_GetFrameTime ) + EVENT( EV_Thread_GetRawFrameTime, idThread::Event_GetRawFrameTime ) // DG: for com_gameHz (returns frametime, unlike getFrameTime() *not* scaled for slowmo) EVENT( EV_Thread_GetTicsPerSecond, idThread::Event_GetTicsPerSecond ) EVENT( EV_CacheSoundShader, idThread::Event_CacheSoundShader ) EVENT( EV_Thread_DebugLine, idThread::Event_DebugLine ) @@ -1843,6 +1845,16 @@ void idThread::Event_GetFrameTime( void ) { idThread::ReturnFloat( MS2SEC( gameLocal.msec ) ); } +/* +================ +idThread::Event_GetRawFrameTime +================ +*/ +void idThread::Event_GetRawFrameTime( void ) { + // DG: for com_gameHz, to replace GAME_FRAMETIME (raw frametime, unlike getFrameTime() *not* scaled for slowmo) + idThread::ReturnFloat( MS2SEC( gameLocal.gameMsec ) ); +} + /* ================ idThread::Event_GetTicsPerSecond diff --git a/neo/d3xp/script/Script_Thread.h b/neo/d3xp/script/Script_Thread.h index 7d7764385..8de4baa19 100644 --- a/neo/d3xp/script/Script_Thread.h +++ b/neo/d3xp/script/Script_Thread.h @@ -190,6 +190,7 @@ class idThread : public idClass { void Event_IsClient( void ); void Event_IsMultiplayer( void ); void Event_GetFrameTime( void ); + void Event_GetRawFrameTime( void ); // DG: for com_gameHz (returns frametime, unlike getFrameTime() *not* scaled for slowmo) void Event_GetTicsPerSecond( void ); void Event_CacheSoundShader( const char *soundName ); void Event_DebugLine( const idVec3 &color, const idVec3 &start, const idVec3 &end, const float lifetime ); diff --git a/neo/game/script/Script_Compiler.cpp b/neo/game/script/Script_Compiler.cpp index 5ee0c77a0..48ee811e8 100644 --- a/neo/game/script/Script_Compiler.cpp +++ b/neo/game/script/Script_Compiler.cpp @@ -2516,6 +2516,37 @@ void idCompiler::ParseEventDef( idTypeDef *returnType, const char *name ) { // mark the parms as local func.locals = func.parmTotal; + + // DG: Hack: when "scriptEvent float getFrameTime()" is parsed, + // inject "scriptEvent float getRawFrameTime()" + if ( idStr::Cmp( name, "getFrameTime" ) == 0 + && gameLocal.program.FindType( "getRawFrameTime" ) == NULL ) + { + // NOTE: getRawFrameTime() has the same signature as getFrameTime() + // so its type settings can be copied, only the name must be changed + newtype.SetName( "getRawFrameTime" ); + + ev = idEventDef::FindEvent( "getRawFrameTime" ); + if ( ev == NULL ) { + Error( "Couldn't find Event getRawFrameTime!" ); + } + + type = gameLocal.program.AllocType( newtype ); + type->def = gameLocal.program.AllocDef( type, "getRawFrameTime", &def_namespace, true ); + + function_t &func2 = gameLocal.program.AllocFunction( type->def ); + func2.eventdef = ev; + func2.parmSize.SetNum( num ); + for( i = 0; i < num; i++ ) { + argType = newtype.GetParmType( i ); + func2.parmTotal += argType->Size(); + func2.parmSize[ i ] = argType->Size(); + } + + // mark the parms as local + func2.locals = func2.parmTotal; + } + // DG end } } diff --git a/neo/game/script/Script_Thread.cpp b/neo/game/script/Script_Thread.cpp index 320645228..6fa6c3179 100644 --- a/neo/game/script/Script_Thread.cpp +++ b/neo/game/script/Script_Thread.cpp @@ -106,6 +106,7 @@ const idEventDef EV_Thread_RadiusDamage( "radiusDamage", "vEEEsf" ); const idEventDef EV_Thread_IsClient( "isClient", NULL, 'f' ); const idEventDef EV_Thread_IsMultiplayer( "isMultiplayer", NULL, 'f' ); const idEventDef EV_Thread_GetFrameTime( "getFrameTime", NULL, 'f' ); +const idEventDef EV_Thread_GetRawFrameTime( "getRawFrameTime", NULL, 'f' ); // DG: for com_gameHz (returns frametime, unlike getFrameTime() *not* scaled for slowmo) const idEventDef EV_Thread_GetTicsPerSecond( "getTicsPerSecond", NULL, 'f' ); const idEventDef EV_Thread_DebugLine( "debugLine", "vvvf" ); const idEventDef EV_Thread_DebugArrow( "debugArrow", "vvvdf" ); @@ -185,6 +186,7 @@ CLASS_DECLARATION( idClass, idThread ) EVENT( EV_Thread_IsClient, idThread::Event_IsClient ) EVENT( EV_Thread_IsMultiplayer, idThread::Event_IsMultiplayer ) EVENT( EV_Thread_GetFrameTime, idThread::Event_GetFrameTime ) + EVENT( EV_Thread_GetRawFrameTime, idThread::Event_GetRawFrameTime ) // DG: for com_gameHz (returns frametime, unlike getFrameTime() *not* scaled for slowmo) EVENT( EV_Thread_GetTicsPerSecond, idThread::Event_GetTicsPerSecond ) EVENT( EV_CacheSoundShader, idThread::Event_CacheSoundShader ) EVENT( EV_Thread_DebugLine, idThread::Event_DebugLine ) @@ -1763,6 +1765,16 @@ void idThread::Event_GetFrameTime( void ) { idThread::ReturnFloat( MS2SEC( gameLocal.msec ) ); } +/* +================ +idThread::Event_GetRawFrameTime +================ +*/ +void idThread::Event_GetRawFrameTime( void ) { + // DG: for com_gameHz, to replace GAME_FRAMETIME (raw frametime, unlike getFrameTime() *not* scaled for slowmo) + idThread::ReturnFloat( MS2SEC( gameLocal.gameMsec ) ); +} + /* ================ idThread::Event_GetTicsPerSecond diff --git a/neo/game/script/Script_Thread.h b/neo/game/script/Script_Thread.h index fe24943f9..afa3ac09b 100644 --- a/neo/game/script/Script_Thread.h +++ b/neo/game/script/Script_Thread.h @@ -179,6 +179,7 @@ class idThread : public idClass { void Event_IsClient( void ); void Event_IsMultiplayer( void ); void Event_GetFrameTime( void ); + void Event_GetRawFrameTime( void ); // DG: for com_gameHz (returns frametime, unlike getFrameTime() *not* scaled for slowmo) void Event_GetTicsPerSecond( void ); void Event_CacheSoundShader( const char *soundName ); void Event_DebugLine( const idVec3 &color, const idVec3 &start, const idVec3 &end, const float lifetime ); diff --git a/neo/idlib/Parser.cpp b/neo/idlib/Parser.cpp index 70e32ef93..bd4fe7b76 100644 --- a/neo/idlib/Parser.cpp +++ b/neo/idlib/Parser.cpp @@ -1149,8 +1149,7 @@ int idParser::Directive_define( void ) { int numHackTokens = 0; idToken* hackTokens[12] = {}; if ( idStr::Icmp(defName, "GAME_FPS") == 0 || idStr::Icmp(defName, "GAME_FRAMETIME") == 0 ) { - // FIXME: in d3xp, getFrameTime returns gameLocal.msec, which is modified for slowmo, which is not what we want - const char* funName = (idStr::Icmp(defName, "GAME_FPS") == 0) ? "getTicsPerSecond" : "getFrameTime"; + const char* funName = (idStr::Icmp(defName, "GAME_FPS") == 0) ? "getTicsPerSecond" : "getRawFrameTime"; int line = token.line; // change "#define GAME_FPS 60" to "#define GAME_FPS sys.getTicsPerSecond()" From 4c0db2143d971e2c07336a58ab4cb22d124024ce Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Thu, 27 Jun 2024 21:27:05 +0200 Subject: [PATCH 08/34] d3xp: When com_gameHz changes, scale some time values accordingly also do that when loading a savegame, so those values are correct even if com_gameHz has a different value than it had when saving --- neo/d3xp/Game_local.cpp | 28 ++++++++++++++++++++++++++-- neo/d3xp/Game_local.h | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/neo/d3xp/Game_local.cpp b/neo/d3xp/Game_local.cpp index 44d38fb3e..49bc94508 100644 --- a/neo/d3xp/Game_local.cpp +++ b/neo/d3xp/Game_local.cpp @@ -626,6 +626,7 @@ void idGameLocal::SaveGame( idFile *f ) { savegame.WriteInt( time ); #ifdef _D3XP + savegame.WriteInt( gameMsec ); // DG: added with INTERNAL_SAVEGAME_VERSION 2 savegame.WriteInt( msec ); #endif @@ -1535,7 +1536,15 @@ bool idGameLocal::InitFromSaveGame( const char *mapName, idRenderWorld *renderWo savegame.ReadInt( time ); #ifdef _D3XP + int savedGameMsec = 16; + if( savegame.GetInternalSavegameVersion() > 1 ) { + savegame.ReadInt( savedGameMsec ); + } + float gameMsecScale = float(gameMsec) / float(savedGameMsec); + savegame.ReadInt( msec ); + // DG: the saved msec must be scaled, in case com_gameHz has a different value now + msec *= gameMsecScale; #endif savegame.ReadInt( vacuumAreaNum ); @@ -1558,12 +1567,15 @@ bool idGameLocal::InitFromSaveGame( const char *mapName, idRenderWorld *renderWo fast.Restore( &savegame ); slow.Restore( &savegame ); + fast.msec *= gameMsecScale; // DG: the saved value must be scaled, in case com_gameHz has a different value now + slow.msec *= gameMsecScale; // same here int blah; savegame.ReadInt( blah ); slowmoState = (slowmoState_t)blah; savegame.ReadFloat( slowmoMsec ); + slowmoMsec *= gameMsecScale; // DG: the saved value must be scaled, in case com_gameHz has a different value now savegame.ReadBool( quickSlowmoReset ); if ( slowmoState == SLOWMO_STATE_OFF ) { @@ -5048,8 +5060,20 @@ void idGameLocal::GetMapLoadingGUI( char gui[ MAX_STRING_CHARS ] ) { } void idGameLocal::SetGameHz( float hz, float frametime, float ticScaleFactor ) { gameHz = hz; + int oldGameMsec = gameMsec; gameMsec = frametime; - msec = frametime; // TODO: maybe msec must be scaled - // TODO: adjust this->fast and this->slow gameTicScale = ticScaleFactor; + + if ( slowmoState == SLOWMO_STATE_OFF ) { + // if slowmo is off, msec, slowmoMsec and slow/fast.msec should all be set to gameMsec + // ResetSlowTimeVars() does just that + ResetSlowTimeVars(); + } else { + // otherwise the msec values must be scaled accordingly + float gameMsecScale = frametime / float(oldGameMsec); + msec *= gameMsecScale; + slowmoMsec *= gameMsecScale; + fast.msec *= gameMsecScale; + slow.msec *= gameMsecScale; + } } diff --git a/neo/d3xp/Game_local.h b/neo/d3xp/Game_local.h index 94b80876a..639a374ed 100644 --- a/neo/d3xp/Game_local.h +++ b/neo/d3xp/Game_local.h @@ -520,7 +520,7 @@ class idGameLocal : public idGame { private: const static int INITIAL_SPAWN_COUNT = 1; - const static int INTERNAL_SAVEGAME_VERSION = 1; // DG: added this for >= 1305 savegames + const static int INTERNAL_SAVEGAME_VERSION = 2; // DG: added this for >= 1305 savegames idStr mapFileName; // name of the map, empty string if no map loaded idMapFile * mapFile; // will be NULL during the game unless in-game editing is used From d063a0ee309be490b371244da6dd7d5f9037fcb9 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Fri, 28 Jun 2024 00:10:32 +0200 Subject: [PATCH 09/34] Make sure gameLocal.msec, .gameMsec, etc are always initialized --- neo/d3xp/Game_local.cpp | 4 ++++ neo/game/Game_local.cpp | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/neo/d3xp/Game_local.cpp b/neo/d3xp/Game_local.cpp index 49bc94508..1f1669391 100644 --- a/neo/d3xp/Game_local.cpp +++ b/neo/d3xp/Game_local.cpp @@ -205,6 +205,10 @@ idGameLocal::idGameLocal */ idGameLocal::idGameLocal() { Clear(); + // DG: some sane default values until the proper values are set by SetGameHz() + msec = gameMsec = 16; + gameHz = 60; + gameTicScale = 1.0f; } /* diff --git a/neo/game/Game_local.cpp b/neo/game/Game_local.cpp index 2a873190c..8eeb2b657 100644 --- a/neo/game/Game_local.cpp +++ b/neo/game/Game_local.cpp @@ -172,6 +172,10 @@ idGameLocal::idGameLocal */ idGameLocal::idGameLocal() { Clear(); + // DG: some sane default values until the proper values are set by SetGameHz() + msec = gameMsec = 16; + gameHz = 60; + gameTicScale = 1.0f; } /* From ae1b8ee2819a08c33e935046f911015403e111cb Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Sat, 29 Jun 2024 05:25:30 +0200 Subject: [PATCH 10/34] Change how com_frameTime is updated It used to be set with just com_frameTime = com_ticNumber * USERCMD_MSEC; where com_ticNumber is incremented in each idCommonLocal::SingleAsyncTic(). That worked well enough when USERCMD_MSEC was a fixed constant, but now changing com_gameHz could cause com_frameTime to decrease, which leads to all kinds of funny glitches like fading into the main menu when switching mods taking way too long, resulting in a black screen (when the mod has higher com_gameHz => lower USERCMD_MSEC). So now this is done in a function with slightly more logic that adds to com_frameTime instead of recalculating it with a multiplication. I also changed how SingleAsyncTic() (or idCommonLocal::Async()) is called, by using a proper thread instead of an SDL_Timer. This gives me more control over it so it can be more precise (and, as far as I can tell, it is) --- neo/framework/Common.cpp | 72 +++++++++++++++++++++++++-------------- neo/framework/Session.cpp | 4 ++- neo/idlib/Parser.cpp | 4 +-- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/neo/framework/Common.cpp b/neo/framework/Common.cpp index e121ee7d6..b3b42b291 100644 --- a/neo/framework/Common.cpp +++ b/neo/framework/Common.cpp @@ -122,7 +122,7 @@ int time_gameDraw; int time_frontend; // renderSystem frontend time int time_backend; // renderSystem backend time -int com_frameTime; // time for the current frame in milliseconds - TODO: DG: make it double? +int com_frameTime; // time (since start) for the current frame in milliseconds - TODO: DG: make it double? int com_frameNumber; // variable frame number volatile int com_ticNumber; // 60 hz tics int com_editors; // currently opened editor(s) @@ -250,12 +250,29 @@ class idCommonLocal : public idCommon { idCompressor * config_compressor; #endif - SDL_TimerID async_timer; + static int AsyncThread(void* arg); + xthreadInfo asyncThread; + volatile bool runAsyncThread; }; idCommonLocal commonLocal; idCommon * common = &commonLocal; +// DG: updates com_frameTime based on the current tic number and USERCMD_MSEC (com_gameFrameTime == 1000/com_gameHz) +void Com_UpdateFrameTime() { + // It used to be just com_frameTime = com_ticNumber * USERCMD_MSEC; + // But now that USERCMD_MSEC isn't fixed to 16 for fixed 60fps anymore (thanks to com_gameHz), + // that doesn't work anymore (com_frameTime would decrease when setting com_gameHz to a lower value!) + // So I moved updating it into a function (it's done in 3 places) that has just slightly more logic + // to ensure com_frameTime never decreases (well, until it overflows :-p) + static int lastTicNum = 0; + int ticNum = com_ticNumber; + int ticDiff = ticNum - lastTicNum; + assert(ticDiff >= 0); + com_frameTime += ticDiff * USERCMD_MSEC; + lastTicNum = ticNum; +} + /* ================== idCommonLocal::idCommonLocal @@ -279,7 +296,8 @@ idCommonLocal::idCommonLocal( void ) { config_compressor = NULL; #endif - async_timer = 0; + memset( &asyncThread, 0, sizeof(asyncThread) ); + runAsyncThread = false; } /* @@ -2458,7 +2476,7 @@ void idCommonLocal::Frame( void ) { // DG: prepare new ImGui frame - I guess this is a good place, as all new events should be available? D3::ImGuiHooks::NewFrame(); - com_frameTime = com_ticNumber * USERCMD_MSEC; // TODO: can we continue using 60hz tics and still run at higher fps? com_frameNumber vs com_ticNumber suggests that.. + Com_UpdateFrameTime(); // DG: put updating com_frameTime into a function idAsyncNetwork::RunFrame(); @@ -2504,7 +2522,7 @@ idCommonLocal::GUIFrame void idCommonLocal::GUIFrame( bool execCmd, bool network ) { Sys_GenerateEvents(); eventLoop->RunEventLoop( execCmd ); // and execute any commands - com_frameTime = com_ticNumber * USERCMD_MSEC; + Com_UpdateFrameTime(); // DG: put updating com_frameTime into a function if ( network ) { idAsyncNetwork::RunFrame(); } @@ -2541,8 +2559,8 @@ typedef struct { static const int MAX_ASYNC_STATS = 1024; asyncStats_t com_asyncStats[MAX_ASYNC_STATS]; // indexed by com_ticNumber -int prevAsyncMsec; -int lastTicMsec; +static int lastTicMsec; +static int nextTicTargetMsec; // when (according to Sys_Milliseconds()) the next async tic should start void idCommonLocal::SingleAsyncTic( void ) { // main thread code can prevent this from happening while modifying @@ -2593,6 +2611,7 @@ void idCommonLocal::Async( void ) { if ( !com_preciseTic.GetBool() ) { // just run a single tic, even if the exact msec isn't precise SingleAsyncTic(); + nextTicTargetMsec = msec + USERCMD_MSEC; return; } @@ -2618,6 +2637,7 @@ void idCommonLocal::Async( void ) { SingleAsyncTic(); lastTicMsec += ticMsec; } + nextTicTargetMsec = lastTicMsec + ticMsec; } /* @@ -2814,21 +2834,22 @@ void idCommonLocal::SetMachineSpec( void ) { } } -static unsigned int AsyncTimer(unsigned int interval, void *) { - common->Async(); - Sys_TriggerEvent(TRIGGER_EVENT_ONE); +int idCommonLocal::AsyncThread(void* arg) +{ + idCommonLocal* self = (idCommonLocal*)arg; + + while ( self->runAsyncThread ) { - // calculate the next interval to get as close to 60fps as possible - unsigned int now = SDL_GetTicks(); - unsigned int tick = com_ticNumber * USERCMD_MSEC; - // FIXME: this is pretty broken and basically always returns 1 because now now is much bigger than tic - // (probably com_tickNumber only starts incrementing a second after engine starts?) - // only reason this works is common->Async() checking again before calling SingleAsyncTic() + self->Async(); - if (now >= tick) - return 1; + Sys_TriggerEvent(TRIGGER_EVENT_ONE); - return tick - now; + // TODO: -1 is so we don't sleep too long - would -2 be better, or can we have a more precise sleep? + // IIRC especially on Windows sleeping is imprecise by at least on MS + int sleepTime = Max( 0, nextTicTargetMsec - (int)Sys_Milliseconds() - 1 ); + Sys_Sleep( sleepTime ); + } + return 0; } #ifdef _WIN32 @@ -3096,10 +3117,8 @@ void idCommonLocal::Init( int argc, char **argv ) { Sys_Error( "Error during initialization" ); } - async_timer = SDL_AddTimer(USERCMD_MSEC, AsyncTimer, NULL); // TODO: maybe rework the whole async timer thing, might be too imprecise - - if (!async_timer) - Sys_Error("Error while starting the async timer: %s", SDL_GetError()); + runAsyncThread = true; + Sys_CreateThread( AsyncThread, this, asyncThread, "AsyncThread" ); } @@ -3109,9 +3128,10 @@ idCommonLocal::Shutdown ================= */ void idCommonLocal::Shutdown( void ) { - if (async_timer) { - SDL_RemoveTimer(async_timer); - async_timer = 0; + if ( asyncThread.threadHandle != NULL ) { + runAsyncThread = false; + Sys_DestroyThread( asyncThread ); + memset( &asyncThread, 0, sizeof(asyncThread) ); } idAsyncNetwork::server.Kill(); diff --git a/neo/framework/Session.cpp b/neo/framework/Session.cpp index 184a90ee3..08f257b69 100644 --- a/neo/framework/Session.cpp +++ b/neo/framework/Session.cpp @@ -531,6 +531,8 @@ void idSessionLocal::CompleteWipe() { } } +extern void Com_UpdateFrameTime(); + /* ================ idSessionLocal::ShowLoadingGui @@ -548,7 +550,7 @@ void idSessionLocal::ShowLoadingGui() { int stop = Sys_Milliseconds() + 1000; int force = 10; while ( Sys_Milliseconds() < stop || force-- > 0 ) { - com_frameTime = com_ticNumber * USERCMD_MSEC; + Com_UpdateFrameTime(); // DG: put updating com_frameTime into a function session->Frame(); session->UpdateScreen( false ); } diff --git a/neo/idlib/Parser.cpp b/neo/idlib/Parser.cpp index bd4fe7b76..a8455eb42 100644 --- a/neo/idlib/Parser.cpp +++ b/neo/idlib/Parser.cpp @@ -1153,7 +1153,7 @@ int idParser::Directive_define( void ) { int line = token.line; // change "#define GAME_FPS 60" to "#define GAME_FPS sys.getTicsPerSecond()" - // (or equivalent for GAME_FRAMETIME and sys.getFrameTime()) + // (or equivalent for GAME_FRAMETIME and sys.getRawFrameTime()) hackTokens[0] = createToken( "sys", TT_NAME, 3, line ); hackTokens[1] = createToken( ".", TT_PUNCTUATION, P_REF, line ); hackTokens[2] = createToken( funName, TT_NAME, strlen(funName), line ); // getTicsPerSecond or getFrameTime @@ -1206,7 +1206,7 @@ int idParser::Directive_define( void ) { if ( numHackTokens != 0 ) { // skip rest of the line, inject our hackTokens instead and return while ( idParser::ReadLine( &token ) ) - {}; + {} define->tokens = hackTokens[0]; last = hackTokens[0]; From 6d6d38d551b8c9bc7da675dafafae299740faf37 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Tue, 2 Jul 2024 03:11:45 +0200 Subject: [PATCH 11/34] Dhewm3SettingsMenu: Add setting for com_gameHz --- neo/framework/Common.cpp | 3 ++- neo/framework/Dhewm3SettingsMenu.cpp | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/neo/framework/Common.cpp b/neo/framework/Common.cpp index b3b42b291..bafc74fd1 100644 --- a/neo/framework/Common.cpp +++ b/neo/framework/Common.cpp @@ -110,7 +110,8 @@ idCVar com_dbgServerAdr( "com_dbgServerAdr", "localhost", CVAR_SYSTEM | CVAR_ARC idCVar com_product_lang_ext( "com_product_lang_ext", "1", CVAR_INTEGER | CVAR_SYSTEM | CVAR_ARCHIVE, "Extension to use when creating language files." ); // DG: the next block is for configurable framerate -idCVar com_gameHz( "com_gameHz", "60", CVAR_INTEGER | CVAR_ARCHIVE | CVAR_SYSTEM, "Frames per second the game runs at", 10, 480 ); // TODO: make it float? make it default to 62.5? +#define COM_GAMEHZ_DESCR "Frames per second the game should run at - keep in mind that Vertical Sync (or a too slow computer) may slow it down, and that running below this configured framerate can cause problems!" +idCVar com_gameHz( "com_gameHz", "60", CVAR_INTEGER | CVAR_ARCHIVE | CVAR_SYSTEM, COM_GAMEHZ_DESCR, 10, 480 ); // TODO: make it float? make it default to 62.5? // the next three values will be set based on com_gameHz int com_gameHzVal = 60; int com_gameFrameTime = 16; // length of one frame in msec, 1000 / com_gameHz diff --git a/neo/framework/Dhewm3SettingsMenu.cpp b/neo/framework/Dhewm3SettingsMenu.cpp index 66d654a48..4b2ab5a4f 100644 --- a/neo/framework/Dhewm3SettingsMenu.cpp +++ b/neo/framework/Dhewm3SettingsMenu.cpp @@ -2227,6 +2227,7 @@ static CVarOption gameOptions[] = { CVarOption( "ui_autoReload", "Auto Weapon Reload", OT_BOOL ), CVarOption( "ui_autoSwitch", "Auto Weapon Switch", OT_BOOL ), CVarOption( "Visual" ), + CVarOption( "com_gameHz", "Framerate", OT_INT, 10, 500 ), CVarOption( "g_showHud", "Show HUD", OT_BOOL ), CVarOption( "com_showFPS", "Show Framerate (FPS)", OT_BOOL ), CVarOption( "ui_showGun", "Show Gun Model", OT_BOOL ), From 694e8de4e012c9e9ebdf30e3c29f9f21b614fa28 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Tue, 2 Jul 2024 03:14:42 +0200 Subject: [PATCH 12/34] Use r_displayRefresh for real fullscreen mode (SDL2-only) So far dhewm3 ignored that CVar, now it tries to set that refreshrate Also display the current refreshrate in the Dhewm3SettingsMenu, but no way to configure it there yet. --- neo/framework/Dhewm3SettingsMenu.cpp | 8 ++- neo/renderer/RenderSystem_init.cpp | 2 +- neo/renderer/tr_local.h | 7 +- neo/sys/glimp.cpp | 99 +++++++++++++++------------- neo/sys/stub/stub_gl.cpp | 4 +- 5 files changed, 65 insertions(+), 55 deletions(-) diff --git a/neo/framework/Dhewm3SettingsMenu.cpp b/neo/framework/Dhewm3SettingsMenu.cpp index 4b2ab5a4f..4b8e6fdb9 100644 --- a/neo/framework/Dhewm3SettingsMenu.cpp +++ b/neo/framework/Dhewm3SettingsMenu.cpp @@ -1758,9 +1758,11 @@ static bool VideoHasResettableChanges() return false; } +static glimpParms_t curState; + static bool VideoHasApplyableChanges() { - glimpParms_t curState = GLimp_GetCurState(); + curState = GLimp_GetCurState(); int wantedWidth = 0, wantedHeight = 0; R_GetModeInfo( &wantedWidth, &wantedHeight, r_mode.GetInteger() ); if ( wantedWidth != curState.width || wantedHeight != curState.height ) { @@ -1914,10 +1916,10 @@ static void DrawVideoOptionsMenu() float scale = float(glConfig.vidWidth)/glConfig.winWidth; int pw = scale * displayRect.w; int ph = scale * displayRect.h; - ImGui::TextDisabled( "Display Size: %d x %d (Physical: %d x %d)", displayRect.w, displayRect.h, pw, ph ); + ImGui::TextDisabled( "Display Size: %d x %d (Physical: %d x %d) @ %d Hz", displayRect.w, displayRect.h, pw, ph, curState.displayHz ); } else { ImGui::TextDisabled( "Current Resolution: %d x %d", glConfig.vidWidth, glConfig.vidHeight ); - ImGui::TextDisabled( "Display Size: %d x %d", displayRect.w, displayRect.h ); + ImGui::TextDisabled( "Display Size: %d x %d @ %d Hz", displayRect.w, displayRect.h, curState.displayHz ); } // MSAA diff --git a/neo/renderer/RenderSystem_init.cpp b/neo/renderer/RenderSystem_init.cpp index 58b47a5ee..cd5792fe4 100644 --- a/neo/renderer/RenderSystem_init.cpp +++ b/neo/renderer/RenderSystem_init.cpp @@ -59,7 +59,7 @@ idCVar r_inhibitFragmentProgram( "r_inhibitFragmentProgram", "0", CVAR_RENDERER idCVar r_useLightPortalFlow( "r_useLightPortalFlow", "1", CVAR_RENDERER | CVAR_BOOL, "use a more precise area reference determination" ); idCVar r_multiSamples( "r_multiSamples", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "number of antialiasing samples" ); idCVar r_mode( "r_mode", "5", CVAR_ARCHIVE | CVAR_RENDERER | CVAR_INTEGER, "video mode number" ); -idCVar r_displayRefresh( "r_displayRefresh", "0", CVAR_RENDERER | CVAR_INTEGER | CVAR_NOCHEAT, "optional display refresh rate option for vid mode", 0.0f, 200.0f ); +idCVar r_displayRefresh( "r_displayRefresh", "0", CVAR_RENDERER | CVAR_INTEGER | CVAR_NOCHEAT | CVAR_ARCHIVE, "optional display refresh rate option for vid mode", 0.0f, 500.0f ); idCVar r_fullscreen( "r_fullscreen", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "0 = windowed, 1 = full screen" ); idCVar r_fullscreenDesktop( "r_fullscreenDesktop", "0", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_BOOL, "0: 'real' fullscreen mode 1: keep resolution 'desktop' fullscreen mode" ); idCVar r_customWidth( "r_customWidth", "720", CVAR_RENDERER | CVAR_ARCHIVE | CVAR_INTEGER, "custom screen width. set r_mode to -1 to activate" ); diff --git a/neo/renderer/tr_local.h b/neo/renderer/tr_local.h index 55183e993..b30bf425b 100644 --- a/neo/renderer/tr_local.h +++ b/neo/renderer/tr_local.h @@ -1082,8 +1082,9 @@ bool GLimp_Init( glimpParms_t parms ); // The renderer will then reset the glimpParms to "safe mode" of 640x480 // fullscreen and try again. If that also fails, the error will be fatal. -bool GLimp_SetScreenParms( glimpParms_t parms ); -// will set up gl up with the new parms +bool GLimp_SetScreenParms( glimpParms_t parms, bool fromInit = false ); +// will set up gl up with the new parms (set multisamples to -1 if you don't care about them) +// fromInit should only be set when called from GLimp_Init() void GLimp_Shutdown( void ); // Destroys the rendering context, closes the window, resets the resolution, @@ -1126,7 +1127,7 @@ bool GLimp_SetSwapInterval( int swapInterval ); bool GLimp_SetWindowResizable( bool enableResizable ); void GLimp_UpdateWindowSize(); -glimpParms_t GLimp_GetCurState(); +glimpParms_t GLimp_GetCurState( bool checkConsistency = true ); /* ==================================================================== diff --git a/neo/sys/glimp.cpp b/neo/sys/glimp.cpp index 1f0bad2e4..d1c736e79 100644 --- a/neo/sys/glimp.cpp +++ b/neo/sys/glimp.cpp @@ -359,58 +359,40 @@ bool GLimp_Init(glimpParms_t parms) { common->Warning("Can't get display mode: %s\n", SDL_GetError()); return false; // trying other color depth etc is unlikely to help with this issue } - if ((real_mode.w != parms.width) || (real_mode.h != parms.height)) + bool gotRequestedResolution = (real_mode.w == parms.width && real_mode.h == parms.height); + bool gotRequestedDisplayHz = (parms.displayHz == 0 || real_mode.refresh_rate == parms.displayHz); + if ( !gotRequestedResolution || !gotRequestedDisplayHz ) { - common->Warning("Current display mode isn't requested display mode\n"); - common->Warning("Likely SDL bug #4700, trying to work around it..\n"); + if ( !gotRequestedResolution ) { + common->Warning("Current display mode isn't requested display mode\n"); + common->Warning("Likely SDL bug #4700, trying to work around it..\n"); + } + if ( !gotRequestedDisplayHz ) { + common->Printf( "Requested a different display refreshrate than the default one, need to set it..\n" ); + } int dIdx = SDL_GetWindowDisplayIndex(window); if(dIdx != displayIndex) { common->Warning("Window's display index is %d, but we wanted %d!\n", dIdx, displayIndex); } - /* Mkay, try to hack around that. */ - SDL_DisplayMode wanted_mode = {}; - - wanted_mode.w = parms.width; - wanted_mode.h = parms.height; - - if (SDL_SetWindowDisplayMode(window, &wanted_mode) != 0) + // try again - this time uses GLimp_SetScreenParms(), it contains the code for this + // and also handles the refreshrate (that can't be set in SDL_CreateWindow()) + glimpParms_t parms2 = parms; + parms2.multiSamples = -1; // ignore multisample settings. + if ( !GLimp_SetScreenParms(parms2, true) ) { SDL_DestroyWindow(window); window = NULL; - common->Warning("Can't force resolution to %ix%i: %s\n", parms.width, parms.height, SDL_GetError()); - return false; // trying other color depth etc is unlikely to help with this issue } - - /* The SDL doku says, that SDL_SetWindowSize() shouldn't be - used on fullscreen windows. But at least in my test with - SDL 2.0.9 the subsequent SDL_GetWindowDisplayMode() fails - if I don't call it. */ - SDL_SetWindowSize(window, wanted_mode.w, wanted_mode.h); - - if (SDL_GetWindowDisplayMode(window, &real_mode) != 0) + SDL_DisplayMode real_mode = {}; + if ( SDL_GetWindowDisplayMode( window, &real_mode ) != 0 ) { - SDL_DestroyWindow(window); - window = NULL; - - common->Warning("Can't get display mode: %s\n", SDL_GetError()); - + common->Warning( "GLimp_SetScreenParms(): Can't get display mode: %s\n", SDL_GetError() ); return false; // trying other color depth etc is unlikely to help with this issue } - - if ((real_mode.w != parms.width) || (real_mode.h != parms.height)) - { - SDL_DestroyWindow(window); - window = NULL; - - common->Warning("Still in wrong display mode: %ix%i instead of %ix%i\n", - real_mode.w, real_mode.h, parms.width, parms.height); - - return false; // trying other color depth etc is unlikely to help with this issue - } - common->Warning("Now we have the requested resolution (%d x %d)\n", parms.width, parms.height); + common->Warning("Now we have the requested resolution (%d x %d @ %d Hz)\n", parms.width, parms.height, real_mode.refresh_rate); } } @@ -630,19 +612,18 @@ bool GLimp_Init(glimpParms_t parms) { GLimp_SetScreenParms =================== */ -bool GLimp_SetScreenParms(glimpParms_t parms) { +bool GLimp_SetScreenParms( glimpParms_t parms, bool fromInit ) { #if SDL_VERSION_ATLEAST(2, 0, 0) - glimpParms_t curState = GLimp_GetCurState(); + glimpParms_t curState = GLimp_GetCurState( !fromInit ); if( parms.multiSamples != -1 && parms.multiSamples != curState.multiSamples ) { + common->Printf( " (GLimp_SetScreenParms() not possible because multiSample settings have changed: Have %d, want %d)\n", curState.multiSamples, parms.multiSamples ); // if MSAA settings have changed, we really need a vid_restart return false; } bool wantFullscreenDesktop = parms.fullScreen && parms.fullScreenDesktop; - // TODO: parms.displayHz ? - if ( curState.fullScreenDesktop && wantFullscreenDesktop ) { return true; // nothing to do (resolution is not configurable in that mode) } @@ -671,8 +652,20 @@ bool GLimp_SetScreenParms(glimpParms_t parms) { wanted_mode.w = parms.width; wanted_mode.h = parms.height; + wanted_mode.refresh_rate = parms.displayHz; + + SDL_DisplayMode closest_mode = {}; + int displayIndex = SDL_GetWindowDisplayIndex( window ); + if ( SDL_GetClosestDisplayMode( displayIndex, &wanted_mode, &closest_mode ) == NULL ) { + common->Warning( "GLimp_SetScreenParms(): Couldn't get a matching fullscreen display mode for %d x %d on display %d (%s): %s\n", + parms.width, parms.height, displayIndex, SDL_GetDisplayName( displayIndex ), SDL_GetError() ); + return false; + } - // TODO: refresh rate? parms.displayHz should probably try to get most similar mode before trying to set it? + if ( parms.displayHz != 0 && closest_mode.refresh_rate != 0 && parms.displayHz != closest_mode.refresh_rate ) { + common->Warning( "GLimp_SetScreenParms(): Couldn't find a fullscreen display mode with requested refresh rate %d, getting %d instead\n", + parms.displayHz, closest_mode.refresh_rate ); + } if ( SDL_SetWindowDisplayMode( window, &wanted_mode ) != 0 ) { @@ -692,7 +685,13 @@ bool GLimp_SetScreenParms(glimpParms_t parms) { if ( SDL_GetWindowDisplayMode( window, &real_mode ) != 0 ) { common->Warning( "GLimp_SetScreenParms(): Can't get display mode: %s\n", SDL_GetError() ); - return false; // trying other color depth etc is unlikely to help with this issue + return false; + } + + if ( parms.displayHz != 0 && real_mode.refresh_rate != 0 && parms.displayHz != real_mode.refresh_rate ) { + common->Warning( "GLimp_SetScreenParms(): Couldn't get the requested refresh rate %d, got %d instead\n", + parms.displayHz, real_mode.refresh_rate ); + // don't make this an error, I think } if ( (real_mode.w != wanted_mode.w) || (real_mode.h != wanted_mode.h) ) @@ -717,7 +716,7 @@ bool GLimp_SetScreenParms(glimpParms_t parms) { // sets a glimpParms_t based on the current true state (according to SDL) // Note: here, ret.fullScreenDesktop is only true if currently in fullscreen desktop mode // (and ret.fullScreen is true as well) -glimpParms_t GLimp_GetCurState() +glimpParms_t GLimp_GetCurState( bool checkConsistency) { glimpParms_t ret = {}; @@ -745,13 +744,21 @@ glimpParms_t GLimp_GetCurState() } else { common->Warning( "GLimp_GetCurState(): Can't get display mode: %s\n", SDL_GetError() ); } + } else { + SDL_DisplayMode real_mode = {}; + int displayIndex = SDL_GetWindowDisplayIndex( window ); + if ( SDL_GetDesktopDisplayMode(displayIndex, &real_mode) == 0 ) { + ret.displayHz = real_mode.refresh_rate; + } } if ( ret.width == 0 && ret.height == 0 ) { // windowed mode or SDL_GetWindowDisplayMode() failed SDL_GetWindowSize( window, &ret.width, &ret.height ); } - assert( ret.width == glConfig.winWidth && ret.height == glConfig.winHeight ); - assert( ret.fullScreen == glConfig.isFullscreen ); + if ( checkConsistency ) { + assert( ret.width == glConfig.winWidth && ret.height == glConfig.winHeight ); + assert( ret.fullScreen == glConfig.isFullscreen ); + } #else assert( 0 && "Don't use GLimp_GetCurState() with SDL1.2 !" ); diff --git a/neo/sys/stub/stub_gl.cpp b/neo/sys/stub/stub_gl.cpp index ec4bf8754..962d38688 100644 --- a/neo/sys/stub/stub_gl.cpp +++ b/neo/sys/stub/stub_gl.cpp @@ -391,7 +391,7 @@ GLExtension_t GLimp_ExtensionPointer( const char *a) { return StubFunction; }; bool GLimp_Init(glimpParms_t a) {return true;}; void GLimp_SetGamma(unsigned short*a, unsigned short*b, unsigned short*c) {}; void GLimp_ResetGamma() {} -bool GLimp_SetScreenParms(glimpParms_t parms) { return true; }; +bool GLimp_SetScreenParms(glimpParms_t parms, bool) { return true; }; void GLimp_Shutdown() {}; void GLimp_SwapBuffers() {}; void GLimp_ActivateContext() {}; @@ -400,7 +400,7 @@ void GLimp_GrabInput(int flags) {}; bool GLimp_SetSwapInterval( int swapInterval ) { return false; } void GLimp_UpdateWindowSize() {} bool GLimp_SetWindowResizable( bool enableResizable ) { return false; } -glimpParms_t GLimp_GetCurState() { glimpParms_t ret = {}; return ret; } +glimpParms_t GLimp_GetCurState(bool) { glimpParms_t ret = {}; return ret; } #ifdef _MSC_VER #pragma warning(pop) From caa83c574b609f408a8c6be70d7216523b31624c Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Tue, 2 Jul 2024 17:48:18 +0200 Subject: [PATCH 13/34] Introduce Sys_MillisecondsPrecise() and Sys_SleepUntilPrecise() Sys_MillisecondsPrecise() returns the milliseconds since start of the engine as a double, so it can also represent micro- or even nanoseconds. Sys_Milliseconds() now just returns that value casted to unsigned int, for backwards compatibility. Sys_SleepUntilPrecise( double targetTimeMS ) sleeps until Sys_MillisecondsPrecise() returns a value >= targetTimeMS. It aims to have a precision of about 0.01ms (10usec), and achieves that by using a busy loop (with pause instructions) for the last 1-2 ms --- neo/sys/aros/aros_main.cpp | 56 +++++++++++ neo/sys/posix/posix_main.cpp | 178 +++++++++++++++++++++++++++++++++++ neo/sys/sys_public.h | 7 ++ neo/sys/threads.cpp | 2 +- neo/sys/win32/win_main.cpp | 127 +++++++++++++++++++++++++ 5 files changed, 369 insertions(+), 1 deletion(-) diff --git a/neo/sys/aros/aros_main.cpp b/neo/sys/aros/aros_main.cpp index afad94f7f..97da94f99 100644 --- a/neo/sys/aros/aros_main.cpp +++ b/neo/sys/aros/aros_main.cpp @@ -923,6 +923,60 @@ void idSysLocal::OpenURL( const char *url, bool quit ) { AROS_OpenURL( url ); } + + +// DG: apparently AROS supports clock_gettime(), so use that for Sys_MillisecondsPrecise() +static struct timespec first; +static void AROS_initTime() { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + + long nsec = now.tv_nsec; + long long sec = now.tv_sec; + // set back first by 1.5ms so neither Sys_MillisecondsPrecise() nor Sys_Milliseconds() + // (which calls Sys_MillisecondsPrecise()) will ever return 0 or even a negative value + nsec -= 1500000; + if(nsec < 0) + { + nsec += 1000000000ll; // 1s in ns => definitely positive now + --sec; + } + + first.tv_sec = sec; + first.tv_nsec = nsec; +} + +/* +======================= +Sys_MillisecondsPrecise +======================= +*/ +double Sys_MillisecondsPrecise() { + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + + long long sec = now.tv_sec - first.tv_sec; + long long nsec = now.tv_nsec - first.tv_nsec; + + double ret = sec * 1000.0; + ret += double(nsec) * 0.000001; + return ret; +} + +void Sys_SleepUntilPrecise( double targetTimeMS ) { + // TODO: I know nothing about how precise AROS' sleep implementation is, + // and apparently even their clock_gettime() implementation only has milliseconds resolution + // (https://aros.sourceforge.io/documentation/developers/autodocs/posixc.php#clock-gettime) + // So I'm keeping this extremely simple. + // See POSIX implementation for an actually precise implementation (that requires precise clock_gettime()) + double now = Sys_MillisecondsPrecise(); + double msToWait = targetTimeMS - now; + if ( msToWait > 0.1 ) { + unsigned long usec = msToWait * 1000; + usleep( usec - 100 ); // sleep 100usec less so we don't oversleep that much + } +} + /* =============== main @@ -931,6 +985,8 @@ main int main(int argc, char **argv) { bug("[ADoom3] %s()\n", __PRETTY_FUNCTION__); + AROS_initTime(); + AROS_EarlyInit( ); if ( argc > 1 ) { diff --git a/neo/sys/posix/posix_main.cpp b/neo/sys/posix/posix_main.cpp index ee975d323..492bfd0f4 100644 --- a/neo/sys/posix/posix_main.cpp +++ b/neo/sys/posix/posix_main.cpp @@ -51,6 +51,11 @@ If you have questions concerning this license or the applicable additional terms #include // clipboard +#ifdef __APPLE__ // for clock_get_time() in Sys_MillisecondsPrecise() +#include +#include +#endif + #define COMMAND_HISTORY 64 static int input_hide = 0; @@ -413,6 +418,177 @@ int Sys_GetDriveFreeSpace( const char *path ) { return 1000 * 1024; } +// ---------- Time Stuff ------------- + +// D3_CpuPause() abstracts a CPU pause instruction, to make busy waits a bit less power-hungry +// (code taken from Yamagi Quake II) +#ifdef SDL_CPUPauseInstruction + #define D3_CpuPause() SDL_CPUPauseInstruction() +#elif defined(__GNUC__) + #if (__i386 || __x86_64__) + #define D3_CpuPause() asm volatile("pause") + #elif defined(__aarch64__) || (defined(__ARM_ARCH) && __ARM_ARCH >= 7) || defined(__ARM_ARCH_6K__) + #define D3_CpuPause() asm volatile("yield") + #elif defined(__powerpc__) || defined(__powerpc64__) + #define D3_CpuPause() asm volatile("or 27,27,27") + #elif defined(__riscv) && __riscv_xlen == 64 + #define D3_CpuPause() asm volatile(".insn i 0x0F, 0, x0, x0, 0x010"); + #endif +#endif + +#ifndef D3_CpuPause + #warning "No D3_CpuPause implementation for this platform/architecture! Will busy-wait sometimes!" + // TODO: something that prevents the loop from being optimized away? + //#define D3_CpuPause() +#endif + + +#ifdef __APPLE__ + static mach_timespec_t first; +#else + static struct timespec first; + + #ifdef _POSIX_MONOTONIC_CLOCK + #define D3_GETTIME_CLOCK CLOCK_MONOTONIC + #else + #define D3_GETTIME_CLOCK CLOCK_REALTIME + #endif +#endif + +static size_t pauseLoopsPer5usec = 100; // set in initTime() + +static void Posix_InitTime() { +#ifdef __APPLE__ + // OSX didn't have clock_gettime() until recently, so use Mach's clock_get_time() + // instead. fortunately its mach_timespec_t seems identical to POSIX struct timespec + // so lots of code can be shared + clock_serv_t cclock; + mach_timespec_t now; + + host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, &cclock); + clock_get_time(cclock, &now); + mach_port_deallocate(mach_task_self(), cclock); + +#else // not __APPLE__ - other Unix-likes will hopefully support clock_gettime() + struct timespec now; + clock_gettime(D3_GETTIME_CLOCK, &now); +#endif + + long nsec = now.tv_nsec; + long long sec = now.tv_sec; + // set back first by 1.5ms so neither Sys_MillisecondsPrecise() nor Sys_Milliseconds() + // (which calls Sys_MillisecondsPrecise()) will ever return 0 or even a negative value + nsec -= 1500000; + if(nsec < 0) + { + nsec += 1000000000ll; // 1s in ns => definitely positive now + --sec; + } + + first.tv_sec = sec; + first.tv_nsec = nsec; + + double before = Sys_MillisecondsPrecise(); + for ( int i=0; i<1000; ++i ) { + // volatile so the call doesn't get optimized away + volatile double x = Sys_MillisecondsPrecise(); + (void)x; + } + double after = Sys_MillisecondsPrecise(); + double callDiff = after - before; + +#ifdef D3_CpuPause + // figure out how long D3_CpuPause() instructions take + before = Sys_MillisecondsPrecise(); + for( int i=0; i < 1000000; ++i ) { + // call it 4 times per loop, so the ratio between pause and loop-instructions is better + D3_CpuPause(); D3_CpuPause(); D3_CpuPause(); D3_CpuPause(); + } + after = Sys_MillisecondsPrecise(); + double diff = after - before; + double onePauseIterTime = diff / 1000000.0; + if ( onePauseIterTime > 0.00000001 ) { + double loopsPer10usec = 0.005 / onePauseIterTime; + pauseLoopsPer5usec = loopsPer10usec; + printf( "Posix_InitTime(): A call to Sys_MillisecondsPrecise() takes about %g nsec; 1mio pause loops took %g ms => pauseLoopsPer5usec = %zd\n", + callDiff*1000.0, diff, pauseLoopsPer5usec ); + if ( pauseLoopsPer5usec == 0 ) + pauseLoopsPer5usec = 1; + } else { + assert( 0 && "apparently 1mio pause loops are so fast we can't even measure it?!" ); + pauseLoopsPer5usec = 1000000; + } + // Note: Due to CPU frequency scaling this is not super precise, but it should be within + // an order of magnitude of the real current value, I think, which should suffice for our purposes +#else + printf( "Posix_InitTime(): A call to Sys_MillisecondsPrecise() takes about %g nsecs\n", callDiff*1000.0 ); +#endif +} + +/* +======================= +Sys_MillisecondsPrecise +======================= +*/ +double Sys_MillisecondsPrecise() { +#ifdef __APPLE__ + // OSX didn't have clock_gettime() until recently, so use Mach's clock_get_time() + // instead. fortunately its mach_timespec_t seems identical to POSIX struct timespec + // so lots of code can be shared + clock_serv_t cclock; + mach_timespec_t now; + + host_get_clock_service(mach_host_self(), SYSTEM_CLOCK, &cclock); + clock_get_time(cclock, &now); + mach_port_deallocate(mach_task_self(), cclock); + +#else // not __APPLE__ - other Unix-likes will hopefully support clock_gettime() + struct timespec now; + clock_gettime(D3_GETTIME_CLOCK, &now); +#endif + + long long sec = now.tv_sec - first.tv_sec; + long long nsec = now.tv_nsec - first.tv_nsec; + + double ret = sec * 1000.0; + ret += double(nsec) * 0.000001; + return ret; +} + + + +/* +===================== +Sys_SleepUntilPrecise +===================== +*/ +void Sys_SleepUntilPrecise( double targetTimeMS ) { + double msec = targetTimeMS - Sys_MillisecondsPrecise(); + if ( msec < 0.01 ) // don't bother for less than 10usec + return; + + // at least on Linux, usleep() is pretty precise, so use it for everything but the last 100usec (*micro*seconds) + // if it isn't on other platforms, set a different value here with an #ifdef + static const double sleepThreshold = 0.100; + + if ( msec > sleepThreshold ) { + useconds_t usec = (msec - sleepThreshold) * 1000.0; + // yes, usleep() is deprecated, I don't care, nanosleep() is more painful to use + usleep( usec ); + } + + // wait for the remaining time with a busy loop, as that has higher precision + do { +#ifdef D3_CpuPause + for ( size_t i=0; i < pauseLoopsPer5usec; ++i ) { + // call it 4 times per loop, so the ratio between pause and loop-instructions is better + D3_CpuPause(); D3_CpuPause(); D3_CpuPause(); D3_CpuPause(); + } +#endif + + msec = targetTimeMS - Sys_MillisecondsPrecise(); + } while ( msec >= 0.01 ); +} // ----------- lots of signal handling stuff ------------ @@ -628,6 +804,8 @@ static bool createPathRecursive(char* path) void Posix_InitSignalHandlers( void ) { + Posix_InitTime(); // the base time for Sys_MillisecondsPrecise() should be set very early + #ifdef D3_HAVE_LIBBACKTRACE // can't use idStr here and thus can't use Sys_GetPath(PATH_EXE) => added Posix_GetExePath() const char* exePath = Posix_GetExePath(); diff --git a/neo/sys/sys_public.h b/neo/sys/sys_public.h index dbff09dfd..11e3fb152 100644 --- a/neo/sys/sys_public.h +++ b/neo/sys/sys_public.h @@ -163,6 +163,13 @@ void Sys_Sleep( int msec ); // any game related timing information should come from event timestamps unsigned int Sys_Milliseconds( void ); +// like Sys_Milliseconds(), but with higher precision +double Sys_MillisecondsPrecise( void ); + +// sleep until Sys_MillisecondsPrecise() returns >= targetTimeMS +// aims for about 0.01ms precision (but might busy wait for the last 1.5ms or so) +void Sys_SleepUntilPrecise( double targetTimeMS ); + // returns a selection of the CPUID_* flags int Sys_GetProcessorId( void ); diff --git a/neo/sys/threads.cpp b/neo/sys/threads.cpp index 0b1132aea..dc030b93c 100644 --- a/neo/sys/threads.cpp +++ b/neo/sys/threads.cpp @@ -74,7 +74,7 @@ Sys_Milliseconds ================ */ unsigned int Sys_Milliseconds() { - return SDL_GetTicks(); + return (unsigned int)Sys_MillisecondsPrecise(); } /* diff --git a/neo/sys/win32/win_main.cpp b/neo/sys/win32/win_main.cpp index 8b3a1c814..7eb85f3b7 100644 --- a/neo/sys/win32/win_main.cpp +++ b/neo/sys/win32/win_main.cpp @@ -1001,6 +1001,131 @@ int Win_ChoosePixelFormat(HDC hdc) } #endif +// ---------- Time Stuff ------------- + +// D3_CpuPause() abstracts a CPU pause instruction, to make busy waits a bit less power-hungry +// (code taken from Yamagi Quake II) +#ifdef SDL_CPUPauseInstruction + #define D3_CpuPause() SDL_CPUPauseInstruction() +#elif defined(__GNUC__) + #if (__i386 || __x86_64__) + #define D3_CpuPause() asm volatile("pause") + #elif defined(__aarch64__) || (defined(__ARM_ARCH) && __ARM_ARCH >= 7) || defined(__ARM_ARCH_6K__) + #define D3_CpuPause() asm volatile("yield") + #elif defined(__powerpc__) || defined(__powerpc64__) + #define D3_CpuPause() asm volatile("or 27,27,27") + #elif defined(__riscv) && __riscv_xlen == 64 + #define D3_CpuPause() asm volatile(".insn i 0x0F, 0, x0, x0, 0x010"); + #endif +#elif defined(_MSC_VER) + #if defined(_M_IX86) || defined(_M_X64) + #define D3_CpuPause() _mm_pause() + #elif defined(_M_ARM) || defined(_M_ARM64) + #define D3_CpuPause() __yield() + #endif +#endif + +#ifndef D3_CpuPause + #warning "No D3_CpuPause implementation for this platform/architecture! Will busy-wait sometimes!" + // TODO: something that prevents the loop from being optimized away? + //#define D3_CpuPause() +#endif + +static double perfCountToMS = 0.0; // set in initTime() +static LARGE_INTEGER firstCount = { 0 }; + +static size_t pauseLoopsPer5usec = 100; // set in initTime() + +static void Win_InitTime() { + LARGE_INTEGER freq = { 0 }; + QueryPerformanceFrequency(&freq); // in Hz + perfCountToMS = 1000.0 / (double)freq.QuadPart; // 1/freq would be factor for seconds, we want milliseconds + QueryPerformanceCounter(&firstCount); + firstCount.QuadPart -= 1.5 / perfCountToMS; // make sure Sys_MillisecondsPrecise() always returns value >= 1 + + double before = Sys_MillisecondsPrecise(); + for ( int i=0; i < 1000; ++i ) { + // volatile so the call isn't optimized away + volatile double x = Sys_MillisecondsPrecise(); + (void)x; + } + double after = Sys_MillisecondsPrecise(); + double callDiff = after - before; + printf( "Win_InitTime(): 1000 calls to Sys_MillisecondsPrecise() took %g usec (%g usec per call)\n", diff*1000, diff ); + +#ifdef D3_CpuPause + // figure out how long D3_CpuPause() instructions take + before = Sys_MillisecondsPrecise(); + for( int i=0; i < 1000000; ++i ) { + // call it 4 times per loop, so the ratio between pause and loop-instructions is better + D3_CpuPause(); D3_CpuPause(); D3_CpuPause(); D3_CpuPause(); + } + after = Sys_MillisecondsPrecise(); + double diff = after - before; + double onePauseIterTime = diff / 1000000; + if ( onePauseIterTime > 0.00000001 ) { + double loopsPer10usec = 0.005 / onePauseIterTime; + pauseLoopsPer5usec = loopsPer10usec; + printf( "Win_InitTime(): A call to Sys_MillisecondsPrecise() takes about %g nsec; 1mio pause loops took %g ms => pauseLoopsPer5usec = %zd\n", + callDiff*1000.0, diff, pauseLoopsPer5usec ); + if ( pauseLoopsPer5usec == 0 ) + pauseLoopsPer5usec = 1; + } else { + assert( 0 && "apparently 1mio pause loops are so fast we can't even measure it?!" ); + pauseLoopsPer5usec = 1000000; + } + // Note: Due to CPU frequency scaling this is not super precise, but it should be within + // an order of magnitude of the real current value, I think, which should suffice for our purposes +#else + printf( "Win_InitTime(): A call to Sys_MillisecondsPrecise() takes about %g nsecs\n", callDiff*1000.0 ); +#endif +} + +/* +======================= +Sys_MillisecondsPrecise +======================= +*/ +double Sys_MillisecondsPrecise() { + LARGE_INTEGER cur; + QueryPerformanceCounter(&cur); + + double ret = cur.QuadPart - firstCount.QuadPart; + ret *= perfCountToMS; + return ret; +} + +/* +===================== +Sys_SleepUntilPrecise +===================== +*/ +void Sys_SleepUntilPrecise( double targetTimeMS ) { + double msec = targetTimeMS - Sys_MillisecondsPrecise(); + if ( msec < 0.01 ) // don't bother for less than 10usec + return; + + if ( msec > 2.0 ) { + // Note: Theoretically one could use SetWaitableTimer() and WaitForSingleObject() + // for higher precision, but last time I tested (on Win10), + // in practice that also only had millisecond-precision + dword sleepMS = msec - 1.0; // wait for last MS or so in busy(-ish) loop below + Sleep( sleepMS ); + } + + // wait for the remaining time with a busy loop, as that has higher precision + do { +#ifdef D3_CpuPause + for ( size_t i=0; i < pauseLoopsPer5usec; ++i ) { + // call it 4 times per loop, so the ratio between pause and loop-instructions is better + D3_CpuPause(); D3_CpuPause(); D3_CpuPause(); D3_CpuPause(); + } +#endif + + msec = targetTimeMS - Sys_MillisecondsPrecise(); + } while ( msec >= 0.01 ); +} + /* ================== WinMain @@ -1021,6 +1146,8 @@ int main(int argc, char *argv[]) { InitializeCriticalSection( &printfCritSect ); + Win_InitTime(); + #ifdef ID_DEDICATED MSG msg; #else From e8368e842208ff917163242f64807304e8e0fc22 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Tue, 2 Jul 2024 17:54:10 +0200 Subject: [PATCH 14/34] Make the Async Loop more precise by using Sys_MillisecondsPrecise() and Sys_SleepUntilPrecise() and by adding com_preciseFrameLengthMS (as float instead of int). Renamed com_gameFrameTime to com_gameFrameLengthMS, so it's less confusing with com_frameTime (which is the timestamp of the current frame, not its length) --- neo/framework/Common.cpp | 54 ++++++++++++++++++++++++-------------- neo/framework/Common.h | 6 +++-- neo/framework/UsercmdGen.h | 2 +- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/neo/framework/Common.cpp b/neo/framework/Common.cpp index bafc74fd1..883ff5abd 100644 --- a/neo/framework/Common.cpp +++ b/neo/framework/Common.cpp @@ -114,16 +114,19 @@ idCVar com_product_lang_ext( "com_product_lang_ext", "1", CVAR_INTEGER | CVAR_SY idCVar com_gameHz( "com_gameHz", "60", CVAR_INTEGER | CVAR_ARCHIVE | CVAR_SYSTEM, COM_GAMEHZ_DESCR, 10, 480 ); // TODO: make it float? make it default to 62.5? // the next three values will be set based on com_gameHz int com_gameHzVal = 60; -int com_gameFrameTime = 16; // length of one frame in msec, 1000 / com_gameHz +int com_gameFrameLengthMS = 16; // length of one frame in msec, 1000 / com_gameHz +float com_preciseFrameLengthMS = 16.6667f; // 1000.0f / gameHzVal float com_gameTicScale = 1.0f; // com_gameHzVal/60.0f, multiply stuff assuming one tic is 16ms with this +double com_preciseFrameTimeMS = 0; // like com_frameTime but as double: time (since start) for the current frame in milliseconds + // com_speeds times int time_gameFrame; int time_gameDraw; int time_frontend; // renderSystem frontend time int time_backend; // renderSystem backend time -int com_frameTime; // time (since start) for the current frame in milliseconds - TODO: DG: make it double? +int com_frameTime; // time (since start) for the current frame in milliseconds int com_frameNumber; // variable frame number volatile int com_ticNumber; // 60 hz tics int com_editors; // currently opened editor(s) @@ -270,7 +273,8 @@ void Com_UpdateFrameTime() { int ticNum = com_ticNumber; int ticDiff = ticNum - lastTicNum; assert(ticDiff >= 0); - com_frameTime += ticDiff * USERCMD_MSEC; + com_preciseFrameTimeMS += ticDiff * com_preciseFrameLengthMS; + com_frameTime = idMath::Rint( com_preciseFrameTimeMS ); lastTicNum = ticNum; } @@ -2560,8 +2564,8 @@ typedef struct { static const int MAX_ASYNC_STATS = 1024; asyncStats_t com_asyncStats[MAX_ASYNC_STATS]; // indexed by com_ticNumber -static int lastTicMsec; -static int nextTicTargetMsec; // when (according to Sys_Milliseconds()) the next async tic should start +static double lastTicMsec = 0.0; +static double nextTicTargetMsec = 0.0; // when (according to Sys_Milliseconds()) the next async tic should start void idCommonLocal::SingleAsyncTic( void ) { // main thread code can prevent this from happening while modifying @@ -2604,19 +2608,19 @@ idCommonLocal::Async ================= */ void idCommonLocal::Async( void ) { - int msec = Sys_Milliseconds(); // TODO: make double? + double msec = Sys_MillisecondsPrecise(); if ( !lastTicMsec ) { - lastTicMsec = msec - USERCMD_MSEC; + lastTicMsec = msec - com_preciseFrameLengthMS; } if ( !com_preciseTic.GetBool() ) { // just run a single tic, even if the exact msec isn't precise SingleAsyncTic(); - nextTicTargetMsec = msec + USERCMD_MSEC; + nextTicTargetMsec = msec + com_preciseFrameLengthMS; return; } - int ticMsec = USERCMD_MSEC; // TODO: make float? + float ticMsec = com_preciseFrameLengthMS; // the number of msec per tic can be varies with the timescale cvar float timescale = com_timescale.GetFloat(); @@ -2629,8 +2633,8 @@ void idCommonLocal::Async( void ) { // don't skip too many if ( timescale == 1.0f ) { - if ( lastTicMsec + 10 * USERCMD_MSEC < msec ) { - lastTicMsec = msec - 10*USERCMD_MSEC; + if ( lastTicMsec + 10 * com_preciseFrameLengthMS < msec ) { + lastTicMsec = msec - 10.0*com_preciseFrameLengthMS; } } @@ -2769,7 +2773,7 @@ void idCommonLocal::LoadGameDLL( void ) { // initialize the game object if ( game != NULL ) { - game->SetGameHz( com_gameHzVal, com_gameFrameTime, com_gameTicScale ); // DG: make sure it knows the ticrate + game->SetGameHz( com_gameHzVal, com_gameFrameLengthMS, com_gameTicScale ); // DG: make sure it knows the ticrate game->Init(); } } @@ -2841,14 +2845,22 @@ int idCommonLocal::AsyncThread(void* arg) while ( self->runAsyncThread ) { + // The idea is to make this run super-exact, but round *down* com_gameFrameTime (USERCMD_MSEC). + // Then (I think..) when the game thread actually runs (0.x ms later than it might expect) + // all the things that waited for USERCMD_MSEC will run because they're (slightly) overdue + // => they'll be exactly on time + // TODO: .. well, unless maybe if they waited for so many frametimes that we're a frame early.. + // But does it even matter for such long waits? + // (and also, so far USERCMD_MSEC *has* been rounded down from 16.6667 to 16, + // and with VSync enabled there was more or less correct timing) + self->Async(); + // idSessionLocal::Frame() waits for TRIGGER_EVENT_ONE + // => this syncs the main thread with the async thread Sys_TriggerEvent(TRIGGER_EVENT_ONE); - // TODO: -1 is so we don't sleep too long - would -2 be better, or can we have a more precise sleep? - // IIRC especially on Windows sleeping is imprecise by at least on MS - int sleepTime = Max( 0, nextTicTargetMsec - (int)Sys_Milliseconds() - 1 ); - Sys_Sleep( sleepTime ); + Sys_SleepUntilPrecise( nextTicTargetMsec ); } return 0; } @@ -3371,15 +3383,17 @@ void idCommonLocal::UpdateGameHz() { com_gameHz.ClearModified(); com_gameHzVal = com_gameHz.GetInteger(); + com_preciseFrameLengthMS = 1000.0f / com_gameHzVal; // only rounding up the frame time a little bit, so for 144hz (6.94ms) it becomes 7ms, // but for 60Hz (16.6667ms) it remains 16ms, like before - com_gameFrameTime = ( 1000.0f / com_gameHzVal ) + 0.1f; // TODO: idMath::Rint ? - com_gameTicScale = com_gameHzVal / 60.0f; // TODO: or / 62.5 ? + com_gameFrameLengthMS = com_preciseFrameLengthMS + 0.1f; // TODO: idMath::Rint ? or always round down? + + com_gameTicScale = com_gameHzVal / 60.0f; // TODO: or / 62.5? - Printf( "Running the game at com_gameHz = %dHz, frametime %dms\n", com_gameHzVal, com_gameFrameTime ); + Printf( "Running the game at com_gameHz = %dHz, frametime %dms\n", com_gameHzVal, com_gameFrameLengthMS ); if ( game != NULL ) { - game->SetGameHz( com_gameHzVal, com_gameFrameTime, com_gameTicScale ); + game->SetGameHz( com_gameHzVal, com_gameFrameLengthMS, com_gameTicScale ); } } diff --git a/neo/framework/Common.h b/neo/framework/Common.h index 00315b439..13aec6b2b 100644 --- a/neo/framework/Common.h +++ b/neo/framework/Common.h @@ -80,8 +80,10 @@ extern idCVar com_dbgServerAdr; // DG: the next block is for configurable framerate extern idCVar com_gameHz; extern int com_gameHzVal; -extern int com_gameFrameTime; // round(1000.0f / gameHzVal), I guess -extern float com_gameTicScale; // com_gameHzVal/60.0f, multiply stuff assuming one tic is 16ms with this +extern int com_gameFrameLengthMS; // 1000.0f / gameHzVal, I guess +extern float com_preciseFrameLengthMS; // 1000.0f / gameHzVal +extern float com_gameTicScale; // com_gameHzVal/60.0f, multiply stuff assuming one tic is 16ms with this +extern double com_preciseFrameTimeMS; // like com_frameTime but as double: time (since start) for the current frame in milliseconds extern int time_gameFrame; // game logic time extern int time_gameDraw; // game present time diff --git a/neo/framework/UsercmdGen.h b/neo/framework/UsercmdGen.h index 09dd5e9a1..7ada85ac8 100644 --- a/neo/framework/UsercmdGen.h +++ b/neo/framework/UsercmdGen.h @@ -45,7 +45,7 @@ If you have questions concerning this license or the applicable additional terms #define USERCMD_MSEC gameLocal.gameMsec #else #define USERCMD_HZ com_gameHzVal - #define USERCMD_MSEC com_gameFrameTime + #define USERCMD_MSEC com_gameFrameLengthMS #endif // usercmd_t->button bits From 2cd4b573850b96f3c65e6d466ff18e802a7d6e1d Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Tue, 2 Jul 2024 20:10:07 +0200 Subject: [PATCH 15/34] Fix Windows build and small adjustments to Sys_MillisecondsPrecise() for the nsec part of struct timespec a simple long is enough --- neo/sys/aros/aros_main.cpp | 2 +- neo/sys/posix/posix_main.cpp | 2 +- neo/sys/win32/win_main.cpp | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/neo/sys/aros/aros_main.cpp b/neo/sys/aros/aros_main.cpp index 97da94f99..d3ca294f0 100644 --- a/neo/sys/aros/aros_main.cpp +++ b/neo/sys/aros/aros_main.cpp @@ -956,7 +956,7 @@ double Sys_MillisecondsPrecise() { clock_gettime(CLOCK_MONOTONIC, &now); long long sec = now.tv_sec - first.tv_sec; - long long nsec = now.tv_nsec - first.tv_nsec; + long nsec = now.tv_nsec - first.tv_nsec; double ret = sec * 1000.0; ret += double(nsec) * 0.000001; diff --git a/neo/sys/posix/posix_main.cpp b/neo/sys/posix/posix_main.cpp index 492bfd0f4..75e5b8db2 100644 --- a/neo/sys/posix/posix_main.cpp +++ b/neo/sys/posix/posix_main.cpp @@ -548,7 +548,7 @@ double Sys_MillisecondsPrecise() { #endif long long sec = now.tv_sec - first.tv_sec; - long long nsec = now.tv_nsec - first.tv_nsec; + long nsec = now.tv_nsec - first.tv_nsec; double ret = sec * 1000.0; ret += double(nsec) * 0.000001; diff --git a/neo/sys/win32/win_main.cpp b/neo/sys/win32/win_main.cpp index 7eb85f3b7..03b8cab08 100644 --- a/neo/sys/win32/win_main.cpp +++ b/neo/sys/win32/win_main.cpp @@ -1051,7 +1051,6 @@ static void Win_InitTime() { } double after = Sys_MillisecondsPrecise(); double callDiff = after - before; - printf( "Win_InitTime(): 1000 calls to Sys_MillisecondsPrecise() took %g usec (%g usec per call)\n", diff*1000, diff ); #ifdef D3_CpuPause // figure out how long D3_CpuPause() instructions take From e39e6cb2e1cb54e69c8b24a1825315b7aedf8c07 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Wed, 3 Jul 2024 01:27:40 +0200 Subject: [PATCH 16/34] GLimp_Init(): In fullscreen case log values from SDL instead of the ones from parms, as they can be different --- neo/sys/glimp.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/neo/sys/glimp.cpp b/neo/sys/glimp.cpp index d1c736e79..8552732d7 100644 --- a/neo/sys/glimp.cpp +++ b/neo/sys/glimp.cpp @@ -392,7 +392,11 @@ bool GLimp_Init(glimpParms_t parms) { common->Warning( "GLimp_SetScreenParms(): Can't get display mode: %s\n", SDL_GetError() ); return false; // trying other color depth etc is unlikely to help with this issue } - common->Warning("Now we have the requested resolution (%d x %d @ %d Hz)\n", parms.width, parms.height, real_mode.refresh_rate); + // TODO: in obscure cases (XWayland fake "real" fullscreen with lower than desktop resolution) + // SDL_GetWindowDisplayMode() seems to return the wrong resolution, but SDL_GetWindowSize() + // returns the correct one.. *might* make sense to switch to that, but I think it was broken + // in other cases (at least in older SDL versions) + common->Warning( "Now we have %d x %d @ %d Hz\n", real_mode.w, real_mode.h, real_mode.refresh_rate ); } } From 4c37f647ecc04615017a0d4b6d3eabc8311ca18e Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Wed, 3 Jul 2024 02:05:50 +0200 Subject: [PATCH 17/34] Support com_showFPS 2, showing also avg/min/max frametimes also made it more precise by using floats with Sys_MillisecondsPrecise() instead of ints with Sys_Milliseconds() --- neo/framework/Common.cpp | 2 +- neo/framework/Console.cpp | 43 +++++++++++++++++++++++---------------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/neo/framework/Common.cpp b/neo/framework/Common.cpp index 883ff5abd..75873ca06 100644 --- a/neo/framework/Common.cpp +++ b/neo/framework/Common.cpp @@ -94,7 +94,7 @@ idCVar com_forceGenericSIMD( "com_forceGenericSIMD", "0", CVAR_BOOL | CVAR_SYSTE idCVar com_developer( "developer", "0", CVAR_BOOL|CVAR_SYSTEM|CVAR_NOCHEAT, "developer mode" ); idCVar com_allowConsole( "com_allowConsole", "0", CVAR_BOOL | CVAR_SYSTEM | CVAR_NOCHEAT, "allow toggling console with the tilde key" ); idCVar com_speeds( "com_speeds", "0", CVAR_BOOL|CVAR_SYSTEM|CVAR_NOCHEAT, "show engine timings" ); -idCVar com_showFPS( "com_showFPS", "0", CVAR_BOOL|CVAR_SYSTEM|CVAR_ARCHIVE|CVAR_NOCHEAT, "show frames rendered per second" ); +idCVar com_showFPS( "com_showFPS", "0", CVAR_INTEGER|CVAR_SYSTEM|CVAR_ARCHIVE|CVAR_NOCHEAT, "show frames rendered per second" ); idCVar com_showMemoryUsage( "com_showMemoryUsage", "0", CVAR_BOOL|CVAR_SYSTEM|CVAR_NOCHEAT, "show total and per frame memory usage" ); idCVar com_showAsyncStats( "com_showAsyncStats", "0", CVAR_BOOL|CVAR_SYSTEM|CVAR_NOCHEAT, "show async network stats" ); idCVar com_showSoundDecoders( "com_showSoundDecoders", "0", CVAR_BOOL|CVAR_SYSTEM|CVAR_NOCHEAT, "show sound decoders" ); diff --git a/neo/framework/Console.cpp b/neo/framework/Console.cpp index 96357022b..ded49ee96 100644 --- a/neo/framework/Console.cpp +++ b/neo/framework/Console.cpp @@ -193,41 +193,50 @@ void SCR_DrawTextRightAlign( float &y, const char *text, ... ) { SCR_DrawFPS ================== */ -#define FPS_FRAMES 4 +#define FPS_FRAMES 10 float SCR_DrawFPS( float y ) { - char *s; - int w; - static int previousTimes[FPS_FRAMES]; + static float previousTimes[FPS_FRAMES]; static int index; - int i, total; - int fps; - static int previous; - int t, frameTime; + static double previous; // don't use serverTime, because that will be drifting to // correct for internet lag changes, timescales, timedemos, etc - t = Sys_Milliseconds(); - frameTime = t - previous; + double t = Sys_MillisecondsPrecise(); + float frameTime = t - previous; previous = t; previousTimes[index % FPS_FRAMES] = frameTime; index++; if ( index > FPS_FRAMES ) { // average multiple frames together to smooth changes out a bit - total = 0; - for ( i = 0 ; i < FPS_FRAMES ; i++ ) { - total += previousTimes[i]; + float total = 0.0; + float minTime = 10000; + float maxTime = 0; + for ( int i = 0 ; i < FPS_FRAMES ; i++ ) { + float pt = previousTimes[i]; + total += pt; + minTime = Min( minTime, pt ); + maxTime = Max( maxTime, pt ); } if ( !total ) { total = 1; } - fps = 10000 * FPS_FRAMES / total; - fps = (fps + 5)/10; + float fps = (1000.0f * FPS_FRAMES) / total; + int ifps = idMath::Rint( fps ); - s = va( "%ifps", fps ); - w = strlen( s ) * BIGCHAR_WIDTH; + char* s = va( "%dfps", ifps ); + int w = strlen( s ) * BIGCHAR_WIDTH; renderSystem->DrawBigStringExt( 635 - w, idMath::FtoiFast( y ) + 2, s, colorWhite, true, localConsole.charSetShader); + + if ( com_showFPS.GetInteger() > 1 ) { + y += BIGCHAR_HEIGHT + 4; + + s = va( "avg %.2fms min %.2f max %.2f", total * (1.0f / FPS_FRAMES), minTime, maxTime ); + w = strlen ( s ) * SMALLCHAR_WIDTH; + renderSystem->DrawSmallStringExt( 635 - w, idMath::FtoiFast( y ) + 2, s, colorWhite, true, localConsole.charSetShader ); + } + } return y + BIGCHAR_HEIGHT + 4; From 65e4b8d31686106b422ae62c3e31e04ce919c965 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Wed, 3 Jul 2024 06:26:44 +0200 Subject: [PATCH 18/34] Fix cursor blinking speed it uses com_ticNumber to decide when to (not) draw - that must be scaled for shorter tics (at higher FPS) --- neo/framework/Common.cpp | 2 +- neo/framework/EditField.cpp | 4 +++- neo/ui/DeviceContext.cpp | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/neo/framework/Common.cpp b/neo/framework/Common.cpp index 75873ca06..453338dad 100644 --- a/neo/framework/Common.cpp +++ b/neo/framework/Common.cpp @@ -2565,7 +2565,7 @@ typedef struct { static const int MAX_ASYNC_STATS = 1024; asyncStats_t com_asyncStats[MAX_ASYNC_STATS]; // indexed by com_ticNumber static double lastTicMsec = 0.0; -static double nextTicTargetMsec = 0.0; // when (according to Sys_Milliseconds()) the next async tic should start +static double nextTicTargetMsec = 0.0; // when (according to Sys_MillisecondsPrecise()) the next async tic should start void idCommonLocal::SingleAsyncTic( void ) { // main thread code can prevent this from happening while modifying diff --git a/neo/framework/EditField.cpp b/neo/framework/EditField.cpp index 41ef17baa..c75d66356 100644 --- a/neo/framework/EditField.cpp +++ b/neo/framework/EditField.cpp @@ -587,7 +587,9 @@ void idEditField::Draw( int x, int y, int width, bool showCursor, const idMateri return; } - if ( (int)( com_ticNumber >> 4 ) & 1 ) { + // DG: fix cursor blinking speed for >60fps + unsigned scaledTicNumber = com_ticNumber / com_gameTicScale; + if ( ( scaledTicNumber >> 4 ) & 1 ) { return; // off blink } diff --git a/neo/ui/DeviceContext.cpp b/neo/ui/DeviceContext.cpp index 6a5db6da6..824838c3c 100644 --- a/neo/ui/DeviceContext.cpp +++ b/neo/ui/DeviceContext.cpp @@ -995,7 +995,9 @@ idRegion *idDeviceContext::GetTextRegion(const char *text, float textScale, idRe } void idDeviceContext::DrawEditCursor( float x, float y, float scale ) { - if ( (int)( com_ticNumber >> 4 ) & 1 ) { + // DG: fix cursor blinking speed for >60fps + unsigned scaledTicNumber = com_ticNumber / com_gameTicScale; + if ( ( scaledTicNumber >> 4 ) & 1 ) { return; } SetFontByScale(scale); From 5c2bc003b9ef1948f7c50525dda8c57121ee8e96 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Wed, 3 Jul 2024 16:11:04 +0200 Subject: [PATCH 19/34] Fix d3xp falling apart when changing com_gameHz while game is running turns out ResetSlowTimeVars() also does things one absolutely does *not* want at that point, like resetting frame counters and times that are *always* used, no matter if slowmo is enabled or not --- neo/d3xp/Game_local.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/neo/d3xp/Game_local.cpp b/neo/d3xp/Game_local.cpp index 1f1669391..c73f80340 100644 --- a/neo/d3xp/Game_local.cpp +++ b/neo/d3xp/Game_local.cpp @@ -5070,8 +5070,7 @@ void idGameLocal::SetGameHz( float hz, float frametime, float ticScaleFactor ) if ( slowmoState == SLOWMO_STATE_OFF ) { // if slowmo is off, msec, slowmoMsec and slow/fast.msec should all be set to gameMsec - // ResetSlowTimeVars() does just that - ResetSlowTimeVars(); + msec = slowmoMsec = slow.msec = fast.msec = gameMsec; } else { // otherwise the msec values must be scaled accordingly float gameMsecScale = frametime / float(oldGameMsec); From a183987b526030e90e1088fc41b0590f1b13ba9e Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Wed, 3 Jul 2024 16:23:11 +0200 Subject: [PATCH 20/34] com_showFPS: Use all frames from last half second Smoothes things out a bit, but also makes deviations more visible in the min/max frametimes of com_showFPS 2 --- neo/framework/Console.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/neo/framework/Console.cpp b/neo/framework/Console.cpp index ded49ee96..d2f62531e 100644 --- a/neo/framework/Console.cpp +++ b/neo/framework/Console.cpp @@ -193,26 +193,29 @@ void SCR_DrawTextRightAlign( float &y, const char *text, ... ) { SCR_DrawFPS ================== */ -#define FPS_FRAMES 10 +#define FPS_FRAMES 256 float SCR_DrawFPS( float y ) { static float previousTimes[FPS_FRAMES]; - static int index; + static unsigned index; static double previous; + // DG: take all frames from last half second into account + const int maxFrames = Min(FPS_FRAMES, com_gameHzVal/2); + // don't use serverTime, because that will be drifting to // correct for internet lag changes, timescales, timedemos, etc - double t = Sys_MillisecondsPrecise(); + double t = Sys_MillisecondsPrecise(); // DG: more precision float frameTime = t - previous; previous = t; - previousTimes[index % FPS_FRAMES] = frameTime; + previousTimes[index % maxFrames] = frameTime; index++; - if ( index > FPS_FRAMES ) { + if ( index > maxFrames ) { // average multiple frames together to smooth changes out a bit float total = 0.0; float minTime = 10000; float maxTime = 0; - for ( int i = 0 ; i < FPS_FRAMES ; i++ ) { + for ( int i = 0 ; i < maxFrames ; i++ ) { float pt = previousTimes[i]; total += pt; minTime = Min( minTime, pt ); @@ -221,7 +224,7 @@ float SCR_DrawFPS( float y ) { if ( !total ) { total = 1; } - float fps = (1000.0f * FPS_FRAMES) / total; + float fps = (1000.0f * maxFrames) / total; int ifps = idMath::Rint( fps ); char* s = va( "%dfps", ifps ); @@ -232,7 +235,7 @@ float SCR_DrawFPS( float y ) { if ( com_showFPS.GetInteger() > 1 ) { y += BIGCHAR_HEIGHT + 4; - s = va( "avg %.2fms min %.2f max %.2f", total * (1.0f / FPS_FRAMES), minTime, maxTime ); + s = va( "avg %5.2fms min %5.2f max %5.2f", total * (1.0f / maxFrames), minTime, maxTime ); w = strlen ( s ) * SMALLCHAR_WIDTH; renderSystem->DrawSmallStringExt( 635 - w, idMath::FtoiFast( y ) + 2, s, colorWhite, true, localConsole.charSetShader ); } From 2141198ba3bd5108e5834a78308309fdd18dbeee Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Wed, 3 Jul 2024 22:00:11 +0200 Subject: [PATCH 21/34] Fix crane (and potentially other similar problems) in dezo2's code it was SetSteerSpeed(const float speed) { steerSpeed = speed * 60.0f / USERCMD_HZ; } I replaced it with `steerSpeed = speed * gameLocal.gameTicScale;` but gameTicScale is gameHz/60, not 60/gameHz, so it must be `steerSpeed = speed / gameLocal.gameTicScale;` Same for STOP_TIME. --- neo/d3xp/physics/Physics_AF.h | 2 +- neo/d3xp/physics/Physics_RigidBody.cpp | 4 ++-- neo/game/physics/Physics_AF.h | 2 +- neo/game/physics/Physics_RigidBody.cpp | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/neo/d3xp/physics/Physics_AF.h b/neo/d3xp/physics/Physics_AF.h index 4c2eec088..fcde99139 100644 --- a/neo/d3xp/physics/Physics_AF.h +++ b/neo/d3xp/physics/Physics_AF.h @@ -383,7 +383,7 @@ class idAFConstraint_HingeSteering : public idAFConstraint { void Setup( idAFConstraint_Hinge *cc ); void SetSteerAngle( const float degrees ) { steerAngle = degrees; } // DG: take com_gameHz into account - void SetSteerSpeed( const float speed ) { steerSpeed = speed * gameLocal.gameTicScale; } + void SetSteerSpeed( const float speed ) { steerSpeed = speed / gameLocal.gameTicScale; } void SetEpsilon( const float e ) { epsilon = e; } bool Add( idPhysics_AF *phys, float invTimeStep ); virtual void Translate( const idVec3 &translation ); diff --git a/neo/d3xp/physics/Physics_RigidBody.cpp b/neo/d3xp/physics/Physics_RigidBody.cpp index 888c91520..8ee9400f5 100644 --- a/neo/d3xp/physics/Physics_RigidBody.cpp +++ b/neo/d3xp/physics/Physics_RigidBody.cpp @@ -124,7 +124,7 @@ bool idPhysics_RigidBody::CollisionImpulse( const trace_t &collision, idVec3 &im impactInfo_t info; idEntity *ent; - const float STOP_SPEED = 10.0f * gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change + const float STOP_SPEED = 10.0f / gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change // get info from other entity involved ent = gameLocal.entities[collision.c.entityNum]; @@ -337,7 +337,7 @@ bool idPhysics_RigidBody::TestIfAtRest( void ) const { // linear velocity orthogonal to gravity direction v -= gv * gravityNormal; - const float STOP_SPEED = 10.0f * gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change + const float STOP_SPEED = 10.0f / gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change // if too much velocity orthogonal to gravity direction if ( v.Length() > STOP_SPEED ) { diff --git a/neo/game/physics/Physics_AF.h b/neo/game/physics/Physics_AF.h index 4c2eec088..fcde99139 100644 --- a/neo/game/physics/Physics_AF.h +++ b/neo/game/physics/Physics_AF.h @@ -383,7 +383,7 @@ class idAFConstraint_HingeSteering : public idAFConstraint { void Setup( idAFConstraint_Hinge *cc ); void SetSteerAngle( const float degrees ) { steerAngle = degrees; } // DG: take com_gameHz into account - void SetSteerSpeed( const float speed ) { steerSpeed = speed * gameLocal.gameTicScale; } + void SetSteerSpeed( const float speed ) { steerSpeed = speed / gameLocal.gameTicScale; } void SetEpsilon( const float e ) { epsilon = e; } bool Add( idPhysics_AF *phys, float invTimeStep ); virtual void Translate( const idVec3 &translation ); diff --git a/neo/game/physics/Physics_RigidBody.cpp b/neo/game/physics/Physics_RigidBody.cpp index 888c91520..8ee9400f5 100644 --- a/neo/game/physics/Physics_RigidBody.cpp +++ b/neo/game/physics/Physics_RigidBody.cpp @@ -124,7 +124,7 @@ bool idPhysics_RigidBody::CollisionImpulse( const trace_t &collision, idVec3 &im impactInfo_t info; idEntity *ent; - const float STOP_SPEED = 10.0f * gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change + const float STOP_SPEED = 10.0f / gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change // get info from other entity involved ent = gameLocal.entities[collision.c.entityNum]; @@ -337,7 +337,7 @@ bool idPhysics_RigidBody::TestIfAtRest( void ) const { // linear velocity orthogonal to gravity direction v -= gv * gravityNormal; - const float STOP_SPEED = 10.0f * gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change + const float STOP_SPEED = 10.0f / gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change // if too much velocity orthogonal to gravity direction if ( v.Length() > STOP_SPEED ) { From a6daf555a1e7bfbf37b1d926a080838a5bf769e7 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Sat, 6 Jul 2024 13:27:09 +0200 Subject: [PATCH 22/34] (Hopefully) fix STOP_SPEED in idPhysics_Rigidbody turns out it should be `10.0f * gameLocal.gameTicScale` instead of `10.0f / gameLocal.gameTicScale` after all, probably, at least it seems to work better according to https://github.com/dhewm/dhewm3/pull/585#issuecomment-2211710652 (Doesn't fix the issues with all framerates though, apparently 200fps work better now, but 144 or 200 don't) Moved the calculation into a function so if further adjustments are needed it's easier to do --- neo/d3xp/physics/Physics_RigidBody.cpp | 9 ++++++--- neo/game/physics/Physics_RigidBody.cpp | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/neo/d3xp/physics/Physics_RigidBody.cpp b/neo/d3xp/physics/Physics_RigidBody.cpp index 8ee9400f5..ce25d563f 100644 --- a/neo/d3xp/physics/Physics_RigidBody.cpp +++ b/neo/d3xp/physics/Physics_RigidBody.cpp @@ -41,7 +41,10 @@ END_CLASS // DG: moved STOP_SPEED into the functions using it, because it now // depends on com_gameHz which can change at runtime // const float STOP_SPEED = 10.0f; - +static inline float GetStopSpeed() +{ + return 10.0f * gameLocal.gameTicScale; +} #undef RB_TIMINGS @@ -124,7 +127,7 @@ bool idPhysics_RigidBody::CollisionImpulse( const trace_t &collision, idVec3 &im impactInfo_t info; idEntity *ent; - const float STOP_SPEED = 10.0f / gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change + const float STOP_SPEED = GetStopSpeed(); // DG: moved this here because gameLocal.gameHz can change // get info from other entity involved ent = gameLocal.entities[collision.c.entityNum]; @@ -337,7 +340,7 @@ bool idPhysics_RigidBody::TestIfAtRest( void ) const { // linear velocity orthogonal to gravity direction v -= gv * gravityNormal; - const float STOP_SPEED = 10.0f / gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change + const float STOP_SPEED = GetStopSpeed(); // DG: moved this here because gameLocal.gameHz can change // if too much velocity orthogonal to gravity direction if ( v.Length() > STOP_SPEED ) { diff --git a/neo/game/physics/Physics_RigidBody.cpp b/neo/game/physics/Physics_RigidBody.cpp index 8ee9400f5..ce25d563f 100644 --- a/neo/game/physics/Physics_RigidBody.cpp +++ b/neo/game/physics/Physics_RigidBody.cpp @@ -41,7 +41,10 @@ END_CLASS // DG: moved STOP_SPEED into the functions using it, because it now // depends on com_gameHz which can change at runtime // const float STOP_SPEED = 10.0f; - +static inline float GetStopSpeed() +{ + return 10.0f * gameLocal.gameTicScale; +} #undef RB_TIMINGS @@ -124,7 +127,7 @@ bool idPhysics_RigidBody::CollisionImpulse( const trace_t &collision, idVec3 &im impactInfo_t info; idEntity *ent; - const float STOP_SPEED = 10.0f / gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change + const float STOP_SPEED = GetStopSpeed(); // DG: moved this here because gameLocal.gameHz can change // get info from other entity involved ent = gameLocal.entities[collision.c.entityNum]; @@ -337,7 +340,7 @@ bool idPhysics_RigidBody::TestIfAtRest( void ) const { // linear velocity orthogonal to gravity direction v -= gv * gravityNormal; - const float STOP_SPEED = 10.0f / gameLocal.gameTicScale; // DG: moved this here because gameLocal.gameHz can change + const float STOP_SPEED = GetStopSpeed(); // DG: moved this here because gameLocal.gameHz can change // if too much velocity orthogonal to gravity direction if ( v.Length() > STOP_SPEED ) { From f1ebfc2993e8d398405ae6532990473414fee821 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Fri, 12 Jul 2024 18:43:43 +0200 Subject: [PATCH 23/34] Undo changes to physics code for com_gameHz in preparation of trying out TDM's fixes --- neo/d3xp/physics/Physics_AF.h | 3 +-- neo/d3xp/physics/Physics_RigidBody.cpp | 13 ++----------- neo/game/physics/Physics_AF.h | 3 +-- neo/game/physics/Physics_RigidBody.cpp | 13 ++----------- 4 files changed, 6 insertions(+), 26 deletions(-) diff --git a/neo/d3xp/physics/Physics_AF.h b/neo/d3xp/physics/Physics_AF.h index fcde99139..90b57c968 100644 --- a/neo/d3xp/physics/Physics_AF.h +++ b/neo/d3xp/physics/Physics_AF.h @@ -382,8 +382,7 @@ class idAFConstraint_HingeSteering : public idAFConstraint { idAFConstraint_HingeSteering( void ); void Setup( idAFConstraint_Hinge *cc ); void SetSteerAngle( const float degrees ) { steerAngle = degrees; } - // DG: take com_gameHz into account - void SetSteerSpeed( const float speed ) { steerSpeed = speed / gameLocal.gameTicScale; } + void SetSteerSpeed( const float speed ) { steerSpeed = speed; } void SetEpsilon( const float e ) { epsilon = e; } bool Add( idPhysics_AF *phys, float invTimeStep ); virtual void Translate( const idVec3 &translation ); diff --git a/neo/d3xp/physics/Physics_RigidBody.cpp b/neo/d3xp/physics/Physics_RigidBody.cpp index ce25d563f..838065699 100644 --- a/neo/d3xp/physics/Physics_RigidBody.cpp +++ b/neo/d3xp/physics/Physics_RigidBody.cpp @@ -38,13 +38,8 @@ If you have questions concerning this license or the applicable additional terms CLASS_DECLARATION( idPhysics_Base, idPhysics_RigidBody ) END_CLASS -// DG: moved STOP_SPEED into the functions using it, because it now -// depends on com_gameHz which can change at runtime -// const float STOP_SPEED = 10.0f; -static inline float GetStopSpeed() -{ - return 10.0f * gameLocal.gameTicScale; -} +const float STOP_SPEED = 10.0f; + #undef RB_TIMINGS @@ -127,8 +122,6 @@ bool idPhysics_RigidBody::CollisionImpulse( const trace_t &collision, idVec3 &im impactInfo_t info; idEntity *ent; - const float STOP_SPEED = GetStopSpeed(); // DG: moved this here because gameLocal.gameHz can change - // get info from other entity involved ent = gameLocal.entities[collision.c.entityNum]; ent->GetImpactInfo( self, collision.c.id, collision.c.point, &info ); @@ -340,8 +333,6 @@ bool idPhysics_RigidBody::TestIfAtRest( void ) const { // linear velocity orthogonal to gravity direction v -= gv * gravityNormal; - const float STOP_SPEED = GetStopSpeed(); // DG: moved this here because gameLocal.gameHz can change - // if too much velocity orthogonal to gravity direction if ( v.Length() > STOP_SPEED ) { return false; diff --git a/neo/game/physics/Physics_AF.h b/neo/game/physics/Physics_AF.h index fcde99139..90b57c968 100644 --- a/neo/game/physics/Physics_AF.h +++ b/neo/game/physics/Physics_AF.h @@ -382,8 +382,7 @@ class idAFConstraint_HingeSteering : public idAFConstraint { idAFConstraint_HingeSteering( void ); void Setup( idAFConstraint_Hinge *cc ); void SetSteerAngle( const float degrees ) { steerAngle = degrees; } - // DG: take com_gameHz into account - void SetSteerSpeed( const float speed ) { steerSpeed = speed / gameLocal.gameTicScale; } + void SetSteerSpeed( const float speed ) { steerSpeed = speed; } void SetEpsilon( const float e ) { epsilon = e; } bool Add( idPhysics_AF *phys, float invTimeStep ); virtual void Translate( const idVec3 &translation ); diff --git a/neo/game/physics/Physics_RigidBody.cpp b/neo/game/physics/Physics_RigidBody.cpp index ce25d563f..838065699 100644 --- a/neo/game/physics/Physics_RigidBody.cpp +++ b/neo/game/physics/Physics_RigidBody.cpp @@ -38,13 +38,8 @@ If you have questions concerning this license or the applicable additional terms CLASS_DECLARATION( idPhysics_Base, idPhysics_RigidBody ) END_CLASS -// DG: moved STOP_SPEED into the functions using it, because it now -// depends on com_gameHz which can change at runtime -// const float STOP_SPEED = 10.0f; -static inline float GetStopSpeed() -{ - return 10.0f * gameLocal.gameTicScale; -} +const float STOP_SPEED = 10.0f; + #undef RB_TIMINGS @@ -127,8 +122,6 @@ bool idPhysics_RigidBody::CollisionImpulse( const trace_t &collision, idVec3 &im impactInfo_t info; idEntity *ent; - const float STOP_SPEED = GetStopSpeed(); // DG: moved this here because gameLocal.gameHz can change - // get info from other entity involved ent = gameLocal.entities[collision.c.entityNum]; ent->GetImpactInfo( self, collision.c.id, collision.c.point, &info ); @@ -340,8 +333,6 @@ bool idPhysics_RigidBody::TestIfAtRest( void ) const { // linear velocity orthogonal to gravity direction v -= gv * gravityNormal; - const float STOP_SPEED = GetStopSpeed(); // DG: moved this here because gameLocal.gameHz can change - // if too much velocity orthogonal to gravity direction if ( v.Length() > STOP_SPEED ) { return false; From da2f017734551d68fbec019acfa1729c67e71fa5 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Fri, 12 Jul 2024 19:11:36 +0200 Subject: [PATCH 24/34] Merge some physics fixes from The Dark Mod they have *lots* of changes to physics code, partly to support mantling and movement in water, so it's not that easy to tell which are relevant for us. These are in things already identified as unstable here, so I'm hopeful that they help.. --- neo/d3xp/physics/Physics_AF.cpp | 8 ++++++-- neo/d3xp/physics/Physics_RigidBody.cpp | 13 ++++++++++--- neo/game/physics/Physics_AF.cpp | 8 ++++++-- neo/game/physics/Physics_RigidBody.cpp | 16 +++++++++++----- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/neo/d3xp/physics/Physics_AF.cpp b/neo/d3xp/physics/Physics_AF.cpp index 098a4bfc7..96cfbd5a4 100644 --- a/neo/d3xp/physics/Physics_AF.cpp +++ b/neo/d3xp/physics/Physics_AF.cpp @@ -5375,6 +5375,9 @@ void idPhysics_AF::Evolve( float timeStep ) { // make absolutely sure all contact constraints are satisfied VerifyContactConstraints(); + // DG: from TDM: make friction independent of the frametime (i.e. the time between two calls of this function) + float frictionTickMul = timeStep / MS2SEC( 16 ); // USERCMD_MSEC was 16 before introducing com_gameHz + // calculate the position of the bodies for the next physics state for ( i = 0; i < bodies.Num(); i++ ) { body = bodies[i]; @@ -5393,8 +5396,9 @@ void idPhysics_AF::Evolve( float timeStep ) { body->next->worldAxis.OrthoNormalizeSelf(); // linear and angular friction - body->next->spatialVelocity.SubVec3(0) -= body->linearFriction * body->next->spatialVelocity.SubVec3(0); - body->next->spatialVelocity.SubVec3(1) -= body->angularFriction * body->next->spatialVelocity.SubVec3(1); + // DG: from TDM: use frictionTicMul from above + body->next->spatialVelocity.SubVec3(0) -= frictionTickMul * body->linearFriction * body->next->spatialVelocity.SubVec3(0); + body->next->spatialVelocity.SubVec3(1) -= frictionTickMul * body->angularFriction * body->next->spatialVelocity.SubVec3(1); } } diff --git a/neo/d3xp/physics/Physics_RigidBody.cpp b/neo/d3xp/physics/Physics_RigidBody.cpp index 838065699..813752fa0 100644 --- a/neo/d3xp/physics/Physics_RigidBody.cpp +++ b/neo/d3xp/physics/Physics_RigidBody.cpp @@ -38,7 +38,9 @@ If you have questions concerning this license or the applicable additional terms CLASS_DECLARATION( idPhysics_Base, idPhysics_RigidBody ) END_CLASS -const float STOP_SPEED = 10.0f; +// DG: physics fixes from TDM +const float STOP_SPEED = 50.0f; // grayman #3452 (was 10) - allow less movement at end to prevent excessive jiggling +const float OLD_STOP_SPEED = 10.0f; // grayman #3452 - still needed at this value for some of the math #undef RB_TIMINGS @@ -139,8 +141,9 @@ bool idPhysics_RigidBody::CollisionImpulse( const trace_t &collision, idVec3 &im // velocity in normal direction vel = velocity * collision.c.normal; - if ( vel > -STOP_SPEED ) { - impulseNumerator = STOP_SPEED; + // DG: physics fixes from TDM (use OLD_STOP_SPEED here) + if ( vel > -OLD_STOP_SPEED ) { // grayman #3452 - was STOP_SPEED + impulseNumerator = OLD_STOP_SPEED; } else { impulseNumerator = -( 1.0f + bouncyness ) * vel; @@ -844,6 +847,10 @@ bool idPhysics_RigidBody::Evaluate( int timeStepMSec, int endTimeMSec ) { float timeStep; bool collided, cameToRest = false; + // from TDM: stgatilov: avoid doing zero steps (useless and causes division by zero) + if (timeStepMSec <= 0) + return false; + timeStep = MS2SEC( timeStepMSec ); current.lastTimeStep = timeStep; diff --git a/neo/game/physics/Physics_AF.cpp b/neo/game/physics/Physics_AF.cpp index 373048076..096509d21 100644 --- a/neo/game/physics/Physics_AF.cpp +++ b/neo/game/physics/Physics_AF.cpp @@ -5374,6 +5374,9 @@ void idPhysics_AF::Evolve( float timeStep ) { // make absolutely sure all contact constraints are satisfied VerifyContactConstraints(); + // DG: from TDM: make friction independent of the frametime (i.e. the time between two calls of this function) + float frictionTickMul = timeStep / MS2SEC( 16 ); // USERCMD_MSEC was 16 before introducing com_gameHz + // calculate the position of the bodies for the next physics state for ( i = 0; i < bodies.Num(); i++ ) { body = bodies[i]; @@ -5392,8 +5395,9 @@ void idPhysics_AF::Evolve( float timeStep ) { body->next->worldAxis.OrthoNormalizeSelf(); // linear and angular friction - body->next->spatialVelocity.SubVec3(0) -= body->linearFriction * body->next->spatialVelocity.SubVec3(0); - body->next->spatialVelocity.SubVec3(1) -= body->angularFriction * body->next->spatialVelocity.SubVec3(1); + // DG: from TDM: use frictionTicMul from above + body->next->spatialVelocity.SubVec3(0) -= frictionTickMul * body->linearFriction * body->next->spatialVelocity.SubVec3(0); + body->next->spatialVelocity.SubVec3(1) -= frictionTickMul * body->angularFriction * body->next->spatialVelocity.SubVec3(1); } } diff --git a/neo/game/physics/Physics_RigidBody.cpp b/neo/game/physics/Physics_RigidBody.cpp index 838065699..332c54b4c 100644 --- a/neo/game/physics/Physics_RigidBody.cpp +++ b/neo/game/physics/Physics_RigidBody.cpp @@ -38,7 +38,9 @@ If you have questions concerning this license or the applicable additional terms CLASS_DECLARATION( idPhysics_Base, idPhysics_RigidBody ) END_CLASS -const float STOP_SPEED = 10.0f; +// DG: physics fixes from TDM +const float STOP_SPEED = 50.0f; // grayman #3452 (was 10) - allow less movement at end to prevent excessive jiggling +const float OLD_STOP_SPEED = 10.0f; // grayman #3452 - still needed at this value for some of the math #undef RB_TIMINGS @@ -139,10 +141,10 @@ bool idPhysics_RigidBody::CollisionImpulse( const trace_t &collision, idVec3 &im // velocity in normal direction vel = velocity * collision.c.normal; - if ( vel > -STOP_SPEED ) { - impulseNumerator = STOP_SPEED; - } - else { + // DG: physics fixes from TDM (use OLD_STOP_SPEED here) + if ( vel > -OLD_STOP_SPEED ) { // grayman #3452 - was STOP_SPEED + impulseNumerator = OLD_STOP_SPEED; + } else { impulseNumerator = -( 1.0f + bouncyness ) * vel; } impulseDenominator = inverseMass + ( ( inverseWorldInertiaTensor * r.Cross( collision.c.normal ) ).Cross( r ) * collision.c.normal ); @@ -844,6 +846,10 @@ bool idPhysics_RigidBody::Evaluate( int timeStepMSec, int endTimeMSec ) { float timeStep; bool collided, cameToRest = false; + // DG: from TDM: stgatilov: avoid doing zero steps (useless and causes division by zero) + if (timeStepMSec <= 0) + return false; + timeStep = MS2SEC( timeStepMSec ); current.lastTimeStep = timeStep; From 6c17461c274c27483808ca79f87a04d89fb60666 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Sat, 13 Jul 2024 03:05:05 +0200 Subject: [PATCH 25/34] Fix the crane again --- neo/d3xp/physics/Physics_AF.cpp | 2 ++ neo/game/physics/Physics_AF.cpp | 2 ++ 2 files changed, 4 insertions(+) diff --git a/neo/d3xp/physics/Physics_AF.cpp b/neo/d3xp/physics/Physics_AF.cpp index 96cfbd5a4..3a3ab1c12 100644 --- a/neo/d3xp/physics/Physics_AF.cpp +++ b/neo/d3xp/physics/Physics_AF.cpp @@ -2233,6 +2233,8 @@ bool idAFConstraint_HingeSteering::Add( idPhysics_AF *phys, float invTimeStep ) } speed = steerAngle - angle; + // DG: steerSpeed is applied per frame, so it must be adjusted for the actual frametime + float steerSpeed = this->steerSpeed / gameLocal.gameTicScale; if ( steerSpeed != 0.0f ) { if ( speed > steerSpeed ) { speed = steerSpeed; diff --git a/neo/game/physics/Physics_AF.cpp b/neo/game/physics/Physics_AF.cpp index 096509d21..b05d2f800 100644 --- a/neo/game/physics/Physics_AF.cpp +++ b/neo/game/physics/Physics_AF.cpp @@ -2233,6 +2233,8 @@ bool idAFConstraint_HingeSteering::Add( idPhysics_AF *phys, float invTimeStep ) } speed = steerAngle - angle; + // DG: steerSpeed is applied per frame, so it must be adjusted for the actual frametime + float steerSpeed = this->steerSpeed / gameLocal.gameTicScale; if ( steerSpeed != 0.0f ) { if ( speed > steerSpeed ) { speed = steerSpeed; From 9e10c901f8527736d26bfcd4831518e94f1501d2 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Mon, 15 Jul 2024 04:00:20 +0200 Subject: [PATCH 26/34] Fix monsters walking up stairs and noclip speed many thanks to dezo2 for suggesting these fixes! I think in the StepMove() case it's more of a hack than a fix - *maybe* the correct thing would be to accumulate the deltas over multiple frames when you got blocked, or something like this? - but as long as it doesn't break anything else I don't care.. --- neo/d3xp/physics/Physics_Monster.cpp | 4 +++- neo/d3xp/physics/Physics_Player.cpp | 3 ++- neo/game/physics/Physics_Monster.cpp | 4 +++- neo/game/physics/Physics_Player.cpp | 3 ++- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/neo/d3xp/physics/Physics_Monster.cpp b/neo/d3xp/physics/Physics_Monster.cpp index 8b74d3a70..94e1273f5 100644 --- a/neo/d3xp/physics/Physics_Monster.cpp +++ b/neo/d3xp/physics/Physics_Monster.cpp @@ -186,7 +186,9 @@ monsterMoveResult_t idPhysics_Monster::StepMove( idVec3 &start, idVec3 &velocity // try to move at the stepped up position stepPos = tr.endpos; stepVel = velocity; - result2 = SlideMove( stepPos, stepVel, delta ); + // DG: this hack allows monsters to climb stairs at high framerates + idVec3 fixedDelta = delta * gameLocal.gameTicScale; + result2 = SlideMove( stepPos, stepVel, fixedDelta ); if ( result2 == MM_BLOCKED ) { start = noStepPos; velocity = noStepVel; diff --git a/neo/d3xp/physics/Physics_Player.cpp b/neo/d3xp/physics/Physics_Player.cpp index 82ac81213..4aaa866a9 100644 --- a/neo/d3xp/physics/Physics_Player.cpp +++ b/neo/d3xp/physics/Physics_Player.cpp @@ -779,7 +779,8 @@ void idPhysics_Player::NoclipMove( void ) { // friction speed = current.velocity.Length(); - if ( speed < 20.0f ) { + // DG: adjust this for framerate + if ( speed < 20.0f / gameLocal.gameTicScale ) { current.velocity = vec3_origin; } else { diff --git a/neo/game/physics/Physics_Monster.cpp b/neo/game/physics/Physics_Monster.cpp index 8b74d3a70..94e1273f5 100644 --- a/neo/game/physics/Physics_Monster.cpp +++ b/neo/game/physics/Physics_Monster.cpp @@ -186,7 +186,9 @@ monsterMoveResult_t idPhysics_Monster::StepMove( idVec3 &start, idVec3 &velocity // try to move at the stepped up position stepPos = tr.endpos; stepVel = velocity; - result2 = SlideMove( stepPos, stepVel, delta ); + // DG: this hack allows monsters to climb stairs at high framerates + idVec3 fixedDelta = delta * gameLocal.gameTicScale; + result2 = SlideMove( stepPos, stepVel, fixedDelta ); if ( result2 == MM_BLOCKED ) { start = noStepPos; velocity = noStepVel; diff --git a/neo/game/physics/Physics_Player.cpp b/neo/game/physics/Physics_Player.cpp index 82ac81213..4aaa866a9 100644 --- a/neo/game/physics/Physics_Player.cpp +++ b/neo/game/physics/Physics_Player.cpp @@ -779,7 +779,8 @@ void idPhysics_Player::NoclipMove( void ) { // friction speed = current.velocity.Length(); - if ( speed < 20.0f ) { + // DG: adjust this for framerate + if ( speed < 20.0f / gameLocal.gameTicScale ) { current.velocity = vec3_origin; } else { From 88a56598723a398f0a0316ae233379817d81e3ae Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Sat, 20 Jul 2024 02:45:16 +0200 Subject: [PATCH 27/34] Fix d3xp Grabber at high framerates at high framerates, grabbed stuff often fell down before being in front of the Grabber (where it could be thrown away again) Thanks to @dezo2 (again!) for pushing me in the right direction! --- neo/d3xp/physics/Force_Grab.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/neo/d3xp/physics/Force_Grab.cpp b/neo/d3xp/physics/Force_Grab.cpp index 7e7c4ba2c..169e01bd2 100644 --- a/neo/d3xp/physics/Force_Grab.cpp +++ b/neo/d3xp/physics/Force_Grab.cpp @@ -92,7 +92,23 @@ idForce_Grab::Init */ void idForce_Grab::Init( float damping ) { if ( damping >= 0.0f && damping < 1.0f ) { - this->damping = damping; + /* DG: in Evaluate(), the linear velocity (or actually momentum) of this->physics + * is multiplied with damping (0.5 by default) each frame. + * So how quickly the velocity is reduced per second depended on the framerate, + * and at higher framerates the grabbed item is slowed down too much and + * because of that sometimes even drops on the floor (gravity stronger than + * the force dragging it towards the player, or something like that). + * To fix that, damping must be adjusted depending on the framerate. + * The fixed code below is the result of this math (figuring out fixeddamping; + * note that here a^b means pow(a, b)): + * // we want velocity multiplied with damping 60 times per second to have the + * // same value as velocity multiplied with fixeddamping gameHz times per second + * velocity * damping^60 = velocity * fixeddamping^gameHz + * <=> damping^60 = fixeddamping^gameHz // divided by velocity + * <=> gameHz-th-root-of( damping^60 ) = fixeddamping // took gameHz-th root + * <=> fixeddamping = damping^( 60/gameHz ) // n-th-root-of(x^m) == x^(m/n) + */ + this->damping = idMath::Pow( damping, 60.0f/gameLocal.gameHz ); } } From 4024a2e145670062851ae16ecf7be18503c3c734 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Sat, 20 Jul 2024 05:53:25 +0200 Subject: [PATCH 28/34] Recalculate gameLocal.msec each frame so gameHz frames add up to 1000ms based on a patch from @dezo2, who took the general idea from D3BFG this works around the problem that for most framerates, with integer frametimes, gameHz * frametime doesn't add up to 1000ms (e.g. 120Hz * 8ms = 960ms), by making some frames a millisecond longer --- neo/d3xp/Game_local.cpp | 19 ++++++++++++++++--- neo/d3xp/Game_local.h | 4 ++-- neo/game/Game_local.cpp | 11 +++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/neo/d3xp/Game_local.cpp b/neo/d3xp/Game_local.cpp index c73f80340..4807b2dd5 100644 --- a/neo/d3xp/Game_local.cpp +++ b/neo/d3xp/Game_local.cpp @@ -2441,11 +2441,11 @@ void idGameLocal::SortActiveEntityList( void ) { idGameLocal::RunTimeGroup2 ================ */ -void idGameLocal::RunTimeGroup2() { +void idGameLocal::RunTimeGroup2( int msec_fast ) { // dezo2: add argument for high-fps support idEntity *ent; int num = 0; - fast.Increment(); + fast.Increment( msec_fast ); fast.Get( time, previousTime, msec, framenum, realClientTime ); for( ent = activeEntities.Next(); ent != NULL; ent = ent->activeNode.Next() ) { @@ -2461,6 +2461,13 @@ void idGameLocal::RunTimeGroup2() { } #endif +// DG: returns number of milliseconds for this frame, based on gameLocal.gameHz +// either 1000/gameHz or 1000/gameHz + 1, so the frametimes of gameHz frames add up to 1000ms +static int CalcMSec( long long framenum ) { + long long divisor = 100LL * gameLocal.gameHz; + return int( (framenum * 100000LL) / divisor - ((framenum-1) * 100000LL) / divisor ); +} + /* ================ idGameLocal::RunFrame @@ -2504,6 +2511,12 @@ gameReturn_t idGameLocal::RunFrame(const usercmd_t* clientCmds) { // update the game time framenum++; previousTime = time; + // dezo2/DG: for high-fps support, calculate the frametime msec every frame + // the length actually varies between 1000/gameHz and (1000/gameHz) + 1 + // so the sum of gameHz frames is 1000 (while still keeping integer frametimes) + int msec_fast = CalcMSec( framenum ); + if ( slowmoState == SLOWMO_STATE_OFF ) + msec = msec_fast; time += msec; realClientTime = time; @@ -2601,7 +2614,7 @@ gameReturn_t idGameLocal::RunFrame(const usercmd_t* clientCmds) { } #ifdef _D3XP - RunTimeGroup2(); + RunTimeGroup2( msec_fast ); // dezo2: pass msec_fast for better high-fps support #endif // remove any entities that have stopped thinking diff --git a/neo/d3xp/Game_local.h b/neo/d3xp/Game_local.h index 639a374ed..29679a04d 100644 --- a/neo/d3xp/Game_local.h +++ b/neo/d3xp/Game_local.h @@ -233,7 +233,7 @@ struct timeState_t { void Get( int& t, int& pt, int& ms, int& f, int& rct ) { t = time; pt = previousTime; ms = msec; f = framenum; rct = realClientTime; }; void Save( idSaveGame *savefile ) const { savefile->WriteInt( time ); savefile->WriteInt( previousTime ); savefile->WriteInt( msec ); savefile->WriteInt( framenum ); savefile->WriteInt( realClientTime ); } void Restore( idRestoreGame *savefile ) { savefile->ReadInt( time ); savefile->ReadInt( previousTime ); savefile->ReadInt( msec ); savefile->ReadInt( framenum ); savefile->ReadInt( realClientTime ); } - void Increment() { framenum++; previousTime = time; time += msec; realClientTime = time; }; + void Increment(int _msec) { framenum++; previousTime = time; msec = _msec; time += msec; realClientTime = time; }; // dezo2: update msec }; enum slowmoState_t { @@ -346,7 +346,7 @@ class idGameLocal : public idGame { virtual void GetBestGameType( const char* map, const char* gametype, char buf[ MAX_STRING_CHARS ] ); void ComputeSlowMsec(); - void RunTimeGroup2(); + void RunTimeGroup2( int msec_fast ); // dezo2: add argument for high-fps support void ResetSlowTimeVars(); void QuickSlowmoReset(); diff --git a/neo/game/Game_local.cpp b/neo/game/Game_local.cpp index 8eeb2b657..36e5a25ed 100644 --- a/neo/game/Game_local.cpp +++ b/neo/game/Game_local.cpp @@ -2228,6 +2228,13 @@ void idGameLocal::SortActiveEntityList( void ) { sortPushers = false; } +// DG: returns number of milliseconds for this frame, based on gameLocal.gameHz, for high-fps support +// either 1000/gameHz or 1000/gameHz + 1, so the frametimes of gameHz frames add up to 1000ms +static int CalcMSec( long long framenum ) { + long long divisor = 100LL * gameLocal.gameHz; + return int( (framenum * 100000LL) / divisor - ((framenum-1) * 100000LL) / divisor ); +} + /* ================ idGameLocal::RunFrame @@ -2264,6 +2271,10 @@ gameReturn_t idGameLocal::RunFrame( const usercmd_t *clientCmds ) { // update the game time framenum++; previousTime = time; + // dezo2/DG: for high-fps support, calculate the frametime msec every frame + // the length actually varies between 1000/gameHz and (1000/gameHz) + 1 + // so the sum of gameHz frames is 1000 (while still keeping integer frametimes) + msec = CalcMSec( framenum ); time += msec; realClientTime = time; From 4dc3ec72920c7d3c021b82312c02b7b289afb0ed Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Thu, 25 Jul 2024 05:38:14 +0200 Subject: [PATCH 29/34] Pressure user to not set com_gameHzVal to >250Hz it's still possible, but a warning is shown and the setting in the menu only goes up to 250 now. Apart from physics getting wonkier the higher the framerate is, >250Hz breaks slow motion effects in RoE (d3xp), because it divides frametimes by four, and as the frametimes are integers, for >250Hz they're <= 3, so divided by four that's 0, which isn't good. Furthermore, I moved that setting to the Video settings, so it's closer to related settings like VSync and display refreshrate. While at it, I made sure that `com_showFPS 2` can be configured in the menu as well (it still assumed it's a bool). Last but not least I fixed a misleading variable name in Win_InitTime() --- neo/framework/Common.cpp | 9 ++++++++- neo/framework/Dhewm3SettingsMenu.cpp | 11 +++++++++-- neo/sys/win32/win_main.cpp | 4 ++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/neo/framework/Common.cpp b/neo/framework/Common.cpp index 453338dad..1a2a921d7 100644 --- a/neo/framework/Common.cpp +++ b/neo/framework/Common.cpp @@ -110,7 +110,7 @@ idCVar com_dbgServerAdr( "com_dbgServerAdr", "localhost", CVAR_SYSTEM | CVAR_ARC idCVar com_product_lang_ext( "com_product_lang_ext", "1", CVAR_INTEGER | CVAR_SYSTEM | CVAR_ARCHIVE, "Extension to use when creating language files." ); // DG: the next block is for configurable framerate -#define COM_GAMEHZ_DESCR "Frames per second the game should run at - keep in mind that Vertical Sync (or a too slow computer) may slow it down, and that running below this configured framerate can cause problems!" +#define COM_GAMEHZ_DESCR "Frames per second the game should run at. You really shouldn't set a higher value than 250! Also keep in mind that Vertical Sync (or a too slow computer) may slow it down, and that running below this configured framerate can cause problems!" idCVar com_gameHz( "com_gameHz", "60", CVAR_INTEGER | CVAR_ARCHIVE | CVAR_SYSTEM, COM_GAMEHZ_DESCR, 10, 480 ); // TODO: make it float? make it default to 62.5? // the next three values will be set based on com_gameHz int com_gameHzVal = 60; @@ -3383,9 +3383,16 @@ void idCommonLocal::UpdateGameHz() { com_gameHz.ClearModified(); com_gameHzVal = com_gameHz.GetInteger(); + + if ( com_gameHzVal > 250 ) { + Warning( "Setting com_gameHz to values above 250 is known to cause bugs! You generally shouldn't do this, it's only for testing/debugging purposes!" ); + } + com_preciseFrameLengthMS = 1000.0f / com_gameHzVal; // only rounding up the frame time a little bit, so for 144hz (6.94ms) it becomes 7ms, // but for 60Hz (16.6667ms) it remains 16ms, like before + // FIXME: still do this, now that the game code increases the frametime by 1ms for some frames + // so com_gameHz frames add up to 1000ms? com_gameFrameLengthMS = com_preciseFrameLengthMS + 0.1f; // TODO: idMath::Rint ? or always round down? com_gameTicScale = com_gameHzVal / 60.0f; // TODO: or / 62.5? diff --git a/neo/framework/Dhewm3SettingsMenu.cpp b/neo/framework/Dhewm3SettingsMenu.cpp index 4b8e6fdb9..4cb340bc0 100644 --- a/neo/framework/Dhewm3SettingsMenu.cpp +++ b/neo/framework/Dhewm3SettingsMenu.cpp @@ -1608,6 +1608,8 @@ struct VidMode { static CVarOption videoOptionsImmediately[] = { CVarOption( "Options that take effect immediately" ), + CVarOption( "com_gameHz", "Framerate", OT_INT, 30, 250 ), + CVarOption( "r_swapInterval", []( idCVar& cvar ) { int curVsync = idMath::ClampInt( -1, 1, r_swapInterval.GetInteger() ); if ( curVsync == -1 ) { @@ -2229,9 +2231,14 @@ static CVarOption gameOptions[] = { CVarOption( "ui_autoReload", "Auto Weapon Reload", OT_BOOL ), CVarOption( "ui_autoSwitch", "Auto Weapon Switch", OT_BOOL ), CVarOption( "Visual" ), - CVarOption( "com_gameHz", "Framerate", OT_INT, 10, 500 ), CVarOption( "g_showHud", "Show HUD", OT_BOOL ), - CVarOption( "com_showFPS", "Show Framerate (FPS)", OT_BOOL ), + CVarOption( "com_showFPS", []( idCVar& cvar ) { + int curSel = idMath::ClampInt( 0, 2, cvar.GetInteger() ); + if ( ImGui::Combo( "Show Framerate (FPS)", &curSel, "No\0Yes (simple)\0Yes, also frametimes\0" ) ) { + cvar.SetInteger( curSel ); + } + AddTooltip( "com_showFPS" ); + } ), CVarOption( "ui_showGun", "Show Gun Model", OT_BOOL ), CVarOption( "g_decals", "Show Decals", OT_BOOL ), CVarOption( "g_bloodEffects", "Show Blood and Gibs", OT_BOOL ), diff --git a/neo/sys/win32/win_main.cpp b/neo/sys/win32/win_main.cpp index 03b8cab08..43f625bb5 100644 --- a/neo/sys/win32/win_main.cpp +++ b/neo/sys/win32/win_main.cpp @@ -1063,8 +1063,8 @@ static void Win_InitTime() { double diff = after - before; double onePauseIterTime = diff / 1000000; if ( onePauseIterTime > 0.00000001 ) { - double loopsPer10usec = 0.005 / onePauseIterTime; - pauseLoopsPer5usec = loopsPer10usec; + double loopsPer5usec = 0.005 / onePauseIterTime; + pauseLoopsPer5usec = loopsPer5usec; printf( "Win_InitTime(): A call to Sys_MillisecondsPrecise() takes about %g nsec; 1mio pause loops took %g ms => pauseLoopsPer5usec = %zd\n", callDiff*1000.0, diff, pauseLoopsPer5usec ); if ( pauseLoopsPer5usec == 0 ) From 25ea927eea0a922ee1ed0a34089a432774cad031 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Thu, 25 Jul 2024 05:55:48 +0200 Subject: [PATCH 30/34] Apply @dezo2's fix for monsters sliding too fast see https://github.com/dhewm/dhewm3/pull/585#issuecomment-2247085256 --- neo/d3xp/physics/Physics_Monster.cpp | 4 +++- neo/game/physics/Physics_Monster.cpp | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/neo/d3xp/physics/Physics_Monster.cpp b/neo/d3xp/physics/Physics_Monster.cpp index 94e1273f5..2cb93e2bc 100644 --- a/neo/d3xp/physics/Physics_Monster.cpp +++ b/neo/d3xp/physics/Physics_Monster.cpp @@ -187,7 +187,9 @@ monsterMoveResult_t idPhysics_Monster::StepMove( idVec3 &start, idVec3 &velocity stepPos = tr.endpos; stepVel = velocity; // DG: this hack allows monsters to climb stairs at high framerates - idVec3 fixedDelta = delta * gameLocal.gameTicScale; + // the tr.fraction < 1.0 check should prevent monsters from sliding faster when not + // actually on stairs (when climbing stairs it's apparently 1.0) + idVec3 fixedDelta = delta * ( tr.fraction < 1.0f ? 1.0f : gameLocal.gameTicScale ); result2 = SlideMove( stepPos, stepVel, fixedDelta ); if ( result2 == MM_BLOCKED ) { start = noStepPos; diff --git a/neo/game/physics/Physics_Monster.cpp b/neo/game/physics/Physics_Monster.cpp index 94e1273f5..2cb93e2bc 100644 --- a/neo/game/physics/Physics_Monster.cpp +++ b/neo/game/physics/Physics_Monster.cpp @@ -187,7 +187,9 @@ monsterMoveResult_t idPhysics_Monster::StepMove( idVec3 &start, idVec3 &velocity stepPos = tr.endpos; stepVel = velocity; // DG: this hack allows monsters to climb stairs at high framerates - idVec3 fixedDelta = delta * gameLocal.gameTicScale; + // the tr.fraction < 1.0 check should prevent monsters from sliding faster when not + // actually on stairs (when climbing stairs it's apparently 1.0) + idVec3 fixedDelta = delta * ( tr.fraction < 1.0f ? 1.0f : gameLocal.gameTicScale ); result2 = SlideMove( stepPos, stepVel, fixedDelta ); if ( result2 == MM_BLOCKED ) { start = noStepPos; From c89e2618b013e8b5f13e532e3aeb0bb57379d0d8 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Thu, 25 Jul 2024 06:02:27 +0200 Subject: [PATCH 31/34] Apply @dezo2's ragdoll fix to prevent them from jumping so much at high framerates --- neo/d3xp/Actor.cpp | 6 ++++++ neo/game/Actor.cpp | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/neo/d3xp/Actor.cpp b/neo/d3xp/Actor.cpp index 39cec391a..6ff8e44a8 100644 --- a/neo/d3xp/Actor.cpp +++ b/neo/d3xp/Actor.cpp @@ -1668,6 +1668,9 @@ bool idActor::StartRagdoll( void ) { return true; } + // dezo2, modified entity mass (must be here, mass is zeroed in StartFromCurrentPose) + float mass_mod = GetPhysics()->GetMass() * idMath::Pow( gameLocal.gameTicScale, 2.0f ); + // disable the monster bounding box GetPhysics()->DisableClip(); @@ -1702,6 +1705,9 @@ bool idActor::StartRagdoll( void ) { RemoveAttachments(); + // dezo2, use modified entity mass to reduce ragdoll movement at high FPS + GetPhysics()->SetMass( mass_mod ); + return true; } diff --git a/neo/game/Actor.cpp b/neo/game/Actor.cpp index 15061a290..4651c91d1 100644 --- a/neo/game/Actor.cpp +++ b/neo/game/Actor.cpp @@ -1620,6 +1620,9 @@ bool idActor::StartRagdoll( void ) { return true; } + // dezo2, modified entity mass (must be here, mass is zeroed in StartFromCurrentPose) + float mass_mod = GetPhysics()->GetMass() * idMath::Pow( gameLocal.gameTicScale, 2.0f ); + // disable the monster bounding box GetPhysics()->DisableClip(); @@ -1654,6 +1657,9 @@ bool idActor::StartRagdoll( void ) { RemoveAttachments(); + // dezo2, use modified entity mass to reduce ragdoll movement at high FPS + GetPhysics()->SetMass( mass_mod ); + return true; } From 8451dd071bfdafb9dd93502576f473ad8110765e Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Thu, 25 Jul 2024 23:41:18 +0200 Subject: [PATCH 32/34] Bump Version to 1.6.0pre The changes in this branch breaks the Game API, so it won't be in a 1.5.x release. --- neo/framework/Licensee.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neo/framework/Licensee.h b/neo/framework/Licensee.h index 53bb43745..8cbaf50fa 100644 --- a/neo/framework/Licensee.h +++ b/neo/framework/Licensee.h @@ -41,12 +41,12 @@ If you have questions concerning this license or the applicable additional terms #define GAME_NAME "dhewm 3" // appears in errors #endif -#define ENGINE_VERSION "dhewm3 1.5.5pre" // printed in console, used for window title +#define ENGINE_VERSION "dhewm3 1.6.0pre" // printed in console, used for window title #ifdef ID_REPRODUCIBLE_BUILD // for reproducible builds we hardcode values that would otherwise come from __DATE__ and __TIME__ // NOTE: remember to update esp. the date for (pre-) releases and RCs and the like - #define ID__DATE__ "Aug 15 2024" + #define ID__DATE__ "Aug 20 2024" #define ID__TIME__ "13:37:42" #else // not reproducible build, use __DATE__ and __TIME__ macros From b158f243b79f317814938eee2f9aa73f86baf210 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Fri, 16 Aug 2024 04:00:03 +0200 Subject: [PATCH 33/34] Fix oxygen consumption code (idPlayer::airTics) for > 60Hz it already mostly worked, but picking up oxygen bottles gave too little oxygen at >60fps and changing com_gameHz while being "outside" would screw up the remaining oxygen and the implementation was needlessly invasive anyway. I reverted most changes, turned idPlayer::airTics into a float (so they are now virtual 60Hz tics instead of actual number of tics) and adjusted the code that increases or decreases the airTics (idPlayer::UpdateAir()) to scale the values added/subtracted accordingly. Apart from that it's only a few changes to accomodate the fact that it's a float now. --- neo/d3xp/Player.cpp | 40 +++++++++++++++++++--------------------- neo/d3xp/Player.h | 3 ++- neo/game/Player.cpp | 35 ++++++++++++++++++----------------- neo/game/Player.h | 3 ++- 4 files changed, 41 insertions(+), 40 deletions(-) diff --git a/neo/d3xp/Player.cpp b/neo/d3xp/Player.cpp index f716e00d5..5fb5dc012 100644 --- a/neo/d3xp/Player.cpp +++ b/neo/d3xp/Player.cpp @@ -1599,8 +1599,8 @@ void idPlayer::Init( void ) { // stamina always initialized to maximum stamina = pm_stamina.GetFloat(); - // air always initialized to maximum too - DG: pm_airTics must be scaled for actual FPS from com_gameHz - airTics = idMath::Rint( pm_airTics.GetFloat() * gameLocal.gameTicScale ); + // air always initialized to maximum too + airTics = pm_airTics.GetFloat(); airless = false; gibDeath = false; @@ -2152,7 +2152,7 @@ void idPlayer::Save( idSaveGame *savefile ) const { savefile->WriteInt( numProjectileHits ); savefile->WriteBool( airless ); - savefile->WriteInt( airTics ); + savefile->WriteInt( (int)airTics ); savefile->WriteInt( lastAirDamage ); savefile->WriteBool( gibDeath ); @@ -2420,7 +2420,11 @@ void idPlayer::Restore( idRestoreGame *savefile ) { savefile->ReadInt( numProjectileHits ); savefile->ReadBool( airless ); - savefile->ReadInt( airTics ); + // DG: I made made airTics float for high-fps (where we have fractions of 60Hz tics), + // but for saving ints should still suffice (and this preserves savegame compat) + int iairTics; + savefile->ReadInt( iairTics ); + airTics = iairTics; savefile->ReadInt( lastAirDamage ); savefile->ReadBool( gibDeath ); @@ -3516,14 +3520,12 @@ bool idPlayer::Give( const char *statname, const char *value ) { } } else if ( !idStr::Icmp( statname, "air" ) ) { - // DG: pm_airTics must be scaled for actual FPS from com_gameHz - int airTicsCnt = idMath::Rint( pm_airTics.GetFloat() * gameLocal.gameTicScale ); - if ( airTics >= airTicsCnt ) { + if ( airTics >= pm_airTics.GetFloat() ) { // DG: airTics are floats now for high-fps support return false; } - airTics += atoi( value ) / 100.0 * pm_airTics.GetInteger(); - if ( airTics > airTicsCnt ) { - airTics = airTicsCnt; + airTics += atoi( value ) / 100.0 * pm_airTics.GetFloat(); + if ( airTics > pm_airTics.GetFloat() ) { + airTics = pm_airTics.GetFloat(); } #ifdef _D3XP } else if ( !idStr::Icmp( statname, "enviroTime" ) ) { @@ -6078,9 +6080,6 @@ void idPlayer::UpdateAir( void ) { return; } - // DG: pm_airTics must be scaled for actual FPS from com_gameHz - int airTicsCnt = idMath::Rint( pm_airTics.GetFloat() * gameLocal.gameTicScale ); - // see if the player is connected to the info_vacuum bool newAirless = false; @@ -6115,7 +6114,8 @@ void idPlayer::UpdateAir( void ) { hud->HandleNamedEvent( "noAir" ); } } - airTics--; + // DG: was airTics--, but airTics assume 60Hz tics and we support other ticrates now (com_gameHz) + airTics -= 1.0f / gameLocal.gameTicScale; if ( airTics < 0 ) { airTics = 0; // check for damage @@ -6135,16 +6135,16 @@ void idPlayer::UpdateAir( void ) { hud->HandleNamedEvent( "Air" ); } } - airTics+=2; // regain twice as fast as lose - if ( airTics > airTicsCnt ) { - airTics = airTicsCnt; + airTics += 2.0f / gameLocal.gameTicScale; // regain twice as fast as lose - DG: scale for com_gameHz + if ( airTics > pm_airTics.GetFloat() ) { + airTics = pm_airTics.GetFloat(); } } airless = newAirless; if ( hud ) { - hud->SetStateInt( "player_air", 100 * airTics / airTicsCnt ); + hud->SetStateInt( "player_air", 100 * (airTics / pm_airTics.GetFloat()) ); } } @@ -7662,9 +7662,7 @@ bool idPlayer::CanGive( const char *statname, const char *value ) { return true; } else if ( !idStr::Icmp( statname, "air" ) ) { - // DG: pm_airTics must be scaled for actual FPS from com_gameHz - int airTicsCnt = idMath::Rint( pm_airTics.GetFloat() * gameLocal.gameTicScale ); - if ( airTics >= airTicsCnt ) { + if ( airTics >= pm_airTics.GetFloat() ) { return false; } return true; diff --git a/neo/d3xp/Player.h b/neo/d3xp/Player.h index aadf85506..b0a9bbe33 100644 --- a/neo/d3xp/Player.h +++ b/neo/d3xp/Player.h @@ -659,7 +659,8 @@ class idPlayer : public idActor { int numProjectileHits; // number of hits on mobs bool airless; - int airTics; // set to pm_airTics at start, drops in vacuum + // DG: Note: airTics are tics at 60Hz, so no real tics (unless com_gameHz happens to be 60) + float airTics; // set to pm_airTics at start, drops in vacuum int lastAirDamage; bool gibDeath; diff --git a/neo/game/Player.cpp b/neo/game/Player.cpp index d5656b301..63ddce162 100644 --- a/neo/game/Player.cpp +++ b/neo/game/Player.cpp @@ -1294,8 +1294,8 @@ void idPlayer::Init( void ) { // stamina always initialized to maximum stamina = pm_stamina.GetFloat(); - // air always initialized to maximum too - DG: pm_airTics must be scaled for actual FPS for com_gameHz - airTics = idMath::Rint( pm_airTics.GetFloat() * gameLocal.gameTicScale ); + // air always initialized to maximum too + airTics = pm_airTics.GetFloat(); airless = false; gibDeath = false; @@ -1747,7 +1747,7 @@ void idPlayer::Save( idSaveGame *savefile ) const { savefile->WriteInt( numProjectileHits ); savefile->WriteBool( airless ); - savefile->WriteInt( airTics ); + savefile->WriteInt( (int)airTics ); savefile->WriteInt( lastAirDamage ); savefile->WriteBool( gibDeath ); @@ -1979,7 +1979,11 @@ void idPlayer::Restore( idRestoreGame *savefile ) { savefile->ReadInt( numProjectileHits ); savefile->ReadBool( airless ); - savefile->ReadInt( airTics ); + // DG: I made made airTics float for high-fps (where we have fractions of 60Hz tics), + // but for saving ints should still suffice (and this preserves savegame compat) + int iairTics; + savefile->ReadInt( iairTics ); + airTics = iairTics; savefile->ReadInt( lastAirDamage ); savefile->ReadBool( gibDeath ); @@ -2903,14 +2907,12 @@ bool idPlayer::Give( const char *statname, const char *value ) { } } else if ( !idStr::Icmp( statname, "air" ) ) { - // DG: pm_airTics must be scaled for actual FPS from com_gameHz - int airTicsCnt = idMath::Rint( pm_airTics.GetFloat() * gameLocal.gameTicScale ); - if ( airTics >= airTicsCnt ) { + if ( airTics >= pm_airTics.GetFloat() ) { // DG: airTics are floats now for high-fps support return false; } - airTics += atoi( value ) / 100.0 * pm_airTics.GetInteger(); - if ( airTics > airTicsCnt ) { - airTics = airTicsCnt; + airTics += atoi( value ) / 100.0 * pm_airTics.GetFloat(); + if ( airTics > pm_airTics.GetFloat() ) { + airTics = pm_airTics.GetFloat(); } } else { return inventory.Give( this, spawnArgs, statname, value, &idealWeapon, true ); @@ -5071,8 +5073,6 @@ void idPlayer::UpdateAir( void ) { // see if the player is connected to the info_vacuum bool newAirless = false; - // DG: pm_airTics must be scaled for actual FPS from com_gameHz - int airTicsCnt = idMath::Rint( pm_airTics.GetFloat() * gameLocal.gameTicScale ); if ( gameLocal.vacuumAreaNum != -1 ) { int num = GetNumPVSAreas(); @@ -5099,7 +5099,8 @@ void idPlayer::UpdateAir( void ) { hud->HandleNamedEvent( "noAir" ); } } - airTics--; + // DG: was airTics--, but airTics assume 60Hz tics and we support other ticrates now (com_gameHz) + airTics -= 1.0f / gameLocal.gameTicScale; if ( airTics < 0 ) { airTics = 0; // check for damage @@ -5119,16 +5120,16 @@ void idPlayer::UpdateAir( void ) { hud->HandleNamedEvent( "Air" ); } } - airTics+=2; // regain twice as fast as lose - if ( airTics > airTicsCnt ) { - airTics = airTicsCnt; + airTics += 2.0f / gameLocal.gameTicScale; // regain twice as fast as lose - DG: scale for com_gameHz + if ( airTics > pm_airTics.GetFloat() ) { + airTics = pm_airTics.GetFloat(); } } airless = newAirless; if ( hud ) { - hud->SetStateInt( "player_air", 100 * airTics / airTicsCnt ); + hud->SetStateInt( "player_air", 100 * (airTics / pm_airTics.GetFloat()) ); } } diff --git a/neo/game/Player.h b/neo/game/Player.h index b411a0c0e..46b8ef331 100644 --- a/neo/game/Player.h +++ b/neo/game/Player.h @@ -564,7 +564,8 @@ class idPlayer : public idActor { int numProjectileHits; // number of hits on mobs bool airless; - int airTics; // set to pm_airTics at start, drops in vacuum + // DG: Note: airTics are tics at 60Hz, so no real tics (unless com_gameHz happens to be 60) + float airTics; // set to pm_airTics at start, drops in vacuum int lastAirDamage; bool gibDeath; From 7ac7047ddb1e32c0ee49ebfee6bf4ac3ed6b48f3 Mon Sep 17 00:00:00 2001 From: Daniel Gibson Date: Fri, 16 Aug 2024 05:38:23 +0200 Subject: [PATCH 34/34] Make Sys_Milliseconds() inline it only returns Sys_MillisecondsPrecise() casted to unsigned int anyway. --- neo/sys/sys_public.h | 10 ++++++---- neo/sys/threads.cpp | 9 --------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/neo/sys/sys_public.h b/neo/sys/sys_public.h index 11e3fb152..e76d9ee9b 100644 --- a/neo/sys/sys_public.h +++ b/neo/sys/sys_public.h @@ -159,13 +159,15 @@ void Sys_DebugVPrintf( const char *fmt, va_list arg ); // NOTE: due to SDL_TIMESLICE this is very bad portability karma, and should be completely removed void Sys_Sleep( int msec ); -// Sys_Milliseconds should only be used for profiling purposes, -// any game related timing information should come from event timestamps -unsigned int Sys_Milliseconds( void ); - // like Sys_Milliseconds(), but with higher precision double Sys_MillisecondsPrecise( void ); +// Sys_Milliseconds should only be used for profiling purposes, +// any game related timing information should come from event timestamps +ID_INLINE unsigned int Sys_Milliseconds( void ) { + return (unsigned int)Sys_MillisecondsPrecise(); +} + // sleep until Sys_MillisecondsPrecise() returns >= targetTimeMS // aims for about 0.01ms precision (but might busy wait for the last 1.5ms or so) void Sys_SleepUntilPrecise( double targetTimeMS ); diff --git a/neo/sys/threads.cpp b/neo/sys/threads.cpp index dc030b93c..4b242e43f 100644 --- a/neo/sys/threads.cpp +++ b/neo/sys/threads.cpp @@ -68,15 +68,6 @@ void Sys_Sleep(int msec) { SDL_Delay(msec); } -/* -================ -Sys_Milliseconds -================ -*/ -unsigned int Sys_Milliseconds() { - return (unsigned int)Sys_MillisecondsPrecise(); -} - /* ================== Sys_InitThreads