Delta-compress netdemo entity states
Use MSG_WriteDeltaEntity/MSG_ReadDeltaEntity for entity state serialization. Only changed fields are written per frame. PVS fields (svFlags, linked, origin, absmin, absmax) also use a 1-bit change flag to skip when unchanged. Reduces a 10-second demo from ~1400KB to ~52KB (27x smaller). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a8044aad8b
commit
de9863da57
1 changed files with 158 additions and 46 deletions
|
|
@ -57,11 +57,23 @@ snapshot pipeline delivers them to a spectator client.
|
|||
// State
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// per-entity data stored for delta compression (entityState + PVS fields)
|
||||
typedef struct {
|
||||
entityState_t es;
|
||||
int svFlags;
|
||||
qboolean linked;
|
||||
vec3_t currentOrigin;
|
||||
vec3_t absmin;
|
||||
vec3_t absmax;
|
||||
qboolean active; // was this entity present last frame?
|
||||
} svdEntityState_t;
|
||||
|
||||
typedef struct {
|
||||
// recording
|
||||
fileHandle_t recordFile;
|
||||
qboolean recording;
|
||||
char *lastConfigstrings[MAX_CONFIGSTRINGS]; // for delta detection
|
||||
svdEntityState_t prevEntities[MAX_GENTITIES]; // previous frame for delta
|
||||
|
||||
// playback
|
||||
fileHandle_t playFile;
|
||||
|
|
@ -74,6 +86,7 @@ typedef struct {
|
|||
int nextFrameTime; // serverTime of next frame to read
|
||||
qboolean endOfDemo;
|
||||
qboolean needConfigstrings; // apply saved configstrings on first frame
|
||||
svdEntityState_t playPrevEntities[MAX_GENTITIES]; // previous frame for delta read
|
||||
} svDemo_t;
|
||||
|
||||
static svDemo_t demo;
|
||||
|
|
@ -150,37 +163,84 @@ static void SVD_WriteHeader( fileHandle_t f ) {
|
|||
// ---------------------------------------------------------------
|
||||
|
||||
static void SVD_WriteFrame( fileHandle_t f ) {
|
||||
int i, count;
|
||||
int i;
|
||||
sharedEntity_t *ent;
|
||||
short numChanges;
|
||||
msg_t msg;
|
||||
byte msgBuf[MAX_GENTITIES * 64]; // delta-compressed entities are small
|
||||
int msgLen;
|
||||
|
||||
SVD_WriteInt( f, svs.time );
|
||||
SVD_WriteShort( f, (short)sv.num_entities );
|
||||
|
||||
// delta-compress all entities into a message buffer
|
||||
MSG_Init( &msg, msgBuf, sizeof(msgBuf) );
|
||||
|
||||
// count active entities
|
||||
count = 0;
|
||||
for ( i = 0; i < sv.num_entities; i++ ) {
|
||||
qboolean active;
|
||||
ent = SV_GentityNum( i );
|
||||
if ( ent->r.linked || ent->s.eType != 0 ) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
SVD_WriteShort( f, (short)count );
|
||||
active = ( ent->r.linked || ent->s.eType != 0 );
|
||||
|
||||
// write each active entity
|
||||
for ( i = 0; i < sv.num_entities; i++ ) {
|
||||
ent = SV_GentityNum( i );
|
||||
if ( !ent->r.linked && ent->s.eType == 0 ) {
|
||||
continue;
|
||||
if ( active ) {
|
||||
entityState_t baseline;
|
||||
entityState_t *from;
|
||||
if ( demo.prevEntities[i].active ) {
|
||||
from = &demo.prevEntities[i].es;
|
||||
} else {
|
||||
Com_Memset( &baseline, 0, sizeof(baseline) );
|
||||
baseline.number = i;
|
||||
from = &baseline;
|
||||
}
|
||||
MSG_WriteDeltaEntity( &msg, from, &ent->s, qtrue );
|
||||
|
||||
// write PVS fields only if changed from previous frame
|
||||
{
|
||||
qboolean pvsChanged = !demo.prevEntities[i].active
|
||||
|| ent->r.svFlags != demo.prevEntities[i].svFlags
|
||||
|| ent->r.linked != demo.prevEntities[i].linked
|
||||
|| !VectorCompare( ent->r.currentOrigin, demo.prevEntities[i].currentOrigin )
|
||||
|| !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] );
|
||||
}
|
||||
}
|
||||
|
||||
SVD_WriteShort( f, (short)i );
|
||||
FS_Write( &ent->s, sizeof(entityState_t), f );
|
||||
SVD_WriteInt( f, ent->r.svFlags );
|
||||
SVD_WriteInt( f, ent->r.linked );
|
||||
FS_Write( ent->r.currentOrigin, 12, f );
|
||||
FS_Write( ent->r.absmin, 12, f );
|
||||
FS_Write( ent->r.absmax, 12, f );
|
||||
// update prev state
|
||||
demo.prevEntities[i].es = ent->s;
|
||||
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;
|
||||
} else if ( demo.prevEntities[i].active ) {
|
||||
// entity was removed — write a remove marker
|
||||
MSG_WriteDeltaEntity( &msg, &demo.prevEntities[i].es, NULL, qtrue );
|
||||
demo.prevEntities[i].active = qfalse;
|
||||
}
|
||||
// else: entity was inactive and still inactive — write nothing
|
||||
}
|
||||
|
||||
// end of entities marker
|
||||
MSG_WriteBits( &msg, (MAX_GENTITIES - 1), GENTITYNUM_BITS );
|
||||
|
||||
// write compressed message to file
|
||||
msgLen = msg.cursize;
|
||||
SVD_WriteInt( f, msgLen );
|
||||
FS_Write( msg.data, msgLen, f );
|
||||
|
||||
// configstring changes
|
||||
numChanges = 0;
|
||||
|
|
@ -247,6 +307,9 @@ void SVD_Record_f( void ) {
|
|||
Com_Printf( "Recording server demo to %s\n", name );
|
||||
demo.recording = qtrue;
|
||||
|
||||
// clear delta state for fresh recording
|
||||
Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) );
|
||||
|
||||
SVD_WriteHeader( demo.recordFile );
|
||||
}
|
||||
|
||||
|
|
@ -361,8 +424,11 @@ static void SVD_ApplyConfigstrings( void ) {
|
|||
static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
||||
int serverTime;
|
||||
short numEnts, numChanges;
|
||||
int i;
|
||||
int i, entNum, msgLen;
|
||||
sharedEntity_t *ent;
|
||||
msg_t msg;
|
||||
byte msgBuf[MAX_GENTITIES * 64];
|
||||
entityState_t newEs;
|
||||
|
||||
serverTime = SVD_ReadInt( f );
|
||||
if ( serverTime == -1 ) {
|
||||
|
|
@ -370,54 +436,100 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
|||
}
|
||||
|
||||
svs.time = serverTime;
|
||||
numEnts = SVD_ReadShort( f );
|
||||
|
||||
// clear all entities except the spectator slot
|
||||
// read compressed message
|
||||
msgLen = SVD_ReadInt( f );
|
||||
if ( msgLen <= 0 || msgLen > (int)sizeof(msgBuf) ) {
|
||||
return qfalse;
|
||||
}
|
||||
FS_Read( msgBuf, msgLen, f );
|
||||
MSG_Init( &msg, msgBuf, sizeof(msgBuf) );
|
||||
msg.cursize = msgLen;
|
||||
|
||||
// clear all non-spectator entities
|
||||
for ( i = 0; i < sv.num_entities; i++ ) {
|
||||
if ( i == demo.spectatorClientNum ) {
|
||||
continue;
|
||||
}
|
||||
ent = SV_GentityNum( i );
|
||||
ent->r.linked = qfalse;
|
||||
if ( ent->r.linked ) {
|
||||
SV_UnlinkEntity( ent );
|
||||
}
|
||||
ent->s.eType = 0;
|
||||
ent->s.number = i;
|
||||
}
|
||||
|
||||
numEnts = SVD_ReadShort( f );
|
||||
// parse delta-compressed entities
|
||||
while ( 1 ) {
|
||||
entNum = MSG_ReadBits( &msg, GENTITYNUM_BITS );
|
||||
if ( entNum == MAX_GENTITIES - 1 ) {
|
||||
break; // end of entities marker
|
||||
}
|
||||
if ( msg.readcount > msg.cursize ) {
|
||||
break;
|
||||
}
|
||||
|
||||
for ( i = 0; i < numEnts; i++ ) {
|
||||
short entNum;
|
||||
entityState_t es;
|
||||
int svFlags, linked;
|
||||
vec3_t currentOrigin, absmin, absmax;
|
||||
{
|
||||
entityState_t baseline;
|
||||
entityState_t *from;
|
||||
Com_Memset( &newEs, 0, sizeof(newEs) );
|
||||
if ( demo.playPrevEntities[entNum].active ) {
|
||||
from = &demo.playPrevEntities[entNum].es;
|
||||
} else {
|
||||
Com_Memset( &baseline, 0, sizeof(baseline) );
|
||||
baseline.number = entNum;
|
||||
from = &baseline;
|
||||
}
|
||||
MSG_ReadDeltaEntity( &msg, from, &newEs, entNum );
|
||||
}
|
||||
|
||||
entNum = SVD_ReadShort( f );
|
||||
FS_Read( &es, sizeof(entityState_t), f );
|
||||
svFlags = SVD_ReadInt( f );
|
||||
linked = SVD_ReadInt( f );
|
||||
FS_Read( currentOrigin, 12, f );
|
||||
FS_Read( absmin, 12, f );
|
||||
FS_Read( absmax, 12, f );
|
||||
if ( newEs.number == MAX_GENTITIES - 1 ) {
|
||||
// entity was removed
|
||||
demo.playPrevEntities[entNum].active = qfalse;
|
||||
continue;
|
||||
}
|
||||
|
||||
// read PVS fields
|
||||
{
|
||||
qboolean pvsChanged = MSG_ReadBits( &msg, 1 );
|
||||
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].active = qtrue;
|
||||
|
||||
// skip the spectator's slot
|
||||
if ( entNum == demo.spectatorClientNum ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// apply to server entity
|
||||
ent = SV_GentityNum( entNum );
|
||||
ent->s = es;
|
||||
ent->s = newEs;
|
||||
ent->s.number = entNum;
|
||||
ent->r.svFlags = svFlags;
|
||||
ent->r.linked = linked;
|
||||
VectorCopy( currentOrigin, ent->r.currentOrigin );
|
||||
VectorCopy( absmin, ent->r.absmin );
|
||||
VectorCopy( absmax, ent->r.absmax );
|
||||
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 );
|
||||
|
||||
// link into the world so PVS can find this entity
|
||||
if ( linked ) {
|
||||
if ( ent->r.linked ) {
|
||||
SV_LinkEntity( ent );
|
||||
}
|
||||
|
||||
// update num_entities high water mark
|
||||
if ( entNum + 1 > sv.num_entities ) {
|
||||
sv.num_entities = entNum + 1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue