Assertions in Game Development, Part 2

In Part 1, I discussed the so-called “skippable” assertion, which is ever popular in game development. This form of the assert macro may detect a precondition failure, display a message, execute a fix-up or bail out, and allow the program to continue to run “with its fingers crossed”. In game development, this is essential, because it allows the content team, who rely on the unfinished game executable to continue working. The drawback is that skippable assertions present their error messages to everyone and anyone, resulting in a very poor signal-to-noise ratio, and therefore end up getting ignored.

I also discussed “tagged” assertions, which attempt to remedy this aimless spamming of error messages by tagging each assert macro with a likely recipient. But this ends up sending too many messages to the author of the libraries. The assert system need to identify the target programmer more accurately.

And that is what we’ll be doing next.

Introducing: “scoped” assertions

Scoped assertions identify the programmer that needs to be notified of a failure not by marking the assert macro itself, but by marking the scope within the assertion fails.

The following code snippet demonstrates the basic idea of the scoped assertion system.

// A basic scoped assert system

#include "stdio.h"
#include "math.h"
#include <stdarg.h>
#include <cstring>

struct ScopeOwner;

ScopeOwner* g_ScopeOwner = NULL;

struct ScopeOwner
{
  ScopeOwner( const char* name )
  {
    m_Name = name;
    m_Next = g_ScopeOwner;
    g_ScopeOwner = this;
  }
  ~ScopeOwner()
  {
    g_ScopeOwner = m_Next;
  }
  const char*  m_Name;
  ScopeOwner*  m_Next;
};

void HandleSkippableAssert( const char* file_name, const char* function_name, int line_number, const char* expr, const char* fmt, ... )
{
  // This is of course just a placeholder for a real
  // assert handler. The real thing will display a
  // graphical interface and offer options such as
  // skip or debug, and include additional useful
  // data such as a call stack.

  va_list args;
  va_start( args, fmt );
  printf( "*** This message is for: %s\n", g_ScopeOwner->m_Name );
  printf( "*** Assertion failed in %s, line %d, function %s\n", file_name, line_number, function_name );
  printf( "*** Expression: %s\n*** Message: ", expr );
  vprintf( fmt, args );
  printf( "\n*** Press return key to continue" );
  getchar();
  va_end( args );
}

#define ASSERT( expression, format, ... )\
(\
  ( ( expression ) ? \
    true :\
    (\
      HandleSkippableAssert( __FILE__, __FUNCTION__, __LINE__, #expression, format, ##__VA_ARGS__ ),\
      false\
    )\
  )\
)

struct Vec3
{
  Vec3( float X, float Y, float Z ) : x( X ), y( Y ), z( Z ) {}
  float x, y, z;
};

Vec3 operator* ( const Vec3& v, float scalar )
{
  return Vec3( v.x * scalar, v.y * scalar, v.z * scalar );
}

float VecLength( const Vec3& v )
{
  return sqrtf( v.x * v.x + v.y * v.y + v.z * v.z );
}

Vec3 VecNormalize( const Vec3& v )
{
  float length = VecLength( v );
  if( ASSERT( length != 0.f, "Vector length must be > 0" ) )
  {
    return v * ( 1.f / length );
  }
  else
  {
    return v;
  }
}

#define ARRAY_SIZE( a ) ( sizeof( a ) / sizeof( *a ) )

void SomeAnimationFunction()
{
  ScopeOwner scope_owner( "tavery" );
  Vec3 v( 1, 2, 3 );
  Vec3 n = VecNormalize( v * 0 );
  printf( "Vec %f,%f,%f is normalized %f,%f,%f\n", v.x, v.y, v.z, n.x, n.y, n.z );
}

int main( int argc, const char * argv[] )
{
  ScopeOwner scope_owner( "rpieket" );
  SomeAnimationFunction();
  Vec3 my_vecs[] =
  {
    Vec3( 1, 2, 3 ),
    Vec3( 4, 5, 6 ),
    Vec3( 0, 0, 0 ),
    Vec3( 7, 8, 9 ),
  };
  for( int i = 0; i < ARRAY_SIZE( my_vecs ); ++i )
  {
    Vec3 v = my_vecs[ i ];
    Vec3 n = VecNormalize( v );
    printf( "Vec %f,%f,%f is normalized %f,%f,%f\n", v.x, v.y, v.z, n.x, n.y, n.z );
  }
  return 0;
}

In every function that needs to set the scope, a ScopeOwner object is declared. The constructor and destructor automatically link add/remove themselves into/from the list. Note that it is not necessary to create a ScopeOwner object in every single function in the code. That would be crazy! Rather the scope only needs to be declared at the main entry points of each subsystem’s API. In other words, only in your public functions.

In this example, the same assertion macro fails twice: once inside the main() function scope, and once inside the nested SomeAnimationFunction() scope, which, we will pretend, is part of the animation subsystem. The assertion failure in the call to VecNormalize() from the main() scope will be attributed to “rpieket”. The assertion failure in the call to VecNormalize() from SomeAnimationFunction() will automatically be attributed to the animation system programmer “tavery”.

With this information, the assertion handler can forward the message to an engineer with greater accuracy. With some ingenuity (not shown) each error message will end up in the network folder, inbox, or database page of the engineer most likely interested in this particular piece of information.

Of course this demonstrates the bare bones of the scoped assertion idea. An industry-strength solution will make the assertion message much more complete (with a call stack, a memory map, and all kinds of lovely debugging data), store it in a searchable database with a long message history, alert engineers of new occurrences, and so on.

And remember, fellow programmers: failure is not optional. It is an integral part of software development. How well your software fails is an important factor in development success.