From 62bc5a5770ff86687a7656a54bc3544832fca1d2 Mon Sep 17 00:00:00 2001 From: WofWca Date: Tue, 14 Jul 2026 23:31:44 +0400 Subject: [PATCH] perf: set players' baseline state (delta compr) This saves ~0.34% (~1KiB), of traffic and demo size for a ~3-minute match with 4 players, by sending less data when a player or a dead body is seen by another player for the first time in a while. This will increase the size of the initial connection message (`svc_gamestate`, which now needs to send the entities for which we set the baseline state), but it pays off in the long run. How to test this change: 1. `./quake +set cg_autoRecordDemo 1 +set jorunal 1` to record a jorunal file and a demo. 2. Play a game. 3. Build the mod with this patch and install the mod. 4. `./quake +set cg_autoRecordDemo 1 +set jorunal 2` to play back the journal file and record another demo given the very same inputs. Then compare the demo sizes. One can also utilize `cl_shownet 3`. Below is an example of how the "delta from baseline" entries look on vanilla Quake. First two player entries, then two body queue entries: ``` 50: baseline: 2 51: #2 pos.trBase[0]:1024 pos.trBase[1]:1000 pos.trBase[2]:24 apos.trBase[1]:27 eType:1 torsoAnim:11 legsAnim:22 groundEntityNum:1022 pos.trType:1 eFlags:4 weapon:2 clientNum:2 angles[1]:90 apos.trType:1 solid:4200463 (418 bits) 70: baseline: 5 71: #5 pos.trBase[0]:216 pos.trBase[1]:1328 pos.trBase[2]:24 eType:1 torsoAnim:11 legsAnim:22 groundEntityNum:1022 pos.trType:1 eFlags:4 weapon:2 clientNum:5 angles[1]:360 apos.trType:1 solid:4200463 (398 bits) 23: baseline: 65 24: #65 pos.trBase[0]:678 pos.trBase[1]:1446 pos.trBase[2]:24 apos.trBase[1]:135 eType:1 torsoAnim:3 legsAnim:3 groundEntityNum:1022 eFlags:1 clientNum:3 (278 bits) 46: baseline: 66 47: #66 pos.trBase[0]:894 pos.trBase[1]:832 pos.trBase[2]:-15 apos.trBase[1]:225 eType:1 torsoAnim:5 legsAnim:5 groundEntityNum:1022 eFlags:1 clientNum:1 (279 bits) ``` As you can see, basically every field is transmitted, including e.g. `eType`, which is almost always `ET_PLAYER` for players and dead bodies. Now let's see how this patch makes it look: ``` 33: baseline: 3 35: #3 pos.trBase[0]:1024 pos.trBase[1]:1000 pos.trBase[2]:24 apos.trBase[1]:90 eventParm:10 legsAnim:22 angles[1]:90 (255 bits) 84: baseline: 5 86: #5 pos.trBase[0]:1028 pos.trBase[1]:1094 pos.trBase[2]:24 apos.trBase[1]:107 angles2[1]:7 legsAnim:22 eFlags:4 angles[1]:90 (284 bits) 23: baseline: 64 24: #64 pos.trBase[0]:698 pos.trBase[1]:1460 pos.trBase[2]:24 apos.trBase[1]:135 (149 bits) 22: baseline: 65 24: #65 pos.trBase[0]:471 pos.trBase[1]:807 pos.trBase[2]:-15 apos.trBase[1]:315 torsoAnim:3 legsAnim:3 (208 bits) ``` This is much shorter. Sometimes we even don't transmit anything but coordinates and angles. I have tested this on Quake3e and ioq3 engine. The latter still has a bug in `SV_CreateBaseline` which never sends entity numbered 0. But this doesn't cause anything game-breaking, only that this optimization will not be applied to entity 0. --- code/game/g_client.c | 130 +++++++++++++++++++++++++++++++++++++++++++ code/game/g_local.h | 6 ++ code/game/g_main.c | 6 ++ 3 files changed, 142 insertions(+) diff --git a/code/game/g_client.c b/code/game/g_client.c index 664c176d..26dd798c 100644 --- a/code/game/g_client.c +++ b/code/game/g_client.c @@ -276,6 +276,34 @@ void InitBodyQue (void) { ent = G_Spawn(); ent->classname = "bodyque"; ent->neverFree = qtrue; + +#ifndef NO_OPTIMIZED_BASELINE_ENTITY_STATE + // Set likely baseline state, for better delta compression. + // Same as in `ClientsSetBaselineState`. + ent->s.eType = ET_PLAYER; + ent->s.eFlags = EF_DEAD; + // Likely but not guaranteed. + ent->s.groundEntityNum = ENTITYNUM_WORLD; + // This is true 1 times out of 3. + ent->s.torsoAnim = BOTH_DEAD1; + ent->s.legsAnim = BOTH_DEAD1; + + // But ensure that this doesn't affect gameplay, + // i.e. not solid and not shootable and stuff. See also `GibEntity`. + ent->r.contents = 0; + + // FIXME: maybe also set `r.currentOrigin`, + // as in `ClientsSetBaselineState`. + + trap_LinkEntity( ent ); + // It's probably fine not to unlink + // because these entities don't affect gameplay, + // and will eventually get linked anyway as `CopyToBodyQue` gets called. + + // Will be cleared by `CopyToBodyQue`. + ent->r.svFlags = SVF_NOCLIENT; +#endif + level.bodyQue[i] = ent; } } @@ -564,6 +592,88 @@ team_t PickTeam( int ignoreClientNum ) { } +#ifndef NO_OPTIMIZED_BASELINE_ENTITY_STATE +/* +=========== +ClientSetBaselineState + +Called by `G_InitGame`, i.e. right before `SV_CreateBaseline`, +when there are no clients (`ClientConnect` has not been called). +Should be called after `G_LocateSpawnSpots`, otherwise might have no effect. + +Sets likely baseline state, for better network delta compression. +So that every time a player appears in another player's sight, +we only have to send the fields that are different from what we set here. + +One can test the effect of this with `cl_shownet 3` (or `-3`), +paying attention to the "baseline" entries, +specifically to how many fields get sent. +============ +*/ +void ClientsSetBaselineState( ) { + int i; + qboolean warningPrinted = qfalse; + + for ( i = 0 ; i < level.maxclients ; i++ ) { + gentity_t *ent = &g_entities[ i ]; + + // Note that the more fields are non-zero + // the bigger the initial `svc_gamestate` message will be. + + // See `BG_PlayerStateToEntityState` + ent->s.number = i; + ent->s.clientNum = i; + ent->s.eType = ET_PLAYER; + ent->s.pos.trType = TR_INTERPOLATE; + ent->s.apos.trType = TR_INTERPOLATE; + ent->s.torsoAnim = TORSO_STAND; + ent->s.legsAnim = LEGS_RUN; + ent->s.groundEntityNum = ENTITYNUM_WORLD; + ent->s.weapon = WP_MACHINEGUN; + + // `SV_LinkEntity` sets `s.solid` based on these values. + ent->r.contents = CONTENTS_BODY; + VectorCopy( playerMins, ent->r.mins ); + VectorCopy( playerMaxs, ent->r.maxs ); + + // It seems that in `SV_LinkEntity` there is a check + // for whether the entity is "inside the world". + // If not then it leaves `r.linked == qfalse`. + // So we ensure that the origin is indeed inside the world. + // + // Note, however, that this seems to be unnecessary, + // at least with the vanilla engine and vanilla maps. + // Setting origin to even 99999999999+ still results + // in the entity getting linked. + if ( level.spawnSpots[0] ) { + VectorCopy( level.spawnSpots[0]->s.origin, ent->r.currentOrigin ); + } + + // Linking is required because `SV_CreateBaseline` + // will just skip this entity otherwise, at least on the vanilla engine. + trap_LinkEntity( ent ); + if ( !ent->r.linked && !warningPrinted ) { + G_Printf( S_COLOR_YELLOW "WARNING: ClientsSetBaselineState did not actually link the entity, delta compression will be less efficient\n" ); + warningPrinted = qtrue; + } + + // Now that `s.solid` has been set (see comment above), + // we can mark the entity as non-solid, + // to minimize effect on gameplay + // (but we're still gonna unlink the entity soon). + ent->r.contents = 0; + + // We've linked the entity, but let's not send it to clients. + ent->r.svFlags = SVF_NOCLIENT; + } + + // Let's unlink the entities ASAP, + // to ensure that they don't affect gameplay. + level.mustUnlinkAllClientEnts = qtrue; +} +#endif + + /* =========== ClientUserInfoChanged @@ -744,6 +854,26 @@ const char *ClientConnect( int clientNum, qboolean firstTime, qboolean isBot ) { char userinfo[MAX_INFO_STRING]; gentity_t *ent; qboolean isAdmin; + int i; + +#ifndef NO_OPTIMIZED_BASELINE_ENTITY_STATE + // Unfortunately we don't have a better place to run this. + // The idea is to run right this once after `SV_CreateBaseline`, + // + // Technically it doesn't seem entirely necessary to unlink, + // as long as we simply have `r.contents = 0` and `SVF_NOCLIENT`, + // but let's still do things properly. + if ( level.mustUnlinkAllClientEnts ) { + G_Printf( "client entities' baseline state set, now unlinking them all\n" ); + for ( i = 0 ; i < level.maxclients ; i++ ) { + gentity_t *ent = &g_entities[ i ]; + trap_UnlinkEntity( ent ); + // This was also set in `ClientsSetBaselineState`, so clear it. + ent->r.svFlags &= ~SVF_NOCLIENT; + } + level.mustUnlinkAllClientEnts = qfalse; + } +#endif if ( clientNum >= level.maxclients ) { return "Bad connection slot."; diff --git a/code/game/g_local.h b/code/game/g_local.h index d3a3b047..1e32470f 100644 --- a/code/game/g_local.h +++ b/code/game/g_local.h @@ -376,6 +376,9 @@ typedef struct { qboolean restarted; // waiting for a map_restart to fire +#ifndef NO_OPTIMIZED_BASELINE_ENTITY_STATE + qboolean mustUnlinkAllClientEnts; // only qtrue during game init +#endif int numConnectedClients; int numNonSpectatorClients; // includes connecting clients int numPlayingClients; // connected, non-spectators @@ -658,6 +661,9 @@ void G_BroadcastServerCommand( int ignoreClient, const char *command ); // // g_client.c // +#ifndef NO_OPTIMIZED_BASELINE_ENTITY_STATE +void ClientsSetBaselineState(); +#endif const char *ClientConnect( int clientNum, qboolean firstTime, qboolean isBot ); qboolean ClientUserinfoChanged( int clientNum ); void ClientDisconnect( int clientNum ); diff --git a/code/game/g_main.c b/code/game/g_main.c index 34d9cf85..a65134c9 100644 --- a/code/game/g_main.c +++ b/code/game/g_main.c @@ -551,6 +551,12 @@ static void G_InitGame( int levelTime, int randomSeed, int restart ) { G_LocateSpawnSpots(); +#ifndef NO_OPTIMIZED_BASELINE_ENTITY_STATE + G_Printf( "setting %i client entities' baseline state for better delta compression\n", + level.maxclients ); + ClientsSetBaselineState(); +#endif + G_Printf ("-----------------------------------\n"); if( g_gametype.integer == GT_SINGLE_PLAYER || trap_Cvar_VariableIntegerValue( "com_buildScript" ) ) {