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/d3xp/Game_local.cpp b/neo/d3xp/Game_local.cpp index 51c113ba2..4807b2dd5 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; } /* @@ -626,6 +630,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 +1540,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 +1571,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 ) { @@ -2425,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() ) { @@ -2445,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 @@ -2488,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; @@ -2585,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 @@ -4913,10 +4942,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 +4957,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 ); @@ -5043,3 +5072,24 @@ 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; + int oldGameMsec = gameMsec; + gameMsec = frametime; + gameTicScale = ticScaleFactor; + + if ( slowmoState == SLOWMO_STATE_OFF ) { + // if slowmo is off, msec, slowmoMsec and slow/fast.msec should all be set to gameMsec + msec = slowmoMsec = slow.msec = fast.msec = gameMsec; + } 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 f9a969352..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 { @@ -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; @@ -341,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(); @@ -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))); @@ -512,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 diff --git a/neo/d3xp/Player.cpp b/neo/d3xp/Player.cpp index 837447979..5fb5dc012 100644 --- a/neo/d3xp/Player.cpp +++ b/neo/d3xp/Player.cpp @@ -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 ); @@ -3231,9 +3235,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 @@ -3511,12 +3520,12 @@ bool idPlayer::Give( const char *statname, const char *value ) { } } else if ( !idStr::Icmp( statname, "air" ) ) { - if ( airTics >= pm_airTics.GetInteger() ) { + 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 > pm_airTics.GetInteger() ) { - airTics = pm_airTics.GetInteger(); + 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" ) ) { @@ -6105,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 @@ -6125,16 +6135,16 @@ void idPlayer::UpdateAir( void ) { hud->HandleNamedEvent( "Air" ); } } - airTics+=2; // regain twice as fast as lose - if ( airTics > pm_airTics.GetInteger() ) { - airTics = pm_airTics.GetInteger(); + 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 / pm_airTics.GetInteger() ); + hud->SetStateInt( "player_air", 100 * (airTics / pm_airTics.GetFloat()) ); } } @@ -7107,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 ); } } @@ -7648,7 +7662,7 @@ bool idPlayer::CanGive( const char *statname, const char *value ) { return true; } else if ( !idStr::Icmp( statname, "air" ) ) { - if ( airTics >= pm_airTics.GetInteger() ) { + 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/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/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/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 ); } } diff --git a/neo/d3xp/physics/Physics_AF.cpp b/neo/d3xp/physics/Physics_AF.cpp index 098a4bfc7..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; @@ -5375,6 +5377,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 +5398,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_Monster.cpp b/neo/d3xp/physics/Physics_Monster.cpp index 8b74d3a70..2cb93e2bc 100644 --- a/neo/d3xp/physics/Physics_Monster.cpp +++ b/neo/d3xp/physics/Physics_Monster.cpp @@ -186,7 +186,11 @@ 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 + // 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; 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/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/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/framework/Common.cpp b/neo/framework/Common.cpp index 95d11fb20..1a2a921d7 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" ); @@ -109,13 +109,24 @@ 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. 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; +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 for the current frame in milliseconds +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) @@ -219,6 +230,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 @@ -241,12 +254,30 @@ 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_preciseFrameTimeMS += ticDiff * com_preciseFrameLengthMS; + com_frameTime = idMath::Rint( com_preciseFrameTimeMS ); + lastTicNum = ticNum; +} + /* ================== idCommonLocal::idCommonLocal @@ -270,7 +301,8 @@ idCommonLocal::idCommonLocal( void ) { config_compressor = NULL; #endif - async_timer = 0; + memset( &asyncThread, 0, sizeof(asyncThread) ); + runAsyncThread = false; } /* @@ -2439,12 +2471,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_UpdateFrameTime(); // DG: put updating com_frameTime into a function idAsyncNetwork::RunFrame(); @@ -2490,7 +2527,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(); } @@ -2527,8 +2564,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 double lastTicMsec = 0.0; +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 @@ -2571,18 +2608,19 @@ idCommonLocal::Async ================= */ void idCommonLocal::Async( void ) { - int msec = Sys_Milliseconds(); + 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 + com_preciseFrameLengthMS; return; } - int ticMsec = USERCMD_MSEC; + float ticMsec = com_preciseFrameLengthMS; // the number of msec per tic can be varies with the timescale cvar float timescale = com_timescale.GetFloat(); @@ -2595,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; } } @@ -2604,6 +2642,7 @@ void idCommonLocal::Async( void ) { SingleAsyncTic(); lastTicMsec += ticMsec; } + nextTicTargetMsec = lastTicMsec + ticMsec; } /* @@ -2734,6 +2773,7 @@ void idCommonLocal::LoadGameDLL( void ) { // initialize the game object if ( game != NULL ) { + game->SetGameHz( com_gameHzVal, com_gameFrameLengthMS, com_gameTicScale ); // DG: make sure it knows the ticrate game->Init(); } } @@ -2799,21 +2839,30 @@ 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() + // 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) - if (now >= tick) - return 1; + self->Async(); - return tick - now; + // idSessionLocal::Frame() waits for TRIGGER_EVENT_ONE + // => this syncs the main thread with the async thread + Sys_TriggerEvent(TRIGGER_EVENT_ONE); + + Sys_SleepUntilPrecise( nextTicTargetMsec ); + } + return 0; } #ifdef _WIN32 @@ -3081,10 +3130,8 @@ void idCommonLocal::Init( int argc, char **argv ) { Sys_Error( "Error during initialization" ); } - async_timer = SDL_AddTimer(USERCMD_MSEC, AsyncTimer, NULL); - - if (!async_timer) - Sys_Error("Error while starting the async timer: %s", SDL_GetError()); + runAsyncThread = true; + Sys_CreateThread( AsyncThread, this, asyncThread, "AsyncThread" ); } @@ -3094,9 +3141,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(); @@ -3215,6 +3263,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 +3378,32 @@ void idCommonLocal::ShutdownGame( bool reloading ) { fileSystem->Shutdown( reloading ); } +// DG: for configurable framerate +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? + + Printf( "Running the game at com_gameHz = %dHz, frametime %dms\n", com_gameHzVal, com_gameFrameLengthMS ); + + if ( game != NULL ) { + game->SetGameHz( com_gameHzVal, com_gameFrameLengthMS, 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..13aec6b2b 100644 --- a/neo/framework/Common.h +++ b/neo/framework/Common.h @@ -77,6 +77,14 @@ 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_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 extern int time_frontend; // renderer frontend time diff --git a/neo/framework/Console.cpp b/neo/framework/Console.cpp index 96357022b..d2f62531e 100644 --- a/neo/framework/Console.cpp +++ b/neo/framework/Console.cpp @@ -193,41 +193,53 @@ void SCR_DrawTextRightAlign( float &y, const char *text, ... ) { SCR_DrawFPS ================== */ -#define FPS_FRAMES 4 +#define FPS_FRAMES 256 float SCR_DrawFPS( float y ) { - char *s; - int w; - static int previousTimes[FPS_FRAMES]; - static int index; - int i, total; - int fps; - static int previous; - int t, frameTime; + static float previousTimes[FPS_FRAMES]; + 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 - t = Sys_Milliseconds(); - frameTime = t - previous; + 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 - 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 < maxFrames ; 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 * maxFrames) / 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 %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 ); + } + } return y + BIGCHAR_HEIGHT + 4; 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/framework/Dhewm3SettingsMenu.cpp b/neo/framework/Dhewm3SettingsMenu.cpp index 66d654a48..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 ) { @@ -1758,9 +1760,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 +1918,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 @@ -2228,7 +2232,13 @@ static CVarOption gameOptions[] = { CVarOption( "ui_autoSwitch", "Auto Weapon Switch", OT_BOOL ), CVarOption( "Visual" ), 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/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/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/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 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/framework/UsercmdGen.h b/neo/framework/UsercmdGen.h index c9e9ecaee..7ada85ac8 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_gameFrameLengthMS +#endif // usercmd_t->button bits const int BUTTON_ATTACK = BIT(0); 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; } diff --git a/neo/game/Game_local.cpp b/neo/game/Game_local.cpp index 4ca98d6f8..36e5a25ed 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; } /* @@ -2224,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 @@ -2260,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; @@ -4443,3 +4458,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/Player.cpp b/neo/game/Player.cpp index ec9b3ff8b..63ddce162 100644 --- a/neo/game/Player.cpp +++ b/neo/game/Player.cpp @@ -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,12 +2907,12 @@ bool idPlayer::Give( const char *statname, const char *value ) { } } else if ( !idStr::Icmp( statname, "air" ) ) { - if ( airTics >= pm_airTics.GetInteger() ) { + 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 > pm_airTics.GetInteger() ) { - airTics = pm_airTics.GetInteger(); + 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 ); @@ -5095,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 @@ -5115,16 +5120,16 @@ void idPlayer::UpdateAir( void ) { hud->HandleNamedEvent( "Air" ); } } - airTics+=2; // regain twice as fast as lose - if ( airTics > pm_airTics.GetInteger() ) { - airTics = pm_airTics.GetInteger(); + 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 / pm_airTics.GetInteger() ); + hud->SetStateInt( "player_air", 100 * (airTics / pm_airTics.GetFloat()) ); } } @@ -6010,8 +6015,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/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; 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() ); 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/game/physics/Physics_AF.cpp b/neo/game/physics/Physics_AF.cpp index 373048076..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; @@ -5374,6 +5376,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 +5397,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_Monster.cpp b/neo/game/physics/Physics_Monster.cpp index 8b74d3a70..2cb93e2bc 100644 --- a/neo/game/physics/Physics_Monster.cpp +++ b/neo/game/physics/Physics_Monster.cpp @@ -186,7 +186,11 @@ 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 + // 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; 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 { 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; 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 dedf5cc01..a8455eb42 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,89 @@ 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 ) { + 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()" + // (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 + 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 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 ); 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/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 diff --git a/neo/sys/aros/aros_main.cpp b/neo/sys/aros/aros_main.cpp index afad94f7f..d3ca294f0 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 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/glimp.cpp b/neo/sys/glimp.cpp index 1f0bad2e4..8552732d7 100644 --- a/neo/sys/glimp.cpp +++ b/neo/sys/glimp.cpp @@ -359,58 +359,44 @@ 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); + // 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 ); } } @@ -630,19 +616,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 +656,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 +689,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 +720,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 +748,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/posix/posix_main.cpp b/neo/sys/posix/posix_main.cpp index ee975d323..75e5b8db2 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 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/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) diff --git a/neo/sys/sys_public.h b/neo/sys/sys_public.h index dbff09dfd..e76d9ee9b 100644 --- a/neo/sys/sys_public.h +++ b/neo/sys/sys_public.h @@ -159,9 +159,18 @@ 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 ); +// 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 -unsigned int Sys_Milliseconds( void ); +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 ); // 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..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 SDL_GetTicks(); -} - /* ================== Sys_InitThreads diff --git a/neo/sys/win32/win_main.cpp b/neo/sys/win32/win_main.cpp index 8b3a1c814..43f625bb5 100644 --- a/neo/sys/win32/win_main.cpp +++ b/neo/sys/win32/win_main.cpp @@ -1001,6 +1001,130 @@ 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; + +#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 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 ) + 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 +1145,8 @@ int main(int argc, char *argv[]) { InitializeCriticalSection( &printfCritSect ); + Win_InitTime(); + #ifdef ID_DEDICATED MSG msg; #else 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);