Remove PVS data from demo format, derive during playback

Stop recording currentOrigin, absmin, absmax, linked per entity
(44 bytes per moving entity per frame). During playback, G_RunFrame
computes currentOrigin via BG_EvaluateTrajectory and calls
trap_LinkEntity to register in BSP for PVS.

svFlags still recorded (1-bit change flag + 4 bytes when changed).
Entity linking moved from SVD_ReadFrame (server, no trajectory eval)
to G_RunFrame demo mode (game module, has BG_EvaluateTrajectory).

Scan all MAX_GENTITIES during playback since recorded entities may
have indices above level.num_entities (game-module-spawned count).

Demo format bumped to v3. Significant file size reduction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
serge_shubin 2026-03-24 05:00:21 +08:00
parent 5a98ef02cf
commit fe628a2cc4
2 changed files with 34 additions and 58 deletions

View file

@ -1792,6 +1792,27 @@ int start, end;
} }
} }
// evaluate trajectories and link all recorded entities for PVS.
// SVD_ReadFrame injected entityState_t but didn't link because
// the server doesn't have BG_EvaluateTrajectory.
// scan up to MAX_GENTITIES because recorded entities may exceed
// level.num_entities (which only counts game-module-spawned entities).
for ( i = 0; i < MAX_GENTITIES; i++ ) {
ent = &g_entities[i];
if ( ent->s.eType == 0 && !ent->inuse ) {
continue;
}
// skip spectator slot
if ( ent->client && ent->client->pers.connected == CON_CONNECTED
&& ent->client->sess.sessionTeam == TEAM_SPECTATOR ) {
continue;
}
// compute currentOrigin from trajectory
BG_EvaluateTrajectory( &ent->s.pos, level.time, ent->r.currentOrigin );
ent->r.linked = qtrue;
trap_LinkEntity( ent );
}
// update rankings so follow mode can cycle through players // update rankings so follow mode can cycle through players
CalculateRanks(); CalculateRanks();

View file

