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
|
// 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 {
|
typedef struct {
|
||||||
// recording
|
// recording
|
||||||
fileHandle_t recordFile;
|
fileHandle_t recordFile;
|
||||||
qboolean recording;
|
qboolean recording;
|
||||||
char *lastConfigstrings[MAX_CONFIGSTRINGS]; // for delta detection
|
char *lastConfigstrings[MAX_CONFIGSTRINGS]; // for delta detection
|
||||||
|
svdEntityState_t prevEntities[MAX_GENTITIES]; // previous frame for delta
|
||||||
|
|
||||||
// playback
|
// playback
|
||||||
fileHandle_t playFile;
|
fileHandle_t playFile;
|
||||||
|
|
@ -74,6 +86,7 @@ typedef struct {
|
||||||
int nextFrameTime; // serverTime of next frame to read
|
int nextFrameTime; // serverTime of next frame to read
|
||||||
qboolean endOfDemo;
|
qboolean endOfDemo;
|
||||||
qboolean needConfigstrings; // apply saved configstrings on first frame
|
qboolean needConfigstrings; // apply saved configstrings on first frame
|
||||||
|
svdEntityState_t playPrevEntities[MAX_GENTITIES]; // previous frame for delta read
|
||||||
} svDemo_t;
|
} svDemo_t;
|
||||||
|
|
||||||
static svDemo_t demo;
|
static svDemo_t demo;
|
||||||
|
|
@ -150,38 +163,85 @@ static void SVD_WriteHeader( fileHandle_t f ) {
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
static void SVD_WriteFrame( fileHandle_t f ) {
|
static void SVD_WriteFrame( fileHandle_t f ) {
|
||||||
int i, count;
|
int i;
|
||||||
sharedEntity_t *ent;
|
sharedEntity_t *ent;
|
||||||
short numChanges;
|
short numChanges;
|
||||||
|
msg_t msg;
|
||||||
|
byte msgBuf[MAX_GENTITIES * 64]; // delta-compressed entities are small
|
||||||
|
int msgLen;
|
||||||
|
|
||||||
SVD_WriteInt( f, svs.time );
|
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++ ) {
|
for ( i = 0; i < sv.num_entities; i++ ) {
|
||||||
|
qboolean active;
|
||||||
ent = SV_GentityNum( i );
|
ent = SV_GentityNum( i );
|
||||||
if ( ent->r.linked || ent->s.eType != 0 ) {
|
active = ( ent->r.linked || ent->s.eType != 0 );
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
SVD_WriteShort( f, (short)count );
|
|
||||||
|
|
||||||
// write each active entity
|
if ( active ) {
|
||||||
for ( i = 0; i < sv.num_entities; i++ ) {
|
entityState_t baseline;
|
||||||
ent = SV_GentityNum( i );
|
entityState_t *from;
|
||||||
if ( !ent->r.linked && ent->s.eType == 0 ) {
|
if ( demo.prevEntities[i].active ) {
|
||||||
continue;
|
from = &demo.prevEntities[i].es;
|
||||||
}
|
} else {
|
||||||
|
Com_Memset( &baseline, 0, sizeof(baseline) );
|
||||||
|
baseline.number = i;
|
||||||
|
from = &baseline;
|
||||||
|
}
|
||||||
|
MSG_WriteDeltaEntity( &msg, from, &ent->s, qtrue );
|
||||||
|
|
||||||
SVD_WriteShort( f, (short)i );
|
// write PVS fields only if changed from previous frame
|
||||||
FS_Write( &ent->s, sizeof(entityState_t), f );
|
{
|
||||||
SVD_WriteInt( f, ent->r.svFlags );
|
qboolean pvsChanged = !demo.prevEntities[i].active
|
||||||
SVD_WriteInt( f, ent->r.linked );
|
|| ent->r.svFlags != demo.prevEntities[i].svFlags
|
||||||
FS_Write( ent->r.currentOrigin, 12, f );
|
|| ent->r.linked != demo.prevEntities[i].linked
|
||||||
FS_Write( ent->r.absmin, 12, f );
|
|| !VectorCompare( ent->r.currentOrigin, demo.prevEntities[i].currentOrigin )
|
||||||
FS_Write( ent->r.absmax, 12, f );
|
|| !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
|
||||||
|
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
|
// configstring changes
|
||||||
numChanges = 0;
|
numChanges = 0;
|
||||||
for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
|
for ( i = 0; i < MAX_CONFIGSTRINGS; i++ ) {
|
||||||
|
|
@ -247,6 +307,9 @@ void SVD_Record_f( void ) {
|
||||||
Com_Printf( "Recording server demo to %s\n", name );
|
Com_Printf( "Recording server demo to %s\n", name );
|
||||||
demo.recording = qtrue;
|
demo.recording = qtrue;
|
||||||
|
|
||||||
|
// clear delta state for fresh recording
|
||||||
|
Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) );
|
||||||
|
|
||||||
SVD_WriteHeader( demo.recordFile );
|
SVD_WriteHeader( demo.recordFile );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -361,8 +424,11 @@ static void SVD_ApplyConfigstrings( void ) {
|
||||||
static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
||||||
int serverTime;
|
int serverTime;
|
||||||
short numEnts, numChanges;
|
short numEnts, numChanges;
|
||||||
int i;
|
int i, entNum, msgLen;
|
||||||
sharedEntity_t *ent;
|
sharedEntity_t *ent;
|
||||||
|
msg_t msg;
|
||||||
|
byte msgBuf[MAX_GENTITIES * 64];
|
||||||
|
entityState_t newEs;
|
||||||
|
|
||||||
serverTime = SVD_ReadInt( f );
|
serverTime = SVD_ReadInt( f );
|
||||||
if ( serverTime == -1 ) {
|
if ( serverTime == -1 ) {
|
||||||
|
|
@ -370,54 +436,100 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
svs.time = serverTime;
|
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++ ) {
|
for ( i = 0; i < sv.num_entities; i++ ) {
|
||||||
if ( i == demo.spectatorClientNum ) {
|
if ( i == demo.spectatorClientNum ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ent = SV_GentityNum( i );
|
ent = SV_GentityNum( i );
|
||||||
ent->r.linked = qfalse;
|
if ( ent->r.linked ) {
|
||||||
|
SV_UnlinkEntity( ent );
|
||||||
|
}
|
||||||
ent->s.eType = 0;
|
ent->s.eType = 0;
|
||||||
ent->s.number = i;
|
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 baseline;
|
||||||
entityState_t es;
|
entityState_t *from;
|
||||||
int svFlags, linked;
|
Com_Memset( &newEs, 0, sizeof(newEs) );
|
||||||
vec3_t currentOrigin, absmin, absmax;
|
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 );
|
if ( newEs.number == MAX_GENTITIES - 1 ) {
|
||||||
FS_Read( &es, sizeof(entityState_t), f );
|
// entity was removed
|
||||||
svFlags = SVD_ReadInt( f );
|
demo.playPrevEntities[entNum].active = qfalse;
|
||||||
linked = SVD_ReadInt( f );
|
continue;
|
||||||
FS_Read( currentOrigin, 12, f );
|
}
|
||||||
FS_Read( absmin, 12, f );
|
|
||||||
FS_Read( absmax, 12, f );
|
// 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
|
// skip the spectator's slot
|
||||||
if ( entNum == demo.spectatorClientNum ) {
|
if ( entNum == demo.spectatorClientNum ) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply to server entity
|
||||||
ent = SV_GentityNum( entNum );
|
ent = SV_GentityNum( entNum );
|
||||||
ent->s = es;
|
ent->s = newEs;
|
||||||
ent->s.number = entNum;
|
ent->s.number = entNum;
|
||||||
ent->r.svFlags = svFlags;
|
ent->r.svFlags = demo.playPrevEntities[entNum].svFlags;
|
||||||
ent->r.linked = linked;
|
ent->r.linked = demo.playPrevEntities[entNum].linked;
|
||||||
VectorCopy( currentOrigin, ent->r.currentOrigin );
|
VectorCopy( demo.playPrevEntities[entNum].currentOrigin, ent->r.currentOrigin );
|
||||||
VectorCopy( absmin, ent->r.absmin );
|
VectorCopy( demo.playPrevEntities[entNum].absmin, ent->r.absmin );
|
||||||
VectorCopy( absmax, ent->r.absmax );
|
VectorCopy( demo.playPrevEntities[entNum].absmax, ent->r.absmax );
|
||||||
|
|
||||||
// link into the world so PVS can find this entity
|
if ( ent->r.linked ) {
|
||||||
if ( linked ) {
|
|
||||||
SV_LinkEntity( ent );
|
SV_LinkEntity( ent );
|
||||||
}
|
}
|
||||||
|
|
||||||
// update num_entities high water mark
|
|
||||||
if ( entNum + 1 > sv.num_entities ) {
|
if ( entNum + 1 > sv.num_entities ) {
|
||||||
sv.num_entities = entNum + 1;
|
sv.num_entities = entNum + 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue