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 a822cab1c..0f920b7dd 100644 --- a/neo/d3xp/Game_local.cpp +++ b/neo/d3xp/Game_local.cpp @@ -203,6 +203,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; } /* @@ -629,6 +633,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 @@ -1538,7 +1543,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 ); @@ -1561,12 +1574,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 ) { @@ -2449,12 +2465,12 @@ void idGameLocal::RunTimeGroup2( int msec_fast ) { // dezo2/DG: added argument f #endif // dezo2/DG: returns number of milliseconds for this frame, either 1000/gameHz or 1000/gameHz + 1, -// (16 or 17) so the frametimes of gameHz frames add up to 1000ms. +// (e.g. 16 or 17 for 60Hz) so the frametimes of gameHz frames add up to 1000ms. // This prevents animations or videos from running slightly to slow or running out of sync // with audio in cutscenes (those only worked right at 62.5fps with exactly 16ms frames, // but now even without vsync we're enforcing 16.666ms frames for proper 60fps) static int CalcMSec( long long framenum ) { - long long divisor = 100LL * USERCMD_HZ; + long long divisor = 100LL * gameLocal.gameHz; return int( (framenum * 100000LL) / divisor - ((framenum-1) * 100000LL) / divisor ); } @@ -4932,10 +4948,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 { @@ -4947,10 +4963,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 ); @@ -5062,3 +5078,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 a99f5fb86..ac17ea982 100644 --- a/neo/d3xp/Game_local.h +++ b/neo/d3xp/Game_local.h @@ -306,6 +306,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; @@ -403,6 +408,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))); @@ -518,7 +526,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 2b93cc845..cded54d5a 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 ); @@ -3238,6 +3242,9 @@ void idPlayer::DrawHUD( idUserInterface *_hud ) { } else { cursor->SetStateString( "grabbercursor", "0" ); cursor->SetStateString( "combatcursor", "1" ); + cursor->SetStateBool("scaleto43", true); // dezo2, scaled + cursor->StateChanged(gameLocal.realClientTime); // dezo2, set state + } // DG: update scaleto43 state if necessary if ( cursor->GetStateBool( "scaleto43" ) != wantScaleTo43 ) { @@ -3520,12 +3527,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" ) ) { @@ -6114,7 +6121,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 @@ -6134,16 +6142,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()) ); } } @@ -7116,8 +7124,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 ); } } @@ -7657,7 +7669,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 8d702917f..429ff7424 100644 --- a/neo/framework/Common.cpp +++ b/neo/framework/Common.cpp @@ -114,11 +114,15 @@ 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." ); -// in the high-fps branch, the next three values will be set based on com_gameHz -// here (in the old 60fps-only code) they're const and just to reduce difference to the other branch -//const int com_gameHzVal = 60; -//const int com_gameFrameLengthMS = 16; // length of one frame in msec, 1000 / com_gameHz -const double com_preciseFrameLengthMS = 1000.0 / 60.0; +// 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? + +// the next 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 +double com_preciseFrameLengthMS = 1000.0 / 60.0; // 1000 / com_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 @@ -231,6 +235,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 @@ -276,9 +282,9 @@ void Com_UpdateTicNumber() { // usually numTics should be 1, except if timeDiff > 16.6667 (skipped a frame?) // should be `1 + timediff / com_preciseFrameLengthMS` // <=> 1 + timediff / (1000.0 / USERCMD_HZ) // 1000ms in one second - // <=> 1 + timediff * (USERCMD_HZ / 1000.0) // USERCMD_HZ = 60; - // <=> 1 + timediff * 0.06; - int numTics = 1 + timeDiff * 0.06; + // <=> 1 + timediff * (USERCMD_HZ / 1000.0) // USERCMD_HZ = com_gameHzVal; + // <=> 1 + timediff * com_gameHzVal * 0.001 + int numTics = 1 + timeDiff * double(com_gameHzVal) * 0.001; com_ticNumber += numTics; nextTicTime += numTics * com_preciseFrameLengthMS; @@ -289,6 +295,11 @@ void Com_UpdateTicNumber() { // DG: updates com_frameTime based on the current tic number (which is also updated if necessary) // and com_preciseFrameLengthMS 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; Com_UpdateTicNumber(); @@ -2509,6 +2520,11 @@ 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? @@ -2546,8 +2562,8 @@ void idCommonLocal::Frame( void ) { // set idLib frame number for frame based memory dumps idLib::frameNumber = com_frameNumber; - if ( GLimp_GetSwapInterval() != 0 && fabsf(60.0f - GLimp_GetDisplayRefresh()) < 1.0f ) { - // if we're using vsync and the display is running at about 60Hz, start next tic + if ( GLimp_GetSwapInterval() != 0 && fabsf(com_gameHzVal - GLimp_GetDisplayRefresh()) < 1.0f ) { + // if we're using vsync and the display is running at about the target framerate, start next tic // immediately so our internal tic time and vsync don't drift apart double now = Sys_MillisecondsPrecise(); if(nextTicTime > now) { @@ -2777,6 +2793,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(); } } @@ -3276,6 +3293,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(); @@ -3389,6 +3408,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.0 / double(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 ? + + 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 0d0881c4d..e1dc33fc6 100644 --- a/neo/framework/Common.h +++ b/neo/framework/Common.h @@ -76,6 +76,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; // round(1000.0f / gameHzVal), I guess +extern double com_preciseFrameLengthMS; // 1000.0 / 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/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 d5afcfbff..cc34c1b58 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 ) { @@ -1774,9 +1776,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 ) { @@ -1958,10 +1962,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 @@ -2333,7 +2337,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 ddcff1db4..723f34124 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/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 213b9978f..c71ac21d3 100644 --- a/neo/game/Game_local.cpp +++ b/neo/game/Game_local.cpp @@ -170,6 +170,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; } /* @@ -2229,12 +2233,12 @@ void idGameLocal::SortActiveEntityList( void ) { } // dezo2/DG: returns number of milliseconds for this frame, either 1000/gameHz or 1000/gameHz + 1, -// (16 or 17) so the frametimes of gameHz frames add up to 1000ms. +// (e.g. 16 or 17 for 60Hz) so the frametimes of gameHz frames add up to 1000ms. // This prevents animations or videos from running slightly to slow or running out of sync // with audio in cutscenes (those only worked right at 62.5fps with exactly 16ms frames, // but now even without vsync we're enforcing 16.666ms frames for proper 60fps) static int CalcMSec( long long framenum ) { - long long divisor = 100LL * USERCMD_HZ; + long long divisor = 100LL * gameLocal.gameHz; return int( (framenum * 100000LL) / divisor - ((framenum-1) * 100000LL) / divisor ); } @@ -4458,3 +4462,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 1b47fcf08..df9ab1ca8 100644 --- a/neo/game/Game_local.h +++ b/neo/game/Game_local.h @@ -273,10 +273,11 @@ class idGameLocal : public idGame { int previousTime; // time in msec of last frame int time; // in msec - // DG: msec used to be static const, making it mutable - // so it can be set to 16 or 17 in different frames - // so 60 frames add up to 1000ms - int 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 @@ -342,6 +343,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 7914093e1..449efe485 100644 --- a/neo/renderer/RenderSystem_init.cpp +++ b/neo/renderer/RenderSystem_init.cpp @@ -60,7 +60,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 6293462db..ce7d20f94 100644 --- a/neo/renderer/tr_local.h +++ b/neo/renderer/tr_local.h @@ -1088,8 +1088,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, @@ -1134,7 +1135,7 @@ int GLimp_GetSwapInterval(); bool GLimp_SetWindowResizable( bool enableResizable ); void GLimp_UpdateWindowSize(); -glimpParms_t GLimp_GetCurState(); +glimpParms_t GLimp_GetCurState( bool checkConsistency = true ); float GLimp_GetDisplayRefresh(); /* diff --git a/neo/sound/snd_local.h b/neo/sound/snd_local.h index 808faaaf2..01c98e2d7 100644 --- a/neo/sound/snd_local.h +++ b/neo/sound/snd_local.h @@ -100,7 +100,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/glimp.cpp b/neo/sys/glimp.cpp index aef8b0d3d..e5ab2e227 100644 --- a/neo/sys/glimp.cpp +++ b/neo/sys/glimp.cpp @@ -517,58 +517,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 != selectedDisplay) { common->Warning("Window's display index is %d, but we wanted %d!\n", dIdx, selectedDisplay); } - /* 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) - { - 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) + // 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 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_DisplayMode real_mode = {}; + if ( SDL_GetWindowDisplayMode( window, &real_mode ) != 0 ) { - 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); - + common->Warning( "GLimp_SetScreenParms(): Can't get display mode: %s\n", SDL_GetError() ); return false; // trying other color depth etc is unlikely to help with this issue } - common->Warning("Now we have the requested resolution (%d x %d)\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 ); } } #endif // SDL2 @@ -583,14 +569,21 @@ bool GLimp_Init(glimpParms_t parms) { SetSDLIcon(); // for SDL2 this must be done after creating the window + float displayHz = GLimp_GetDisplayRefresh(); + + idStr hzStr; + if ( displayHz != 0.0f ) { + hzStr = idStr::Format(" @ %.2fHz", displayHz); + } + // TODO: also check for fullscreen-desktop? glConfig.isFullscreen = (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0; const char* fsStr = glConfig.isFullscreen ? "fullscreen " : ""; if ( (int)glConfig.winWidth != glConfig.vidWidth ) { - common->Printf( "Got a HighDPI %swindow with physical resolution %d x %d and virtual resolution %g x %g\n", - fsStr, glConfig.vidWidth, glConfig.vidHeight, glConfig.winWidth, glConfig.winHeight ); + common->Printf( "Got a HighDPI %swindow with physical resolution %d x %d and virtual resolution %g x %g %s\n", + fsStr, glConfig.vidWidth, glConfig.vidHeight, glConfig.winWidth, glConfig.winHeight, hzStr.c_str() ); } else { - common->Printf( "Got a %swindow with resolution %g x %g\n", fsStr, glConfig.winWidth, glConfig.winHeight ); + common->Printf( "Got a %swindow with resolution %g x %g %s\n", fsStr, glConfig.winWidth, glConfig.winHeight, hzStr.c_str() ); } #else // SDL1.2 window creation SDL_WM_SetCaption(ENGINE_VERSION, ENGINE_VERSION); @@ -804,19 +797,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) } @@ -908,8 +900,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 ) { @@ -929,7 +933,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) ) @@ -1012,7 +1022,7 @@ float GLimp_GetDisplayRefresh() // 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 = {}; @@ -1074,8 +1084,10 @@ glimpParms_t GLimp_GetCurState() ret.displayHz = roundf(GLimp_GetDisplayRefresh()); } - assert( ret.width == glConfig.winWidth && ret.height == glConfig.winHeight ); - assert( ret.fullScreen == glConfig.isFullscreen ); + if ( checkConsistency ) { + assert( ret.width == glConfig.winWidth && ret.height == glConfig.winHeight ); + assert( ret.fullScreen == glConfig.isFullscreen ); + } #else assert( 0 && "Don't use GLimp_GetCurState() with SDL1.2 !" ); diff --git a/neo/sys/stub/stub_gl.cpp b/neo/sys/stub/stub_gl.cpp index 3eac18fa2..3a3deffb6 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() {}; @@ -401,7 +401,7 @@ bool GLimp_SetSwapInterval( int swapInterval ) { return false; } int GLimp_GetSwapInterval() { return 0; } 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; } float GLimp_GetDisplayRefresh() { return 0.0f; } #ifdef _MSC_VER diff --git a/neo/sys/win32/win_main.cpp b/neo/sys/win32/win_main.cpp index 1d12fa1bd..1dd341853 100644 --- a/neo/sys/win32/win_main.cpp +++ b/neo/sys/win32/win_main.cpp @@ -1082,8 +1082,8 @@ static void Win_InitTime() { double diff = after - before; double onePauseIterTime = diff / 1000000; if ( onePauseIterTime > 0.00000001 ) { - double loopsPer10usec = 0.005 / onePauseIterTime; - pauseLoopsPer5usec = loopsPer10usec; + double loopsPer5usec = 0.005 / onePauseIterTime; + pauseLoopsPer5usec = loopsPer5usec; printf( "Win_InitTime(): A call to Sys_MillisecondsPrecise() takes about %g nsec; 1mio pause loops took %g ms => pauseLoopsPer5usec = %zd\n", callDiff*1000.0, diff, pauseLoopsPer5usec ); if ( pauseLoopsPer5usec == 0 ) diff --git a/neo/ui/DeviceContext.cpp b/neo/ui/DeviceContext.cpp index 757780322..48b2cf5fd 100644 --- a/neo/ui/DeviceContext.cpp +++ b/neo/ui/DeviceContext.cpp @@ -1232,7 +1232,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);