@ -51,7 +51,7 @@ snapshot pipeline delivers them to a spectator client.
// //
#define SVDEMO_MAGIC (('S') | ('V' << 8) | ('D' << 16) | ('M' << 24)) #define SVDEMO_MAGIC (('S') | ('V' << 8) | ('D' << 16) | ('M' << 24))
#define SVDEMO_VERSION 2 // v2: optional LZ4 compression #define SVDEMO_VERSION 3 // v3: removed PVS data, svFlags only
#define SVDEMO_MAX_MAPNAME 64 #define SVDEMO_MAX_MAPNAME 64
// header flags // header flags
@ -61,14 +61,10 @@ snapshot pipeline delivers them to a spectator client.
// State // State
// --------------------------------------------------------------- // ---------------------------------------------------------------
// per-entity data stored for delta compression (entityState + PVS fields) // per-entity data stored for delta compression
typedef struct { typedef struct {
entityState_t es; entityState_t es;
int svFlags; int svFlags;
qboolean linked;
vec3_t currentOrigin;
vec3_t absmin;
vec3_t absmax;
qboolean active; // was this entity present last frame? qboolean active; // was this entity present last frame?
} svdEntityState_t; } svdEntityState_t;
@ -275,38 +271,17 @@ static void SVD_WriteFrame( fileHandle_t f ) {
} }
MSG_WriteDeltaEntity( &msg, from, &ent->s, qtrue ); MSG_WriteDeltaEntity( &msg, from, &ent->s, qtrue );
// write PVS fields only if changed from previous frame // write svFlags only if changed (rarely changes)
{ if ( ent->r.svFlags != demo.prevEntities[i].svFlags ) {
qboolean pvsChanged = !demo.prevEntities[i].active MSG_WriteBits( &msg, 1, 1 );
|| ent->r.svFlags != demo.prevEntities[i].svFlags MSG_WriteLong( &msg, ent->r.svFlags );
|| ent->r.linked != demo.prevEntities[i].linked } else {
|| !VectorCompare( ent->r.currentOrigin, demo.prevEntities[i].currentOrigin ) MSG_WriteBits( &msg, 0, 1 );
|| !VectorCompare( ent->r.absmin, demo.prevEntities[i].absmin )
|| !VectorCompare( ent->r.absmax, demo.prevEntities[i].absmax );
MSG_WriteBits( &msg, pvsChanged, 1 );
if ( pvsChanged ) {
MSG_WriteLong( &msg, ent->r.svFlags );
MSG_WriteLong( &msg, ent->r.linked );
MSG_WriteFloat( &msg, ent->r.currentOrigin[0] );
MSG_WriteFloat( &msg, ent->r.currentOrigin[1] );
MSG_WriteFloat( &msg, ent->r.currentOrigin[2] );
MSG_WriteFloat( &msg, ent->r.absmin[0] );
MSG_WriteFloat( &msg, ent->r.absmin[1] );
MSG_WriteFloat( &msg, ent->r.absmin[2] );
MSG_WriteFloat( &msg, ent->r.absmax[0] );
MSG_WriteFloat( &msg, ent->r.absmax[1] );
MSG_WriteFloat( &msg, ent->r.absmax[2] );
}
} }
// update prev state // update prev state
demo.prevEntities[i].es = ent->s; demo.prevEntities[i].es = ent->s;
demo.prevEntities[i].svFlags = ent->r.svFlags; demo.prevEntities[i].svFlags = ent->r.svFlags;
demo.prevEntities[i].linked = ent->r.linked;
VectorCopy( ent->r.currentOrigin, demo.prevEntities[i].currentOrigin );
VectorCopy( ent->r.absmin, demo.prevEntities[i].absmin );
VectorCopy( ent->r.absmax, demo.prevEntities[i].absmax );
demo.prevEntities[i].active = qtrue; demo.prevEntities[i].active = qtrue;
} else if ( demo.prevEntities[i].active ) { } else if ( demo.prevEntities[i].active ) {
// entity was removed — write a remove marker // entity was removed — write a remove marker
@ -734,40 +709,20 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) {
continue; continue;
} }
// read PVS fields // read svFlags
{ if ( MSG_ReadBits( &msg, 1 ) ) {
qboolean pvsChanged = MSG_ReadBits( &msg, 1 ); demo.playPrevEntities[entNum].svFlags = MSG_ReadLong( &msg );
if ( pvsChanged ) {
demo.playPrevEntities[entNum].svFlags = MSG_ReadLong( &msg );
demo.playPrevEntities[entNum].linked = MSG_ReadLong( &msg );
demo.playPrevEntities[entNum].currentOrigin[0] = MSG_ReadFloat( &msg );
demo.playPrevEntities[entNum].currentOrigin[1] = MSG_ReadFloat( &msg );
demo.playPrevEntities[entNum].currentOrigin[2] = MSG_ReadFloat( &msg );
demo.playPrevEntities[entNum].absmin[0] = MSG_ReadFloat( &msg );
demo.playPrevEntities[entNum].absmin[1] = MSG_ReadFloat( &msg );
demo.playPrevEntities[entNum].absmin[2] = MSG_ReadFloat( &msg );
demo.playPrevEntities[entNum].absmax[0] = MSG_ReadFloat( &msg );
demo.playPrevEntities[entNum].absmax[1] = MSG_ReadFloat( &msg );
demo.playPrevEntities[entNum].absmax[2] = MSG_ReadFloat( &msg );
}
} }
demo.playPrevEntities[entNum].es = newEs; demo.playPrevEntities[entNum].es = newEs;
demo.playPrevEntities[entNum].active = qtrue; demo.playPrevEntities[entNum].active = qtrue;
// apply to server entity // apply to server entity (entity linking done in G_RunFrame
// which has BG_EvaluateTrajectory for computing currentOrigin)
ent = SV_GentityNum( entNum ); ent = SV_GentityNum( entNum );
ent->s = newEs; ent->s = newEs;
ent->s.number = entNum; ent->s.number = entNum;
ent->r.svFlags = demo.playPrevEntities[entNum].svFlags; ent->r.svFlags = demo.playPrevEntities[entNum].svFlags;
ent->r.linked = demo.playPrevEntities[entNum].linked;
VectorCopy( demo.playPrevEntities[entNum].currentOrigin, ent->r.currentOrigin );
VectorCopy( demo.playPrevEntities[entNum].absmin, ent->r.absmin );
VectorCopy( demo.playPrevEntities[entNum].absmax, ent->r.absmax );
if ( ent->r.linked ) {
SV_LinkEntity( ent );
}
if ( entNum + 1 > sv.num_entities ) { if ( entNum + 1 > sv.num_entities ) {
sv.num_entities = entNum + 1; sv.num_entities = entNum + 1;