|
|
//======= Copyright (c) 1996-2009, Valve Corporation, All rights reserved. ======
//
// TODO:
// - USE A MEMPOOL OF SOME SORT
// - GET STALE FRAME REMOVAL WORKING
// - MAKE THREAD SAFE - NEED TO BE ABLE TO WRITE A .DEM FILE ON A SEP THREAD AND
// NOT BE THROWING AWAY ELEMENTS THAT ARE BEING READ. NEED A FLAG IN DATACHUNK
// FOR WHETHER AN INSTANCE IS "IN USE," IE BEING READ TO WRITE TO A DEM FILE.
//
//===============================================================================
#include "demobuffer.h"
#include "edict.h"
#include "host.h"
#include "tier1/mempool.h"
#include "vstdlib/jobthread.h"
#include "replayserver.h" // TODO: Remove
#include "sv_client.h"
#ifndef DEDICATED
#include "cdll_int.h"
#include "client.h"
#endif
#include "tier0/memdbgon.h" // NOTE: Must go last!
#ifndef DEDICATED
#if !defined( _DEBUG )
// These are the buffers defining how demo data is flushed to disk:
// We allocate 5 MB buffer (worth about 5 min of gameplay)
// once over 4 MB is used up we will commit the first 1 MB to disk
// This throttles disk IO, and ensures that the last 3 MB are always
// contained in memory and not committed to disk to prevent file peeking
// for cheating purposes during gameplay
#define DISK_DEMO_BUFFER_TOTAL_SIZE (5*1024*1024)
#define DISK_DEMO_BUFFER_FLUSH_CONSIDER_SIZE (4*1024*1024)
#define DISK_DEMO_BUFFER_FLUSH_TODISK_SIZE (1*1024*1024)
#else
// Smaller sizes in debug engine.dll build to hammer on the subsystems involved
#define DISK_DEMO_BUFFER_TOTAL_SIZE (5*20*1024)
#define DISK_DEMO_BUFFER_FLUSH_CONSIDER_SIZE (4*20*1024)
#define DISK_DEMO_BUFFER_FLUSH_TODISK_SIZE (1*20*1024)
#endif
#endif
//-----------------------------------------------------------------------------
// Specialty class with overrides for stream buffer
//-----------------------------------------------------------------------------
class CDiskDemoBuffer : public IDemoBuffer { public: CDiskDemoBuffer() : m_pBuffer( NULL ) { m_nDecodedOffset = -1; }
~CDiskDemoBuffer() { m_pBuffer->Close(); delete m_pBuffer; }
virtual bool Init( DemoBufferInitParams_t const& params ) { // Convert to proper type
StreamDemoBufferInitParams_t const* pParams = dynamic_cast< StreamDemoBufferInitParams_t const* >( ¶ms ); Assert( pParams );
// Allocate buffer
m_pBuffer = new CUtlStreamBuffer(); if ( !m_pBuffer ) return false;
#ifndef DEDICATED
// Force a very large memory buffer on the clients, this prevents peeking into the demo stream
m_pBuffer->EnsureCapacity( DISK_DEMO_BUFFER_TOTAL_SIZE ); #endif
// Demo files are always little endian
m_pBuffer->SetBigEndian( false ); m_bufDecoded.SetBigEndian( false );
// Open the file
// m_pBuffer->Open( pParams->pFilename, pParams->pszPath, pParams->nFlags, pParams->nOpenFileFlags ); // For main integration...
m_pBuffer->Open( pParams->pFilename, pParams->pszPath, pParams->nFlags ); m_nDecodedOffset = -1;
m_pPlaybackParams = NULL; #ifndef DEDICATED
extern IDemoPlayer *demoplayer; extern IBaseClientDLL *g_ClientDLL; if ( demoplayer && g_ClientDLL ) { m_pPlaybackParams = demoplayer->GetDemoPlaybackParameters(); } #endif
return IsInitialized(); }
virtual void NotifySignonComplete() {}
virtual void WriteHeader( void const *pData, int nSize ) { // Byteswap
demoheader_t littleEndianHeader = *((demoheader_t*)pData); ByteSwap_demoheader_t( littleEndianHeader );
// Goto file start
SeekPut( true, 0 );
// Write
Put( pData, nSize ); }
virtual void NotifyBeginFrame() {} virtual void NotifyEndFrame() {}
virtual void PutChar( char c ) { m_pBuffer->PutChar( c ); } virtual void PutUnsignedChar( unsigned char uc ) { m_pBuffer->PutUnsignedChar( uc ); } virtual void PutInt( int i ) { m_pBuffer->PutInt( i ); }
virtual void WriteTick( int nTick ) { m_pBuffer->PutInt( nTick ); }
virtual char GetChar() OVERRIDE { COnTheFlyDemoBufferReadInfo readRequest( m_pPlaybackParams, m_pBuffer, &m_bufDecoded, &m_nDecodedOffset, sizeof( char ) ); return readRequest.GetReadBuffer()->GetChar(); } virtual unsigned char GetUnsignedChar() OVERRIDE { COnTheFlyDemoBufferReadInfo readRequest( m_pPlaybackParams, m_pBuffer, &m_bufDecoded, &m_nDecodedOffset, sizeof( unsigned char ) ); return readRequest.GetReadBuffer()->GetUnsignedChar(); } virtual int GetInt() OVERRIDE { COnTheFlyDemoBufferReadInfo readRequest( m_pPlaybackParams, m_pBuffer, &m_bufDecoded, &m_nDecodedOffset, sizeof( int ) ); return readRequest.GetReadBuffer()->GetInt(); }
virtual void Get( void* pMem, int size ) OVERRIDE { COnTheFlyDemoBufferReadInfo readRequest( m_pPlaybackParams, m_pBuffer, &m_bufDecoded, &m_nDecodedOffset, size ); readRequest.GetReadBuffer()->Get( pMem, size ); } virtual void Put( const void* pMem, int size ) { m_pBuffer->Put( pMem, size );
#ifndef DEDICATED
if ( ( size > 0 ) && ( m_pBuffer->TellPut() > 0 ) && ( ( ( ( char* ) m_pBuffer->PeekPut() ) - ( ( char * ) m_pBuffer->Base() ) ) > ( ( GetBaseLocalClient().IsActive() && GetBaseLocalClient().ishltv ) ? 2048 : DISK_DEMO_BUFFER_FLUSH_CONSIDER_SIZE ) ) ) { // Periodically try to flush the demo buffer to disk
m_pBuffer->TryFlushToFile( DISK_DEMO_BUFFER_FLUSH_TODISK_SIZE ); } #endif
}
virtual bool IsValid() const { return m_pBuffer && m_pBuffer->IsValid(); } virtual bool IsInitialized() const { return IsValid() && m_pBuffer->IsOpen(); }
inline CUtlBuffer::SeekType_t GetSeekType( bool bAbsolute ) { return bAbsolute ? CUtlBuffer::SEEK_HEAD : CUtlBuffer::SEEK_CURRENT; }
// Change where I'm writing (put)/reading (get)
virtual void SeekPut( bool bAbsolute, int offset ) { m_pBuffer->SeekPut( GetSeekType( bAbsolute ), offset ); } virtual void SeekGet( bool bAbsolute, int offset ) { m_pBuffer->SeekGet( GetSeekType( bAbsolute ), offset ); }
// Where am I writing (put)/reading (get)?
virtual int TellPut( ) const { return m_pBuffer->TellPut(); } virtual int TellGet( ) const { return m_pBuffer->TellGet(); }
virtual int TellMaxPut( ) const { return m_pBuffer->TellMaxPut(); }
virtual void UpdateStartTick( int& nStartTick ) const {} virtual void DumpToFile( char const* pFilename, const demoheader_t &header ) const {}
private: CUtlStreamBuffer *m_pBuffer; CUtlBuffer m_bufDecoded; int m_nDecodedOffset; CDemoPlaybackParameters_t const *m_pPlaybackParams;
class COnTheFlyDemoBufferReadInfo { public: COnTheFlyDemoBufferReadInfo( CDemoPlaybackParameters_t const *pPlaybackParams, CUtlBuffer *pRawData, CUtlBuffer *pDecodeCache, int *pDecodedOffset, int numBytesRequired ) { m_nReadFromBufferOriginalSeekPos = 0; #ifndef DEDICATED
if ( pPlaybackParams ) { // Read from the nearest 16-byte aligned location
int nOriginalGet = pRawData->TellGet(); if ( ( (*pDecodedOffset) < 0 ) || // nothing decoded
( nOriginalGet < (*pDecodedOffset) ) || // reading earlier
( nOriginalGet + numBytesRequired > (*pDecodedOffset) + pDecodeCache->TellPut() ) ) // could read beyond decoded buffer
{ int nNearestAlignedLocation = nOriginalGet &~0xF; int blockRead = ( nOriginalGet + numBytesRequired - nNearestAlignedLocation + 0xF )&~0xF; blockRead = MAX( 1024, blockRead ); // decrypt chunks of 1K bytes at a time
*pDecodedOffset = nNearestAlignedLocation; pDecodeCache->EnsureCapacity( blockRead ); int numBytesSeekBack = nOriginalGet - nNearestAlignedLocation; if ( numBytesSeekBack ) pRawData->SeekGet( pRawData->SEEK_CURRENT, - numBytesSeekBack ); // seek back
int numBytes = MIN( blockRead, pRawData->TellMaxPut() - nNearestAlignedLocation ); pRawData->Get( pDecodeCache->Base(), numBytes ); m_nReadFromBufferOriginalSeekPos += -numBytes+numBytesSeekBack; pDecodeCache->SeekPut( pDecodeCache->SEEK_HEAD, numBytes );
// Decode the chunk
extern IBaseClientDLL *g_ClientDLL; g_ClientDLL->PrepareSignedEvidenceData( pDecodeCache->Base(), numBytes, pPlaybackParams ); }
int nSeekInDecodedBuffer = nOriginalGet - *pDecodedOffset; pDecodeCache->SeekGet( pDecodeCache->SEEK_HEAD, nSeekInDecodedBuffer );
//
// Set the read state
//
m_pReadFromBuffer = pDecodeCache; m_pSeekSyncBuffer = pRawData; m_nReadFromBufferOriginalSeekPos += -nSeekInDecodedBuffer;
return; } #endif
//
// Read raw state
//
m_pReadFromBuffer = pRawData; m_pSeekSyncBuffer = NULL; } ~COnTheFlyDemoBufferReadInfo() { if ( m_pSeekSyncBuffer && m_pReadFromBuffer ) m_pSeekSyncBuffer->SeekGet( m_pSeekSyncBuffer->SEEK_CURRENT, m_pReadFromBuffer->TellGet() + m_nReadFromBufferOriginalSeekPos ); }
CUtlBuffer * GetReadBuffer() const { return m_pReadFromBuffer; } private: CUtlBuffer *m_pReadFromBuffer; CUtlBuffer *m_pSeekSyncBuffer; int m_nReadFromBufferOriginalSeekPos; }; };
//-----------------------------------------------------------------------------
// Specialty class with overrides for stream buffer
//-----------------------------------------------------------------------------
#if defined( REPLAY_ENABLED )
class CMemoryDemoBuffer : public IDemoBuffer { private: static int const CACHE_SIZE = 1024 * 512;
uint8* m_pDataCache; // Data cache for temporary writing
uint8* m_pWrite; // Current write position (based on m_pDataCache)
int m_nBufferSize; // Total buffer size
int m_nMaxPut; // What's the most I've ever written?
int m_nCurrentTickOffset; // Offset (relative to m_pDataCache) of tick - needed so we can rewrite ticks before dumping to disk
struct DataChunk_t { int nCurrentTickOffset; int nTickcount; int nDeltaTickcount; int nSize; uint8 pData[1]; };
inline DataChunk_t* AllocDataChunk( int nSize, int nCurrentTickOffset ) { Assert( nCurrentTickOffset >= 0 );
int nActualSize = sizeof(DataChunk_t) + nSize - 1; Assert( nActualSize < CACHE_SIZE );
DataChunk_t* pNewFrame = (DataChunk_t*)new uint8[ nActualSize ]; pNewFrame->nSize = nSize; pNewFrame->nCurrentTickOffset = nCurrentTickOffset; pNewFrame->nTickcount = -1;
// TODO: pass in the delta tick - get rid of #include "replay" etc.
pNewFrame->nDeltaTickcount = ( replay && replay->m_MasterClient ) ? replay->m_MasterClient->m_nDeltaTick : -1;
return pNewFrame; }
bool m_bSignonComplete;
DataChunk_t* m_pSignonData;
typedef unsigned short Iterator_t; CUtlLinkedList< DataChunk_t*, Iterator_t > m_lstFrames; // Represents a list of demo frames
inline int GetTickCount() { extern CGlobalVars g_ServerGlobalVariables; return g_ServerGlobalVariables.tickcount; }
void RemoveStaleFrames() { // Don't remove any frames in the midst of a write operation
if ( m_nWriteCount > 0 ) return;
extern ConVar replay_movielength;
#ifdef _DEBUG
int nNumFramesRemoved = 0; #endif
// Here we remove any frames that are beyond the length of the movie.
Iterator_t i = m_lstFrames.Head(); while ( i != m_lstFrames.InvalidIndex() ) { if ( m_lstFrames[ i ]->nTickcount >= GetTickCount() - TIME_TO_TICKS( replay_movielength.GetInt() ) ) break;
m_lstFrames.Remove( i ); i = m_lstFrames.Head(); #ifdef _DEBUG
++nNumFramesRemoved; #endif
}
#ifdef _DEBUG
if ( nNumFramesRemoved > 0 ) { DevMsg( "Replay: Removed %d frames(s) from recording buffer.\n", nNumFramesRemoved ); } #endif
}
public: CMemoryDemoBuffer() : m_nBufferSize( 0 ), m_nMaxPut( 0 ), m_nCurrentTickOffset( -1 ), m_pWrite( NULL ), m_pSignonData( NULL ), m_bSignonComplete( false ) { }
~CMemoryDemoBuffer() { delete [] m_pDataCache; delete m_pSignonData;
// Free all list entries
m_lstFrames.PurgeAndDeleteElements(); }
virtual bool Init( DemoBufferInitParams_t const& params ) { m_pDataCache = new uint8[ CACHE_SIZE ]; m_pWrite = m_pDataCache; return true; }
virtual bool IsInitialized() const { return m_pDataCache != NULL; }
virtual bool IsValid() const { return IsInitialized(); }
virtual void WriteHeader( const void *pData, int nSize ) { // There is no need to write the header until we dump the file to disk.
/*
// NOTE: Byteswap happens in dump
// The header gets written twice, once at demo start, and once at demo stop.
// If this is the first time, just write to the beginning of the cache
if ( !m_bSignonComplete ) { Assert( m_pWrite == m_pDataCache ); // Make sure this is the first thing we're writing
Put( pData, nSize ); } else // Otherwise, write to the beginning of the header
{ AssertValidWritePtr( m_pHeaderData, nSize ); V_memcpy( m_pHeaderData, pData, nSize ); } */ }
virtual void NotifySignonComplete() { Assert( !m_pSignonData );
// Compute size and allocate
int nSize = m_pWrite - m_pDataCache; Assert( nSize >= 0 ); m_pSignonData = AllocDataChunk( nSize, -1 );
// NOTE: No need to set m_pSignonData->nTickcount.
// We're done with signon data, copy it over from the cache
V_memcpy( m_pSignonData->pData, m_pDataCache, nSize );
m_pWrite = NULL; m_bSignonComplete = true; }
virtual void NotifyBeginFrame() { if ( !m_bSignonComplete ) return;
Assert( m_pWrite == 0 ); m_pWrite = m_pDataCache; }
virtual void NotifyEndFrame() { if ( !m_bSignonComplete ) return;
RemoveStaleFrames();
// Allocate a new data chunk
int nSize = m_pWrite - m_pDataCache; Assert( nSize >= 0 ); DataChunk_t* pNewFrame = AllocDataChunk( nSize, m_nCurrentTickOffset );
// Set the time
pNewFrame->nTickcount = GetTickCount();
// Copy data from cache to new frame
V_memcpy( pNewFrame->pData, m_pDataCache, nSize );
// Add new frame to list
m_lstFrames.AddToTail( pNewFrame );
#ifdef _DEBUG
m_pWrite = NULL; #endif
}
// Change where I'm writing (put)/reading (get)
virtual void SeekGet( bool bAbsolute, int offset ) { // Don't call this.
Assert( 0 ); }
virtual void SeekPut( bool bAbsolute, int nOffset ) { // The only time this should get called is if we are about to write the header.
Assert( bAbsolute && nOffset == 0 ); }
// Where am I writing (put)/reading (get)?
virtual int TellPut( ) const { return m_pWrite - m_pDataCache; } virtual int TellGet( ) const { Assert( 0 ); return 0; }
// What's the most I've ever written?
virtual int TellMaxPut( ) const { return m_nMaxPut; }
// Get functions should never get called.
virtual char GetChar() { Assert( 0 ); return 0; } virtual unsigned char GetUnsignedChar() { Assert( 0 ); return 0; } virtual int GetInt() { Assert( 0 ); return 0; } virtual void Get( void* pMem, int size ) { Assert( 0 ); }
virtual void PutChar( char c ) { Put( &c, sizeof( c ) ); } virtual void PutUnsignedChar( unsigned char uc ) { Put( &uc, sizeof( uc ) ); } virtual void PutInt( int i ) { Put( &i, sizeof( i ) ); } virtual void Put( const void* pMem, int nSize ) { Assert( m_pWrite - m_pDataCache + nSize < CACHE_SIZE ); V_memcpy( m_pWrite, pMem, nSize ); m_pWrite += nSize; m_nBufferSize += nSize; m_nMaxPut = MAX( m_nMaxPut, nSize ); }
virtual void WriteTick( int nTick ) { // Cache the relative position of the tick in memory for the given frame
m_nCurrentTickOffset = m_pWrite - m_pDataCache;
// Write the tick
PutInt( nTick ); }
//
// For thread safety - this counter keeps us from removing stale frames while writing a .dem file.
// The idea here is that if the counter is anything but zero we should not remove stale frames.
// We increment before creating a new job, and decrement from within that job, at the end. We
// pass an iterator as an argument into the job's constructor so we know where to stop iterating
// across the frame list.
//
mutable CInterlockedIntT<int> m_nWriteCount;
//
// Threaded .dem file write
//
class CDemWriteJob : public CJob { public: CDemWriteJob( const CMemoryDemoBuffer *pDemobuffer, const char *pFilename, Iterator_t itTail, const demoheader_t &header ) : m_pDemobuffer( pDemobuffer ), m_pFilename( pFilename ), m_itTail( itTail ), m_Header( header ) { Assert( pFilename && pFilename[0] ); }
virtual JobStatus_t DoExecute() { // TODO: Does it make sense to return JOB_OK here even on failure?
JobStatus_t nResult = JOB_OK;
if ( m_pFilename && m_pFilename[0] != '\0' ) { // Open the file
CUtlStreamBuffer buf( m_pFilename, NULL ); if ( !buf.IsOpen() ) { Warning( "demobuffer: Failed to open file for writing, %s\n", m_pFilename ); } else { // NOTE: We include the sync tick frame as part of our signon data, which makes the header signon length vary by 6 bytes
// (2 chars and 1 int) from our own signon data size.
const int nTickSyncFrameSize = 6; Assert( m_pDemobuffer->m_pSignonData->nSize == m_Header.signonlength + nTickSyncFrameSize );
// Compute adjusted time/ticks/frames, since we may have removed stale frames
const CUtlLinkedList< CMemoryDemoBuffer::DataChunk_t*, CMemoryDemoBuffer::Iterator_t > &lstFrames = m_pDemobuffer->m_lstFrames; Iterator_t itHead = lstFrames.Head(); Iterator_t itTail = lstFrames.Tail();
Assert( itHead != lstFrames.InvalidIndex() ); Assert( itTail != lstFrames.InvalidIndex() );
const DataChunk_t *pHead = lstFrames.Element( itHead ); const DataChunk_t *pTail = lstFrames.Element( itTail );
demoheader_t littleEndianHeader = m_Header; littleEndianHeader.playback_time = TICKS_TO_TIME( pTail->nTickcount - pHead->nTickcount ); littleEndianHeader.playback_ticks = pTail->nTickcount - pHead->nTickcount; littleEndianHeader.playback_frames = lstFrames.Count();
// Byteswap
ByteSwap_demoheader_t( littleEndianHeader );
// Write header
buf.Put( &littleEndianHeader, sizeof( littleEndianHeader ) );
// Write signon data
AssertValidReadPtr( m_pDemobuffer->m_pSignonData ); buf.Put( m_pDemobuffer->m_pSignonData->pData, m_pDemobuffer->m_pSignonData->nSize );
#if 1
Iterator_t itStart = m_pDemobuffer->m_lstFrames.Head(); #else
// TEST: Skip the first one
Iterator_t itStart = m_pDemobuffer->m_lstFrames.Next( m_lstFrames.Head() ); #endif
// Get first recording tick (NOTE: not start global tick). Recording ticks start at 0 but
// when we remove stale frames the first recording tick becomes greater than zero. We use
// nStartTick here to shift all frame recording ticks down nStartTick ticks.
int nStartTick; if ( itHead != m_pDemobuffer->m_lstFrames.InvalidIndex() ) { DataChunk_t *pFrame = m_pDemobuffer->m_lstFrames[ itHead ]; int *pTickData = reinterpret_cast< int * >( pFrame->pData + pFrame->nCurrentTickOffset ); V_memcpy( &nStartTick, pTickData, sizeof( int ) ); }
// Write frames
for ( Iterator_t i = itStart; i != m_pDemobuffer->m_lstFrames.InvalidIndex(); i = m_pDemobuffer->m_lstFrames.Next( i ) ) { DataChunk_t *pFrame = m_pDemobuffer->m_lstFrames[ i ]; Assert( pFrame->nSize >= 0 );
// Overwrite the tick for the given frame if one exists
if ( nStartTick > 0 && pFrame->nCurrentTickOffset >= 0 ) { int nNewTick; int *pTickData = reinterpret_cast< int * >( pFrame->pData + pFrame->nCurrentTickOffset ); // Copy tick from buffer to nNewTick
V_memcpy( &nNewTick, pTickData, sizeof( int ) );
// Subtract out start tick
nNewTick -= nStartTick;
// Copy back to buffer
V_memcpy( pTickData, &nNewTick, sizeof( int ) ); }
buf.Put( pFrame->pData, pFrame->nSize ); }
// Write dem_stop cmd
buf.PutUnsignedChar( dem_stop ); buf.PutInt( m_pDemobuffer->m_lstFrames.Element( m_pDemobuffer->m_lstFrames.Tail() )->nTickcount ); buf.PutChar( 0 );
buf.Close(); } }
// Decrement the write counter
m_pDemobuffer->m_nWriteCount--;
return nResult; }
private: const CMemoryDemoBuffer *m_pDemobuffer; const char *m_pFilename; Iterator_t m_itTail; const demoheader_t &m_Header; };
virtual void UpdateStartTick( int& nStartTick ) const { Iterator_t itHead = m_lstFrames.Head(); if ( itHead == m_lstFrames.InvalidIndex() ) return;
nStartTick = m_lstFrames[ itHead ]->nTickcount; }
virtual void DumpToFile( const char *pFilename, const demoheader_t &header ) const { Assert( !IsX360() ); // TODO: Not supporting 360 yet. Need alternate thread pool setup to do so.
// HACK:
int n = m_nWriteCount;
// Critical section
m_nWriteCount++;
// Start a new thread
CDemWriteJob* pJob = new CDemWriteJob( this, pFilename, m_lstFrames.Tail(), header ); g_pThreadPool->AddJob( pJob );
while ( m_nWriteCount > n ) DevMsg( "Waiting for file dump to complete\n" ); } }; #endif
//-----------------------------------------------------------------------------
// Factory function
//-----------------------------------------------------------------------------
IDemoBuffer *CreateDemoBuffer( bool bMemoryBuffer, const DemoBufferInitParams_t& params ) { IDemoBuffer *pRet; #if defined( REPLAY_ENABLED )
if ( bMemoryBuffer ) { pRet = static_cast< IDemoBuffer* >( new CMemoryDemoBuffer() ); } else #endif
{ pRet = static_cast< IDemoBuffer* >( new CDiskDemoBuffer() ); }
if ( !pRet->Init( params ) ) { delete pRet; return NULL; }
return pRet; }
|