Friday, August 5, 2011

Shit Just Got Real

Determinism

A fancy word that gets you a free drink at the nerd bar. Basically in the realm of programming we have a need for knowing the where and when of everything. Its kind of like having a Heisenberg compensator for real. If we know the where and when of things in a game it helps us keep all things in sync and makes the engineer a happy person. This is extremely helpful for network games, if I shoot you in the face on my machine, we want you to see the same fantastic blood spatter in your living room. If things are out of sync I might enjoy your exploding head but you are busy mining gold or some such silly online nonsense.

I give you my determinism manager, though not really a manager it sounds fancier. It gives you the ability to test determinism for just about anything your heart desires, all through the use of a few clever macros. Check that out. stay in sync. shoot in face. win for everyone.

///////////////////////////////////////////////////////////////////////////////
//
//
// Determinism Manager
// A Utility collection for testing system determinism
// Carson Fee 6/6/09
///////////////////////////////////////////////////////////////////////////////

// The macros provide an easy way to add and remove the testing code.
// They also enable us to get location information
#if defined(TEST_DETERMINISM)
#define DM_PRINT_SYNC(args) DeterminismManager::GetInstance().PrintArgs( __LINE__, __FILE__ , args ));
#define DM_DUMP_CALLSTACK() DumpCallStack(); // the engineer can add these to code before a variable is modified in a set
//function to see who all the modifiers are and to see who changes it just before the desync occurs
#define DM_HASH_TEST(pData) DeterminismManager::GetInstance().HashTest(pData, sizeof(pData), #pData, __LINE__, __FILE__);
#define DM_HASH_TEST_BLOCK(pData, nSize) DeterminismManager::GetInstance().HashTest(pData, nSize, #pData, __LINE__, __FILE__);
#else
#define DM_PRINT_SYNC(args)
#define DM_DUMP_CALLSTACK()
#define DM_HASH_TEST(pData)
#define DM_HASH_TEST_BLOCK(pData, nSize)
#endif


class DeterminismManager: public Singleton

{
// we may want two structures, one just for hashing and only throwing an assert
// and another like below which is much heavier, slower to generate, using more memory
// but is more helpful.
struct ComparisonData
{
uint32_t nFrame;
uint32_t nHash;
char aVariable[32];
char aString[128];
char aLocation[128];
};

// Start recording the hash values and any other state data we want to test against
void BeginRecord()
{
m_nCurrentFrame = 0;
m_eState = STATE_RECORDING;
}

// Set up indexes so we can start testing from the start of a frame and comparing
// the results generated from the replay with those generated from the frame
void BeginTest()
{
m_nCurrentFrame = 0;
m_nCurrentItem = 0;
m_eState = STATE_TEST;
}

// Stop testing/recording
void Stop()
{
m_eState = STATE_IDLE;
}

// increases the frame count, used to ensure we have the correct data a the correct time
void TickFrame()
{
m_nCurrentFrame++;
}

// During recording this function will store a copy of the string provided and generat a hash
// During testing the hashes will be compared. If their is a difference both strings will be displayed
// for comparison and an indication of failure will be given.
// If they are the same, the string will be displayed with an indiation of success
void PrintArgs(char* pVariable, char* pFile, char* pLine, char* pFormat, ...)
{
// we will put our string into buffer.
////////////////////////////////////////
// Formatting the string to store and hash
// it will be displayed on success and failure
const uint32_t BUFFER_SIZE = 128;
char buffer[BUFFER_SIZE] = {0};

va_list argList;
va_start( argList, pFormat );

int length = vsnprintf( buffer, BUFFER_SIZE-1, pFormat, argList );
/////////////////////////////////////////

if(m_eState == STATE_RECORDING)
{
AddHash(buffer, strlen(buffer), pVariable, pFile, pLine);
}
else if(m_eState == STATE_TEST)
{
// cache the current itme as compare hash has to
int nCurrentItem = m_nCurrentItem;
if(CompareHash(buffer, strlen(buffer))
{
// If the hashes match, just display the data
printf("OK:%s : %s\n", buffer, m_hashValues[nCurrentItem].aLocation);
}
else
{
// If the data does not match, so we are out of sync, display the original
// data and the recently generated data is incorrect
printf("OK :%s : %s\n", buffer, m_hashValues[nCurrentItem].aLocation);
printf("FAIL:%s : %s\n", m_hashValues[nCurrentItem].aString, m_hashValues[nCurrentItem].aLocation);
}
}
}


// This function hides whether the system is running in record or test mode
// In record mode it adds hashes the variables and records that data in a local table
// In test mode it hashes the same game data as during the record phase and compares this with
// the data generated during the record mode
void HashTest(void* pData, uint32_t nSize, char* pVariable, char* pFile, char* pLine)
{
if(m_eState == STATE_RECORDING)
{
AddHash(pData, nSize, pVariable, pFile, pLine);
}
else if(m_eState == STATE_TEST)
{
DumpCallStack(); // Code for this already exists
ASSERT_F(CompareHash(pData, nSize), ("The current state of the system has deviated from that recorded."));
}
}

// Generates a hash from the pointer provided and records location information
void AddHash(void* pData, uint32_t nSize, char* pVariable, char* pFile, char* pLine, char* pString=0)
{
ComparisonData& current = m_hashValues.Add();
current.nFrame = m_nCurrentFrame;
current.nHash = HashMem(pData, nSize);
// check length of strings , valid poitners etc
sprintf(current.location, "%s:%s", pFile, pLine);
sprintf(current.variable, "%s", pVariable);

// This is only used if we are using print args
if(pString)
{
sprintf(current.aString, "%s", pString);
}
}

// this function checks our recorded hashes and compares them to a hash of the current data
// This is only usd during Test mode
bool CompareHash(void pData, uint32_t nSize)
{
if(m_nCurrentItem < m_hashValues.Size()) // check we are still in a valid range of test items { ComparisonData& currentFrame = m_hashValues[m_nCurrentItem]; if(currentFrame.nFrame == m_nCurrentFrame) // confirm we are on the correct frame { if(currentFrame.nHash == HashMem(pData, nSize)) // does our recorded data match the current state of the recorded data { m_nCurrentItem++; return true; } } printf("VAR:%s, FILE:%s\n", currentFrame.variable, currentFrame.location); } return false; } enum eState { STATE_RECORDING = 0, STATE_IDLE, STATE_TEST; }; Array
m_hashValues; // This should be something like ChunkedFileReplayBuffer
uint32_t m_nCurrentFrame;
uint32_t m_nCurrentItem;
eState m_eState;
};

///////////////////////////////////////////////////////////////////////////////
//
//
// Example use of Determinism manager
//
//
///////////////////////////////////////////////////////////////////////////////
// I'd like to be able to do this in responce to a tick event, but I think we will have issues with the
// ReplayManager using the PreTick during playback. This shouldbe examined.
// The solution may be to put this into the Logic::Poll function before the input and the system Tick has been performed.
MainLoop::Poll()
{
...
DeterminismManager::GetInstance().TickFrame();
...
}

// Here we are generating a hash of the 16 bytes of memory from the address of m_msgFlags onwards during recording and saving that in our manager
// On playback we will generate the hash again and compare it to the recorded one. If it does not match we have a determinism problem

Entity::HandleEvents()
{
...
if(msg.id == iMsgRunningTick)
{
HASH_TEST(&m_transform);
}
...
}

// We have to use the replay manager(or Juice) to ensure we have an EXACT copy of the actions done the first time are performed.
ReplayManager::GetInstance().Record()
{
DeterminismManager::BeginRecord();
}

// We have to use the replay manager(or Juice) to ensure we have an EXACT copy of the actions done the first time are performed.
ReplayManager::GetInstance().Playback()
{
DeterminismManager::BeginTest();
}