Sponsored By

Implementing Dataports

In this technical feature, Sony Computer Entertainment's Martin Linklater (Wipeout: Pure) explains his process for utilizing dataports, a data structure which has has a unique global identity at runtime.

Martin Linklater, Blogger

November 16, 2006

12 Min Read

Introduction

As game programming projects grow in size, more care must be taken regarding code and data dependencies between code modules. Without vigilant code construction, class and header file dependencies can spiral out of control, making your project both unwieldy to navigate and slow to compile. The current trend to more data driven game construction techniques and post-release downloaded content can also create extra complexities in code design. Dataports are designed to simplify code and make the data flows between code modules more dynamic.

The concept behind Dataports is very simple – data is bound at runtime rather than at compile time. Their implementation is lightweight and simple to integrate into existing code. First I will go through some Dataports concepts then finish off with example code.

Conceptual Overview

Essentially, a Dataport is a data structure which has has a unique global identity at runtime. Once created, a Dataport registers itself with the Dataport Manager. The Dataport Manager keeps a central repository of registered Dataports, and is the glue that binds Dataports with Dataport pointers. Once the Dataport has been registered, Dataport pointers can request to be connected to a given Dataport via the Dataport Manager.

Dataport pointers attach to and detach from Dataports at runtime, meaning that the dataflow between code modules can be dynamically manipulated. In the following simple example the class 'foo' creates and registers a dataport of type int. Then the class 'bar' attaches and detaches from the dataport. Although in this small example Dataports have been used to connect an integer with an integer pointer, Dataports are usually used to connect user structures or classes to their corresponding pointers.

//------------------------------------
// foo.h – The file with the dataport.
//------------------------------------

Class foo {
foo( char* strID );
~foo();
protected:
// Declare the Dataport
Dataport m_intValue;
};

//------------------------------------
// foo.cpp
//------------------------------------

foo::foo( char* strID)
{
// Register the Dataport at object construction
m_intValue.Register( strID );
}

foo::~foo()
{
// Deregister the Dataport on object destruction
m_intValue.DeRegister();
}

//--------------------------------------------
// bar.h – The file with the dataport pointer.
//--------------------------------------------

Class bar {
public:
void GetData( void );
void ReleaseData( void );

Dataport *m_pData;
};

//--------------------------------------------
// bar.cpp
//--------------------------------------------

void bar::AttachToData( void )
{
m_pData = m_pData->Attach( “my ID” );
}

void bar::DetachFromData( void )
{
m_pData->Detach();
}

//--------------------------------------------
// main.cpp
//--------------------------------------------

main()
{
bar myBar;

foo* pFoo = new foo( "myID" );

myBar.AttachToData( "myID" );

int value = myBar.m_pData->data;

myBar.DetachFromData();

delete pFoo;
}

The Dataport Manager

The Dataport Manager implements the storage and searching mechanisms behind Dataport runtime behavior. The Dataport Manager consists of a hashing function and an array of currently registered Dataports. I'm not going to go into the details of the hashing function here – suffice to say that it needs to be able to hash a string into an integer.

The Dataport Manager needs to store a list of currently registered Dataports, and perform searches on that list. The implementation of this store is down to programmer choice, which in turn depends on the runtime characteristics of Dataport usage. The frequency of insertion, deletion and searching needs to be taken into account when deciding which data structure to use. In my example code I use a simple static array, but you can use linked lists or STL if you so wish.

Type Safety

Type safety can be added to Dataports by implimenting a type specific 'TypeID' function as part of the Dataport registration method. This ID generation can be automated by using templates:

template FUNCPOINTER_TYPE GetID()
{
typedef FUNCPOINTER_TYPE (*TempFunc)();
TempFunc FuncPtr = GetID;
FUNCPOINTER_TYPE retID= (FUNCPOINTER_TYPE)FuncPtr;
return retID;
}

This template will return a unique ID for each class type which is passed in as template argument, and can be used in the Dataport Manager to ensure type safety.

Reference Counting

Reference counting can be added to the Dataport Manager to track the binding of data to pointer. With data being bound at run time it can be a useful debugging aid to track how many pointers are pointing at an instance of data, and only allow the data to be de-registered once all pointers have been detached.

Practical Examples

The camera system for both Quantum Redshift and Wipeout Pure used Dataports for connecting cameras with tripods. Each potential camera position was defined by a tripod Dataport. This Dataport contained information about position, direction, field of view and other camera related data. At run time the camera could switch between possible tripods simply by detaching and attaching to the tripod Dataports.


The handling tweak values for the ships in Wipeout Pure were held in dataports. These tweak values could be bound to the different ships at run time. Since Wipeout Pure supported ship downloads, new tweak dataports could be created at run time to support the downloaded ships. These dataports used part of the ship filename as their ID, which allowed the ship physics code to get the tweak values just by knowing the ship download filename.

The data driven nature of dataports lends itself well to binding with data which is not present at game ship time. Extra downloaded content can expose Dataports using it's filename as part of the Dataport ID string. The rest of the game code can attach to this data knowing only the filename of the new data.

Benefits of Using Dataports

Binding of data with data pointers happens at run time, meaning Dataports can simplify the include file hierarchies of your source code. If class A needs access to data within class B, rather than including class B's header file in the dependencies of class A, create a dataport of simple types that both A and B include. Once the data is created, class B registers the Dataport so that class A can request a pointer to the data.

Dataports lend themselves well to data driven construction techniques since data can be distributed around your code modules using simple knowledge like filenames or other sensibly created string identifiers. Binding of data can be driven by the simple mechanism of file enumeration on the storage device.

Dataports which encapsulate reference counting can be used to defend against hanging pointers. The Dataport Manager can be programmed so as to not allow Dataports to be deleted unless their reference count is zero. When the count is zero, all pointers should have been released and not needed anymore.

Problems With Dataports

Hash value collisions - There is a chance that you will get a hash value collision with two different identifying strings. In this instance, either change the string identifier or improve the hashing function. In debug build the Dataport Manager should perform collision tests every time a new Dataport is registered.

Harder to debug - Since data linkage is defined at run time and is dynamic, debugging data flow through through dataports can be difficult. Extra debug information can be added to Dataports in debug builds to help this process, but the debugging is never going to be as simple as using compile time bound data pointers, since the debugger has no symbolic knowledge of Dataport connections.

Processing overhead - Since the Dataport Manager performs hashing whenever pointers are linked to data, there is a CPU overhead. It would be unwise to use Dataports for pointers that change at high frequency.

Future Work

I have been using Dataports for the last 6 years or so and I have found them a very useful pattern for handling dynamically bound data. I'm sure there are plenty of extensions to the Dataport concept out there which I have not thought about. You can alter and expand Dataports to suit your needs very easily.

Code

#include "DataportManager.h"

#define FUNCPOINTER_TYPE unsigned int

template< class T > FUNCPOINTER_TYPE GetID()
{
typedef FUNCPOINTER_TYPE (*TempFunc)();
TempFunc FuncPtr = GetID< T >;
FUNCPOINTER_TYPE retID = (FUNCPOINTER_TYPE)FuncPtr;
return retID;
}

template< class T > class Dataport
{
public:
int Register( char* szName )
{
FUNCPOINTER_TYPE ID = GetID< T >();
int returnVal = gDataportManager.Add( szName, (void*)this, ID );
refCount = 0;
return returnVal;
}

int DeRegister( bool bCheck = true )
{
return gDataportManager.Remove( (void*)this, bCheck );
}

Dataport< T >* Attach( char* szName )
{
FUNCPOINTER_TYPE ID = GetID< T >();
Dataport< T >* returnVal = ( Dataport< T >* )gDataportManager.Attach( szName, ID );
if( returnVal )
returnVal->refCount++;
return returnVal;
}

int Detach( void )
{
int returnVal = gDataportManager.Detach( (void*)this );
if( returnVal == DATAPORT_SUCCESS )
this->refCount--;
return returnVal;
}

int Poll( void )
{
return gDataportManager.Poll( (void*)this );
}

unsigned int GetRefCount( void ){ return refCount; }

T data;

protected:
unsigned int refCount;
};

//========================================
// dataportmanager.h
//========================================

typedef enum {
DATAPORT_SUCCESS = 0,
DATAPORT_ERROR_NAMECLASH,
DATAPORT_ERROR_NOMOREDATAPORTSLOTS,
DATAPORT_WARNING_BEINGREFERENCED,
DATAPORT_ERROR_ENTRYNOTFOUND,
DATAPORT_ERROR_NOTATTACHED,
DATAPORT_ERROR_WRONGTYPE,
DATAPORT_ERROR_REFCOUNTZERO
} DataportReturn;

static const unsigned int MAX_DATAPORTS = 196;

class DataportManager {
public:
DataportReturn Add(char* strName, void* ptr, unsigned int ID);
DataportReturn Remove(void* ptr, bool bCheck = true );
void* Attach(char* szName, unsigned int ID);
DataportReturn Detach(void*);

DataportReturn Poll(void*);

DataportManager();
protected:

void* m_pDataports[ MAX_DATAPORTS ];
unsigned int m_dataportsMangled[ MAX_DATAPORTS ];
unsigned int m_dataportsRefCount[ MAX_DATAPORTS ];
unsigned int m_numDataports;
};

extern DataportManager gDataportManager;

//-------------------------------------
// dataportmanager.cpp
//-------------------------------------

DataportManager gDataportManager;

//--------------------------------------

DataportManager::DataportManager()
{
// clear the dataport pointer array

memset(&m_pDataports, 0, sizeof(void*) * MAX_DATAPORTS);
memset(&m_dataportsMangled, 0, sizeof(unsigned int) * MAX_DATAPORTS);
memset(&m_dataportsRefCount, 0, sizeof(unsigned int) * MAX_DATAPORTS);

m_numDataports = 0;
}

//--------------------------------------

DataportReturn
DataportManager::Add( char* strName, void* ptr, unsigned int ID )
{
unsigned int hash = *((unsigned int*)( strName )); // simple hash function

if(m_numDataports < MAX_DATAPORTS)
{
if ( strName != NULL )
ID ^= hash;

// check for clash

for( unsigned int i = 0 ; i < m_numDataports ; i++ )
{
if( m_dataportsMangled[ i ] == ID )
{
return DATAPORT_ERROR_NAMECLASH;
}
}

m_pDataports[ m_numDataports ] = ptr;
m_dataportsMangled[ m_numDataports ] = ID;
m_dataportsRefCount[ m_numDataports ] = 0;

m_numDataports++;

return DATAPORT_SUCCESS;
}
else
{
return DATAPORT_ERROR_NOMOREDATAPORTSLOTS;
}
}

//--------------------------------------

DataportReturn
DataportManager::Remove( void* pIn, bool bCheck )
{
for( unsigned int i = 0 ; i < m_numDataports ; i++ )
{
if( m_pDataports[ i ] == pIn )
{
// found entry

m_numDataports--;
m_dataportsMangled[ i ] = m_dataportsMangled[ m_numDataports ];
m_pDataports[ i ] = m_pDataports[ m_numDataports ];
m_dataportsRefCount[ i ] = m_dataportsRefCount[ m_numDataports ];

m_pDataports[ m_numDataports ] = 0;
m_dataportsMangled[ m_numDataports ] = 0;

if( m_dataportsRefCount[ i ] != 0 )
return DATAPORT_WARNING_BEINGREFERENCED;

return DATAPORT_SUCCESS;
}
}

return DATAPORT_ERROR_ENTRYNOTFOUND;
}

//--------------------------------------

void*
DataportManager::Attach( char* strName, unsigned int ID )
{
if ( strName != NULL )
{
// FwHashedString hash( strName );
unsigned int hash = *((unsigned int*)( strName ));
ID ^= hash;
}

for( unsigned int i = 0 ; i < m_numDataports ; i++ )
{
if( m_dataportsMangled[ i ] == ID )
{
m_dataportsRefCount[ i ]++;
return m_pDataports[ i ];
}
}

return (void*)0;
}
//--------------------------------------

 

DataportReturn
DataportManager::Detach( void* pIn )
{
for( unsigned int i = 0 ; i < m_numDataports ; i++ )
{
if( m_pDataports[ i ] == pIn )
{
// found entry

if( m_dataportsRefCount[ i ] > 0)
{
m_dataportsRefCount[ i ]--;
return DATAPORT_SUCCESS;
}
else
{
return DATAPORT_ERROR_NOTATTACHED;
}
}
}
return DATAPORT_ERROR_ENTRYNOTFOUND;
}

//--------------------------------------

DataportReturn
DataportManager::Poll( void* pIn )
{
for( unsigned int i = 0 ; i < m_numDataports ; i++ )
{
if( m_pDataports[ i ] == pIn )
{
// found entry

if( m_dataportsRefCount[ i ] > 0 )
return DATAPORT_SUCCESS;
else
return DATAPORT_ERROR_REFCOUNTZERO;
}
}
return DATAPORT_ERROR_ENTRYNOTFOUND;
}

Read more about:

Features

About the Author(s)

Martin Linklater

Blogger

Martin Linklater has been a professional games programmer for 13 years. Titles he has worked on include Wipeout Pure, Quantum Redshift, N-Gen Racing, Lifeforce: Tenka, Mary Shelley's Frankenstein and Microcosm. He is currently serving as lead programmer on an unannounced PlayStation 3 title being developed at Sony Studio Liverpool. He can be reached at [email protected].

Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like