Optional LZ4 compression for demo files
Per-frame entity and playerState blocks are compressed with LZ4 when svdemo_compress is set (default: 1). The block format writes [original_size][compressed_size][data] — compressed_size=0 means uncompressed. Playback auto-detects based on header flags. Demo format bumped to version 2 with SVDEMO_FLAG_COMPRESSED flag. Version 1 (uncompressed) demos are no longer compatible. Uses the lz4.c/lz4.h library already in the server code directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d2b82580ef
commit
330cc30ae7
3 changed files with 81 additions and 20 deletions
|
|
@ -997,6 +997,8 @@
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<ClCompile Include="server\sv_netdemo.c">
|
<ClCompile Include="server\sv_netdemo.c">
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
<ClCompile Include="server\lz4.c">
|
||||||
|
</ClCompile>
|
||||||
<ClCompile Include="server\sv_world.c">
|
<ClCompile Include="server\sv_world.c">
|
||||||
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug TA DEMO|Win32'">Disabled</Optimization>
|
<Optimization Condition="'$(Configuration)|$(Platform)'=='Debug TA DEMO|Win32'">Disabled</Optimization>
|
||||||
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug TA DEMO|Win32'">true</BrowseInformation>
|
<BrowseInformation Condition="'$(Configuration)|$(Platform)'=='Debug TA DEMO|Win32'">true</BrowseInformation>
|
||||||
|
|
|
||||||
|
|
@ -621,8 +621,9 @@ void SV_Init (void) {
|
||||||
sv_lanForceRate = Cvar_Get ("sv_lanForceRate", "1", CVAR_ARCHIVE );
|
sv_lanForceRate = Cvar_Get ("sv_lanForceRate", "1", CVAR_ARCHIVE );
|
||||||
sv_strictAuth = Cvar_Get ("sv_strictAuth", "1", CVAR_ARCHIVE );
|
sv_strictAuth = Cvar_Get ("sv_strictAuth", "1", CVAR_ARCHIVE );
|
||||||
|
|
||||||
// server-side demo auto-recording
|
// server-side demo settings
|
||||||
Cvar_Get ("svdemo_autorecord", "0", CVAR_ARCHIVE);
|
Cvar_Get ("svdemo_autorecord", "0", CVAR_ARCHIVE);
|
||||||
|
Cvar_Get ("svdemo_compress", "1", CVAR_ARCHIVE);
|
||||||
|
|
||||||
// initialize bot cvars so they are listed and can be set before loading the botlib
|
// initialize bot cvars so they are listed and can be set before loading the botlib
|
||||||
SV_BotInitCvars();
|
SV_BotInitCvars();
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ snapshot pipeline delivers them to a spectator client.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "server.h"
|
#include "server.h"
|
||||||
|
#include "lz4.h"
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// File format
|
// File format
|
||||||
|
|
@ -50,9 +51,12 @@ 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 1
|
#define SVDEMO_VERSION 2 // v2: optional LZ4 compression
|
||||||
#define SVDEMO_MAX_MAPNAME 64
|
#define SVDEMO_MAX_MAPNAME 64
|
||||||
|
|
||||||
|
// header flags
|
||||||
|
#define SVDEMO_FLAG_COMPRESSED 1 // per-frame data is LZ4 compressed
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// State
|
// State
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
@ -78,6 +82,7 @@ typedef struct {
|
||||||
// recording
|
// recording
|
||||||
fileHandle_t recordFile;
|
fileHandle_t recordFile;
|
||||||
qboolean recording;
|
qboolean recording;
|
||||||
|
qboolean compressed; // LZ4 compression enabled
|
||||||
char *lastConfigstrings[MAX_CONFIGSTRINGS]; // for delta detection
|
char *lastConfigstrings[MAX_CONFIGSTRINGS]; // for delta detection
|
||||||
svdEntityState_t prevEntities[MAX_GENTITIES]; // previous frame for delta
|
svdEntityState_t prevEntities[MAX_GENTITIES]; // previous frame for delta
|
||||||
svdPlayerState_t prevPlayers[MAX_CLIENTS]; // previous frame player states
|
svdPlayerState_t prevPlayers[MAX_CLIENTS]; // previous frame player states
|
||||||
|
|
@ -123,6 +128,58 @@ static short SVD_ReadShort( fileHandle_t f ) {
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// LZ4 block I/O: writes [uncompressed_size][compressed_size][data]
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
static void SVD_WriteBlock( fileHandle_t f, const byte *data, int len ) {
|
||||||
|
if ( demo.compressed && len > 0 ) {
|
||||||
|
int bound = LZ4_compressBound( len );
|
||||||
|
static byte compBuf[MAX_GENTITIES * 300];
|
||||||
|
int compLen;
|
||||||
|
|
||||||
|
compLen = LZ4_compress_default( (const char *)data, (char *)compBuf, len, bound );
|
||||||
|
if ( compLen > 0 ) {
|
||||||
|
SVD_WriteInt( f, len ); // original size
|
||||||
|
SVD_WriteInt( f, compLen ); // compressed size
|
||||||
|
FS_Write( compBuf, compLen, f );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// fall through to uncompressed on failure
|
||||||
|
}
|
||||||
|
SVD_WriteInt( f, len ); // original size
|
||||||
|
SVD_WriteInt( f, 0 ); // 0 = not compressed
|
||||||
|
FS_Write( data, len, f );
|
||||||
|
}
|
||||||
|
|
||||||
|
static int SVD_ReadBlock( fileHandle_t f, byte *buf, int bufSize ) {
|
||||||
|
int origLen, compLen;
|
||||||
|
|
||||||
|
origLen = SVD_ReadInt( f );
|
||||||
|
compLen = SVD_ReadInt( f );
|
||||||
|
|
||||||
|
if ( origLen <= 0 || origLen > bufSize ) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( compLen > 0 ) {
|
||||||
|
// compressed
|
||||||
|
static byte compBuf[MAX_GENTITIES * 300];
|
||||||
|
if ( compLen > (int)sizeof(compBuf) ) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
FS_Read( compBuf, compLen, f );
|
||||||
|
if ( LZ4_decompress_safe( (const char *)compBuf, (char *)buf, compLen, bufSize ) != origLen ) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// uncompressed
|
||||||
|
FS_Read( buf, origLen, f );
|
||||||
|
}
|
||||||
|
|
||||||
|
return origLen;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// Write header
|
// Write header
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
@ -133,6 +190,7 @@ static void SVD_WriteHeader( fileHandle_t f ) {
|
||||||
|
|
||||||
SVD_WriteInt( f, SVDEMO_MAGIC );
|
SVD_WriteInt( f, SVDEMO_MAGIC );
|
||||||
SVD_WriteInt( f, SVDEMO_VERSION );
|
SVD_WriteInt( f, SVDEMO_VERSION );
|
||||||
|
SVD_WriteInt( f, demo.compressed ? SVDEMO_FLAG_COMPRESSED : 0 );
|
||||||
SVD_WriteInt( f, sv_maxclients->integer );
|
SVD_WriteInt( f, sv_maxclients->integer );
|
||||||
SVD_WriteInt( f, sv_fps->integer );
|
SVD_WriteInt( f, sv_fps->integer );
|
||||||
|
|
||||||
|
|
@ -176,7 +234,6 @@ static void SVD_WriteFrame( fileHandle_t f ) {
|
||||||
short numChanges;
|
short numChanges;
|
||||||
msg_t msg;
|
msg_t msg;
|
||||||
static byte msgBuf[MAX_GENTITIES * 300]; // worst case: all entities full write from baseline
|
static byte msgBuf[MAX_GENTITIES * 300]; // worst case: all entities full write from baseline
|
||||||
int msgLen;
|
|
||||||
|
|
||||||
SVD_WriteInt( f, svs.time );
|
SVD_WriteInt( f, svs.time );
|
||||||
SVD_WriteShort( f, (short)sv.num_entities );
|
SVD_WriteShort( f, (short)sv.num_entities );
|
||||||
|
|
@ -245,10 +302,8 @@ static void SVD_WriteFrame( fileHandle_t f ) {
|
||||||
// end of entities marker
|
// end of entities marker
|
||||||
MSG_WriteBits( &msg, (MAX_GENTITIES - 1), GENTITYNUM_BITS );
|
MSG_WriteBits( &msg, (MAX_GENTITIES - 1), GENTITYNUM_BITS );
|
||||||
|
|
||||||
// write compressed entity message to file
|
// write entity message to file (optionally LZ4 compressed)
|
||||||
msgLen = msg.cursize;
|
SVD_WriteBlock( f, msg.data, msg.cursize );
|
||||||
SVD_WriteInt( f, msgLen );
|
|
||||||
FS_Write( msg.data, msgLen, f );
|
|
||||||
|
|
||||||
// write player states (delta compressed)
|
// write player states (delta compressed)
|
||||||
{
|
{
|
||||||
|
|
@ -282,9 +337,7 @@ static void SVD_WriteFrame( fileHandle_t f ) {
|
||||||
MSG_WriteBits( &psmsg, MAX_CLIENTS - 1, 6 );
|
MSG_WriteBits( &psmsg, MAX_CLIENTS - 1, 6 );
|
||||||
MSG_WriteBits( &psmsg, 0, 1 );
|
MSG_WriteBits( &psmsg, 0, 1 );
|
||||||
|
|
||||||
msgLen = psmsg.cursize;
|
SVD_WriteBlock( f, psmsg.data, psmsg.cursize );
|
||||||
SVD_WriteInt( f, msgLen );
|
|
||||||
FS_Write( psmsg.data, msgLen, f );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// configstring changes
|
// configstring changes
|
||||||
|
|
@ -346,8 +399,10 @@ static qboolean SVD_StartRecording( const char *demoname ) {
|
||||||
return qfalse;
|
return qfalse;
|
||||||
}
|
}
|
||||||
|
|
||||||
Com_Printf( "Recording server demo to %s\n", path );
|
Com_Printf( "Recording server demo to %s%s\n", path,
|
||||||
|
Cvar_VariableIntegerValue("svdemo_compress") ? " (LZ4)" : "" );
|
||||||
demo.recording = qtrue;
|
demo.recording = qtrue;
|
||||||
|
demo.compressed = Cvar_VariableIntegerValue("svdemo_compress") ? qtrue : qfalse;
|
||||||
|
|
||||||
// clear delta state for fresh recording
|
// clear delta state for fresh recording
|
||||||
Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) );
|
Com_Memset( demo.prevEntities, 0, sizeof(demo.prevEntities) );
|
||||||
|
|
@ -467,6 +522,11 @@ static qboolean SVD_ReadHeader( fileHandle_t f ) {
|
||||||
return qfalse;
|
return qfalse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
int flags = SVD_ReadInt( f );
|
||||||
|
demo.compressed = ( flags & SVDEMO_FLAG_COMPRESSED ) ? qtrue : qfalse;
|
||||||
|
}
|
||||||
|
|
||||||
demo.playMaxClients = SVD_ReadInt( f );
|
demo.playMaxClients = SVD_ReadInt( f );
|
||||||
demo.playFps = SVD_ReadInt( f );
|
demo.playFps = SVD_ReadInt( f );
|
||||||
FS_Read( demo.playMapName, SVDEMO_MAX_MAPNAME, f );
|
FS_Read( demo.playMapName, SVDEMO_MAX_MAPNAME, f );
|
||||||
|
|
@ -523,7 +583,7 @@ 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, entNum, msgLen;
|
int i, entNum, blockLen;
|
||||||
sharedEntity_t *ent;
|
sharedEntity_t *ent;
|
||||||
msg_t msg;
|
msg_t msg;
|
||||||
static byte msgBuf[MAX_GENTITIES * 300];
|
static byte msgBuf[MAX_GENTITIES * 300];
|
||||||
|
|
@ -539,14 +599,13 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
||||||
// time jumps that break client timeouts and heartbeats.
|
// time jumps that break client timeouts and heartbeats.
|
||||||
numEnts = SVD_ReadShort( f );
|
numEnts = SVD_ReadShort( f );
|
||||||
|
|
||||||
// read compressed message
|
// read entity message (optionally LZ4 compressed)
|
||||||
msgLen = SVD_ReadInt( f );
|
blockLen = SVD_ReadBlock( f, msgBuf, sizeof(msgBuf) );
|
||||||
if ( msgLen <= 0 || msgLen > (int)sizeof(msgBuf) ) {
|
if ( blockLen <= 0 ) {
|
||||||
return qfalse;
|
return qfalse;
|
||||||
}
|
}
|
||||||
FS_Read( msgBuf, msgLen, f );
|
|
||||||
MSG_Init( &msg, msgBuf, sizeof(msgBuf) );
|
MSG_Init( &msg, msgBuf, sizeof(msgBuf) );
|
||||||
msg.cursize = msgLen;
|
msg.cursize = blockLen;
|
||||||
|
|
||||||
// clear all non-spectator entities
|
// clear all non-spectator entities
|
||||||
for ( i = 0; i < sv.num_entities; i++ ) {
|
for ( i = 0; i < sv.num_entities; i++ ) {
|
||||||
|
|
@ -647,9 +706,8 @@ static qboolean SVD_ReadFrame( fileHandle_t f ) {
|
||||||
static byte psBuf[MAX_CLIENTS * 600]; // worst case: full playerState from baseline
|
static byte psBuf[MAX_CLIENTS * 600]; // worst case: full playerState from baseline
|
||||||
int psMsgLen;
|
int psMsgLen;
|
||||||
|
|
||||||
psMsgLen = SVD_ReadInt( f );
|
psMsgLen = SVD_ReadBlock( f, psBuf, sizeof(psBuf) );
|
||||||
if ( psMsgLen > 0 && psMsgLen <= (int)sizeof(psBuf) ) {
|
if ( psMsgLen > 0 ) {
|
||||||
FS_Read( psBuf, psMsgLen, f );
|
|
||||||
MSG_Init( &psmsg, psBuf, sizeof(psBuf) );
|
MSG_Init( &psmsg, psBuf, sizeof(psBuf) );
|
||||||
psmsg.cursize = psMsgLen;
|
psmsg.cursize = psMsgLen;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue