Hit & Run 2003

Hello!

Today I would like to discuss the memory management system in the game The Simpsons: Hit & Run 2003. The article will consist of two parts, the first of which will discuss the use of such a memory management system itself, and the second will tell about the internal structure of this system.

Memory management in this game is represented by special allocators. Allocator is a class that implements the details of distribution and release of computer memory resources. That is, some allocator can allocate memory through a regular malloc, with logging of the allocated information, and another allocator can pre-allocate n-th number of bytes for its storage, from which memory will subsequently be requested.

Part 1

Types of allocators

A full list of all types of allocators present in the game can be found in the srrmemory.h file. There it is presented as an enumeration:

enum GameMemoryAllocator
{                                              // 
    GMA_DEFAULT = RADMEMORY_ALLOC_DEFAULT,     //    0         
    GMA_TEMP = RADMEMORY_ALLOC_TEMP,           //    1         
#ifdef RAD_GAMECUBE                                            
    GMA_GC_VMM = RADMEMORY_ALLOC_VMM,          //    2         
#endif                                                         
    GMA_PERSISTENT = 3,                        //    3         
    GMA_LEVEL,                                 //    4          
    GMA_LEVEL_MOVIE,                           //    5         
    GMA_LEVEL_FE,                              //    6         
    GMA_LEVEL_ZONE,                            //    7         
    GMA_LEVEL_OTHER,                           //    8         
    GMA_LEVEL_HUD,                             //    9         
    GMA_LEVEL_MISSION,                         //    10        
    GMA_LEVEL_AUDIO,                           //    11        
    GMA_DEBUG,                                 //    12        
    GMA_SPECIAL,                               //    13        
    GMA_MUSIC,                                 //    14        
    GMA_AUDIO_PERSISTENT,                      //    15        
    GMA_SMALL_ALLOC,                           //    16
#ifdef RAD_XBOX                                
    GMA_XBOX_SOUND_MEMORY,                     //    17           
#endif
#ifdef USE_CHAR_GAG_HEAP
    GMA_CHARS_AND_GAGS,
#else
    GMA_CHARS_AND_GAGS = GMA_LEVEL_OTHER,
#endif
    GMA_ANYWHERE_IN_LEVEL = 25,                //    25        
    GMA_ANYWHERE_IN_FE,                        //    26        
    GMA_EITHER_OTHER_OR_ZONE,                  //    27     

    GMA_ALLOCATOR_SEARCH = ALLOCATOR_SEARCH,   //   If you feel like using this one, see an example in FMVPlayer::LoadData
    NUM_GAME_MEMORY_ALLOCATORS
};

Almost every element of this enum has its own separate allocator, but there are no separate stores for GMA_ALLOCATOR_SEARCH, GMA_ANYWHERE_IN_LEVEL, GMA_ANYWHERE_IN_FE, and GMA_EITHER_OTHER_OR_ZONE.

Almost every such allocator is presented for a separate type of information, be it music (GMA_MUSIC), level-related information (GMA_LEVEL), and so on.

Usage

Let's consider the use of such a memory management system using an example from the avatar.cpp file:

        HeapMgr()->PushHeap (GMA_LEVEL_OTHER);

        mpVehicleMappable = new VehicleMappable;
        mpVehicleMappable->AddRef();

#ifdef RAD_PS2
        mpVehicleMappableUSB0 = new VehicleMappable;
        mpVehicleMappableUSB0->AddRef();

        mpVehicleMappableUSB1 = new VehicleMappable;
        mpVehicleMappableUSB1->AddRef();
#endif
        
        mpHumanVehicleController = new HumanVehicleController;
        mpHumanVehicleController->AddRef();
        
        mpInCarCharacterMappable = new InCarCharacterMappable;
        mpInCarCharacterMappable->AddRef();

        mpBipedCharacterMappable = new BipedCharacterMappable;
        mpBipedCharacterMappable->AddRef();

        mpCameraRelativeCharacterController = new CameraRelativeCharacterController;
        mpCameraRelativeCharacterController->AddRef();

        HeapMgr()->PopHeap (GMA_LEVEL_OTHER);

At the beginning, there is a call to the PushHeap method, to which we pass the type of allocator from which we will request memory. Then comes the memory allocation itself through a call to the overloaded operator new. And at the end, there is a call to the PopHeap method, to which we must pass the type of allocator from which the memory allocation occurred.

Let's analyze this code in order:

HeapManager

HeapManager is a singleton class intermediary between the HeapStack class and the user (HeapManager stores an instance of the HeapStack class in its fields). In turn, HeapStack is a regular stack (data structure) that stores elements of the GameMemoryAllocator type.

The PushHeap method is a call to the Push method on a HeapStack instance, which in turn simply pushes the passed GameMemoryAllocator object onto the top of the stack.

The PopHeap method is a call to the Pop method on a HeapStack instance, which removes the object at the top of the stack.

Hidden text

Initialization of allocators occurs in the PrepareHeapsStartup, PrepareHeapsFeSetup, PrepareHeapsInGame and PrepareHeapsSuperSprint methods.

Operator new

void* operator new( size_t size )
#ifdef RAD_PS2
#ifndef RAD_MW
throw( std::bad_alloc ) // может бросить исключение std::bad_alloc
#endif
#endif
{
    if( gMemorySystemInitialized == false )
    {
        INIT_MEM();
    }

    void* pMemory;

    if (g_NoHeapRoute)
    {
        pMemory = radMemoryAlloc( 0, size );
    }
    else
    {
        GameMemoryAllocator curr = HeapMgr()->GetCurrentHeap();
        pMemory = AllocateThis( curr, size );

#ifdef MEMORYTRACKER_ENABLED
        ::radMemoryMonitorIdentifyAllocation (pMemory, HeapMgr()->GetCurrentGroupID ());
#endif
    }


    //MEMTRACK_ALLOC( pMemory, size, 0 );

    return( pMemory );
}

The overloaded new operator has the form:

Let's examine each line of the new operator in order:

INIT_MEM

#define INIT_MEM()  Memory::InitializeMemoryUtilities();PS2Platform::InitializeMemory();

Initialization of the INIT_MEM macro may differ depending on the platform the game is running on. Their implementations are similar, but for simplicity, we will analyze the implementation of this macro for PS2:

void InitializeMemoryUtilities()
{
    static bool alreadyCalled = false;
    if( alreadyCalled )
    {
        return;
    }
    alreadyCalled = true;
#ifdef RAD_PS2
    //this is the largest amount of memory that is free
    g_MaxFreeMemory = GetFreeMemoryProfile();
#endif
}
Memory::InitializeMemoryUtilities() is a function that is called once during the entire program's operation and calculates the maximum amount of free memory on the device (specifically only for PS2):

Hidden text

It is worth considering that the PS2 memory was presented in the form of memory cards.

The GetFreeMemoryProfile function goes through all PS2 memory cards and calculates how much memory is free when the game is launched.

    const int size = 256;
    void* pointers[ size ];
    size_t sizes[ size ];

    int index = 0;
    int i;
    for( i = 0; i < size; i++ )
    {
        pointers[ i ] = NULL;
        sizes[ i ] = 0;
    }

Let's analyze the code of the GetFreeMemoryProfile function in order:

Initialization:

    do
    {
        int lo = 0;
        int hi = 1024*1024*256;
        int pivot;
        void* memory = NULL;
        do
        {
            pivot = ( hi + lo ) / 2;
            if( memory != NULL )
            {
                free( memory );
                memory = NULL;
            }
            memory = malloc( pivot );
            if( memory != NULL )
            {
                lo = pivot;
            }
            else
            {
                memory = malloc( lo );
                hi = pivot;
            }
        } while( ( hi - lo ) > 1 );

        if( ( memory == NULL ) && ( retrys < 2 ) )
        {
            ++retrys;
        }
        else
        {
            sizes[ index ] = lo;
            pointers[ index ] = memory;
            memory = NULL;
            ++index;
        }
    } while( ( pointers[ index - 1 ] != NULL ) && ( index < size ) );

First we create two arrays of 256 elements, where the pointers array will store all the allocated memory during the function's execution, and the sizes array will store the amount of memory that is free for each memory card. [lo; hi]Next comes the main part of the function, namely the calculation of the maximum amount of free memory:

The calculation of the amount of free memory is carried out for each block of 256 MB separately (since in one call of the malloc function the system can allocate memory only from one memory card) using the binary search method. Initially, we set the variables lo = 0, hi = 1024*1024*256 (256 MB) and pivot (the middle of the segment

). Next, we try to allocate a pivot byte using the malloc function and check whether it returned a NULL value (it was not possible to allocate a pivot byte). If malloc returned NULL, then hi is shifted to the middle. Otherwise, lo is shifted to the middle. This continues until lo and hi become close. As a result, the lo value is the maximum amount of free memory in the currently checked memory map.

Then all the obtained lo values ​​are written to the sizes array, and the memory pointer to the allocated memory of size lo is written to the pointers array for further cleaning.

Hidden text

    size_t total = 0;
    for( i = 0; i < size; i++ )
    {
        total += sizes[ i ];
    }

    for( i = 0; i < size; i++ )
    {
        void* pointer = pointers[ i ];
        if( pointer != NULL )
        {
            free( pointer );
            pointers[ i ] = NULL;
        }
        else
        {
            break;
        }
    }

Although the actual maximum size of a single memory card was 8 MB, the developers of The Simpsons Hit & Run set the maximum size of a single memory card to 256 MB. This is because there were Chinese PS2 memory cards on the market at the time that were much larger than 8 MB. The largest Chinese memory card could hold 256 MB.

Summing up all free memory:

Clearing allocated memory:

As a result, the GetFreeMemoryProfile function returns the total variable, which stores the number of bytes of free memory.

Hidden text

The GetFreeMemoryProfile function is called only for PS2 because for other platforms there were special functions for checking how much memory is currently free. For example, for WIN32 there is a GlobalMemoryStatus function, through which you can find out information about the current use of virtual and physical memory. But for PS2 there was no such function.

PS2Platform::InitializeMemory() is a function that calls radMemoryInitialize, thus initializing the memory system (the principle of operation of the radMemoryInitialize function will be discussed later).

g_NoHeapRoute

g_NoHeapRoute is a Boolean variable that indicates whether the allocator system is enabled (false) or not (true). If this variable is true, the radMemoryAlloc function is called, where the GMA_DEFAULT parameter and the size of the memory allocation request in bytes are passed.

That is, if the allocator system is disabled, then all requests for memory allocation will be addressed to the GMA_DEFAULT allocator.

If the variable g_NoHeapRoute is false, then the AllocateThis function is called, into which the allocator type, which is the current top of the stack (HeapStack), as well as the number of bytes requested for allocation, are passed.

inline void* AllocateThis( GameMemoryAllocator allocator, size_t size )
{
    void* pMemory = NULL;
#ifdef CORRAL_SMALL_ALLOCS
    if( size < 201 )
    {
        if( allocator != GMA_AUDIO_PERSISTENT && allocator != GMA_PERSISTENT && allocator != GMA_TEMP)
        {
            pMemory = radMemoryAlloc( GMA_SMALL_ALLOC, size );
        }
        else
        { 
            pMemory = radMemoryAlloc( allocator, size );
        }
    }
    else
#endif
    {
        if ( allocator >= GMA_ANYWHERE_IN_LEVEL && allocator != ALLOCATOR_SEARCH )
        {
            pMemory = FindFreeMemory( allocator, size );
        }
        else
        {
            pMemory = radMemoryAlloc( allocator, size );
        }
    }

    return pMemory;
}

Hidden text

The radMemoryAlloc function is responsible for the memory allocation itself depending on the allocator. It requests n-th number of bytes from some allocator and returns a pointer to the allocated memory.

void* FindFreeMemory( GameMemoryAllocator allocator, size_t size )
{
    GameMemoryAllocator* list = NULL;
    unsigned int numAvailable = 0;

    if ( allocator == GMA_ANYWHERE_IN_LEVEL )
    {
        list = AVAILABLE_FOR_RENT_IN_LEVEL;
        numAvailable = sizeof ( AVAILABLE_FOR_RENT_IN_LEVEL ) / sizeof( GameMemoryAllocator );
    }
    else if ( allocator == GMA_ANYWHERE_IN_FE )
    {
        list = AVAILABLE_FOR_RENT_IN_FE;
        numAvailable = sizeof ( AVAILABLE_FOR_RENT_IN_FE ) / sizeof( GameMemoryAllocator );
    }
    else if ( allocator == GMA_EITHER_OTHER_OR_ZONE )
    {
        list = AVAILABLE_IN_OTHER_OR_ZONE;
        numAvailable = sizeof ( AVAILABLE_IN_OTHER_OR_ZONE ) / sizeof( GameMemoryAllocator );       
    }
    else
    {
        rAssert( false );
    }

    if ( list != NULL )
    {
        ::radMemorySetUsableAllocators( (radMemoryAllocator*)list, numAvailable );
        void* memory = radMemoryAlloc( ALLOCATOR_SEARCH, size );
      
        return memory;
    }

    return NULL;
}

AllocateThis

static GameMemoryAllocator AVAILABLE_FOR_RENT_IN_LEVEL[] =
{
    GMA_TEMP,
    GMA_LEVEL_ZONE,                            //    7         6     6
    GMA_LEVEL_OTHER,                           //    8         7     7
    GMA_LEVEL_MISSION,                         //    10        9     9
    GMA_LEVEL_HUD                              //    9         8     8
};
static GameMemoryAllocator AVAILABLE_FOR_RENT_IN_FE[] =
{
    GMA_LEVEL_MOVIE,                           //    5         4     4
    GMA_LEVEL_FE,                              //    6         5     5
    GMA_LEVEL_AUDIO                            //    11        10    10
};
static GameMemoryAllocator AVAILABLE_IN_OTHER_OR_ZONE[] =
{
    GMA_LEVEL_OTHER,                           //    8         7     7
    GMA_LEVEL_ZONE                             //    7         6     6
};

AllocateThis is a function that requests n-th number of bytes from some allocator and returns a pointer to the allocated memory.

If there is no native allocator for the passed allocator type (allocator >= GMA_ANYWHERE_IN_LEVEL), then the FindFreeMemory function is called. Otherwise, the radMemoryAlloc function is called.

FindFreeMemory

FindFreeMemory is a function that makes a request for memory allocation among some list of allocators.

First, we create a pointer to some list of allocators and a variable that will store the number of elements of this list. Then, depending on the type of allocator passed to the function, the list pointer starts pointing to the beginning of one of three arrays containing allocator types that are somehow related to each other:

Then, the radMemorySetUsableAllocators function is called, loading data about the types of allocators that will participate in memory allocation requests, and finally, the radMemoryAlloc function is called, passing ALLOCATOR_SEARCH as the allocator type.

(1*) When ALLOCATOR_SEARCH is passed to the radMemoryAlloc function as an allocator type, radMemoryAlloc behaves differently: it creates requests for memory allocation not from a single allocator, but from the entire list loaded earlier via the radMemorySetUsableAllocators function. Accordingly, it takes memory from one (or several if the previous ones have already run out of memory) of these allocators.

Hidden text

Typically, you pass an allocator type greater than or equal to GMA_ANYWHERE_IN_LEVEL to the radMemoryAlloc function when you don't care which of the general-type allocators (AVAILABLE_FOR_RENT_IN_LEVEL, AVAILABLE_FOR_RENT_IN_FE, or AVAILABLE_IN_OTHER_OR_ZONE) should allocate memory.

Conclusion of Part 1

struct IRadMemoryAllocator : public IRefCount
{
    virtual void* GetMemory( unsigned int size ) = 0;
	virtual void  FreeMemory( void* pMemory ) = 0;
    virtual bool  CanFreeMemory( void * pMemory ) = 0;

    virtual void* GetMemoryAligned( unsigned int size, unsigned int alignment ) = 0;
	virtual void  FreeMemoryAligned( void * pMemory ) = 0;
    virtual bool  CanFreeMemoryAligned( void * pMemory ) = 0;

	//
	// Memory statistics
	//

    virtual void GetStatus(
		unsigned int * totalFreeMemory,
		unsigned int * largestBlock,
		unsigned int * numberOfObjects,
		unsigned int * highWaterMark );

    virtual unsigned int GetSize( void );
};

Hidden text

There is actually another overload of the new operator that does not interact with HeapManager at all. Usage: new(GMA_PERSISTENT) HeapManager This overload is useful when you only need to allocate memory for one object. The implementation of this new operator is almost identical to the implementation of the new operator discussed above.

In conclusion of the first part, I would like to present the final block diagram of the memory allocation system:

Part 2

IRadMemoryAllocator

IRadMemoryAllocator – base interface for all allocators in the game.

Let's briefly go over all the mandatory methods that allocators must contain:

GetMemory – allocates memory from the allocator and returns a pointer to the allocated memory.

FreeMemory – clears the memory according to the passed pointer.

if( g_Initialized )
    {
        return;
    }

#ifdef RAD_GAMECUBE
    ::radMemoryPlatInitialize( sizeVMMainMemory, sizeVMARAM );
#else
    ::radMemoryPlatInitialize( );
#endif

    //This is memory reserved to really bad situations where we need to printf.
#ifndef RAD_GAMECUBE
    gEmergencyMemory = radMemoryPlatAlloc( 1024 * 32 );
#endif

    rAssert( g_Initialized == false );
    g_Initialized = true;

    g_pRadMemoryAllocator_Malloc = new ( g_MemoryForMalloc ) radMemoryAllocatorMalloc( );

    g_AllocatorTreeNode_Root.m_pIRadMemoryAllocator = g_pRadMemoryAllocator_Malloc;
    g_AllocatorTreeNode_Root.m_pChildren_Head = NULL;
    g_AllocatorTreeNode_Root.m_pSibling_Next = NULL;
    g_AllocatorTreeNode_Root.m_pParent = NULL;

    for( unsigned int i = 0; i < ALLOCATOR_TABLE_SIZE; i ++ )
    {
        g_AllocatorTreeNodes[ i ].m_pChildren_Head = NULL;
        g_AllocatorTreeNodes[ i ].m_pParent = NULL;
        g_AllocatorTreeNodes[ i ].m_pSibling_Next = NULL;
        g_AllocatorTreeNodes[ i ].m_pIRadMemoryAllocator = g_pRadMemoryAllocator_Malloc;
    }

#ifdef RAD_GAMECUBE
    unsigned aramSize = (1024 * 1024 * 16) - sizeVMARAM;
    radMemorySpaceInitialize( aramSize );
    sVMMDLHeapInitialized = false;

    if ((sizeVMMainMemory != 0) && (sizeVMARAM != 0))
    {
        bool ok = VMAlloc( 0x7E000000, sizeVMARAM );
        rAssert( ok );
        vmmHeap = radMemoryCreateDougLeaHeap( (void *)0x7E000000, sizeVMARAM, RADMEMORY_ALLOC_DEFAULT, "GameCube_VMM" );
        rAssert(vmmHeap != NULL);
        radMemoryRegisterAllocator( RADMEMORY_ALLOC_VMM, RADMEMORY_ALLOC_DEFAULT, vmmHeap );
        sVMMDLHeapInitialized = true;
#ifndef RAD_RELEASE
        VMSetLogStatsCallback(&gcnVMMLogStats);
#endif
    }

#else
    radMemorySpaceInitialize( );
#endif

    //
    // Initialize static heap
    //
    //g_StaticHeap.CreateHeap( STATIC_HEAP_SIZE );

CanFreeMemory – checks whether the passed pointer belongs to this allocator (when clearing memory).

struct radMemoryAllocatorTreeNode
{
    IRadMemoryAllocator        * m_pIRadMemoryAllocator; // the allocator pointer
    radMemoryAllocatorTreeNode * m_pChildren_Head; // list of sub allocators
    radMemoryAllocatorTreeNode * m_pSibling_Next;  // list pointer used by parent 
    radMemoryAllocatorTreeNode * m_pParent;        // optomization                 
};

GetMemoryAligned – the same as GetMemory, only when allocating memory, alignment is also taken into account.

    for( unsigned int i = 0; i < ALLOCATOR_TABLE_SIZE; i ++ ) // ALLOCATOR_TABLE_SIZE = 30
    {
        g_AllocatorTreeNodes[ i ].m_pChildren_Head = NULL;
        g_AllocatorTreeNodes[ i ].m_pParent = NULL;
        g_AllocatorTreeNodes[ i ].m_pSibling_Next = NULL;
        g_AllocatorTreeNodes[ i ].m_pIRadMemoryAllocator = g_pRadMemoryAllocator_Malloc;
    }

FreeMemoryAligned – Frees aligned memory according to the passed pointer.

CanFreeMemoryAligned – the same as CanFreeMemory (taking into account alignment).

radMemoryInitialize

radMemoryInitialize – Function that initializes the allocator system (sets the default allocators).

Body of the radMemoryInitialize function:

#ifdef RAD_PS2
    if(     (numberOfBytes<201)
        && gbSmallAllocCreated 
        && (allocator != HACK_AUDIO_PERSISTENT)
        && (allocator != HACK_PERSISTENT)
        && (allocator != RADMEMORY_ALLOC_TEMP)
        )
    {
        allocator = HACK_SMALL_ALLOC;
    }
#endif
#if ( defined RAD_XBOX ) || ( defined RAD_GAMECUBE ) || ( defined RAD_MW )
    if ( !g_Initialized )
    {
        MemoryHackCallback();
    }
#endif
    if ( numberOfBytes == 0 )
    {
        return NULL;
    }

    rAssert( g_Initialized == true );
    rAssert( allocator < ALLOCATOR_TABLE_SIZE || allocator == ALLOCATOR_SEARCH );

    #ifdef RAD_XBOX
        //rAssert( allocator != 2 );
    #endif
    void * pMem;

    if ( allocator == ALLOCATOR_SEARCH )
    {
        pMem = radMemoryAllocSearch( numberOfBytes, 0 );
    }
    else
    {
        IRadMemoryAllocator * pIRadMemoryAllocator =
            g_AllocatorTreeNodes[ allocator ].m_pIRadMemoryAllocator;

        pMem = pIRadMemoryAllocator->GetMemory( numberOfBytes );
        ::radMemoryMonitorIdentifyAllocation ( pMem, g_CurrentMemoryIdentification );
    }

    if (g_MemoryActivityCallback)
    {
        g_MemoryActivityCallback->MemoryAllocated( allocator, pMem, numberOfBytes );
    }

    CheckForOutOfMemory( pMem, numberOfBytes, allocator ); // логи
    LEAK_DETECTION_ADD_ALLOCATION( pMem, numberOfBytes, allocator ); // логи
    return pMem;

g_AllocatorTreeNodes – array of type radMemoryAllocatorTreeNode (the number of elements in g_AllocatorTreeNodes matches the number of elements in the GameMemoryAllocators enumeration)

In the radMemoryInitialize function, the g_AllocatorTreeNodes array is filled with standard values, namely:

Where g_pRadMemoryAllocator_Malloc is an instance of the radMemoryAllocatorMalloc class (radMemoryAllocatorMalloc also inherits from IRadMemoryAllocator).

radMemoryAllocatorMalloc is an allocator that does not have its own storage, meaning it ultimately allocates memory via malloc.

radMemoryAlloc

radMemoryAlloc is a function that is responsible for the memory allocation itself depending on the allocator. It requests n-th number of bytes from the passed allocator and returns a pointer to the allocated memory.

The body of the radMemoryAlloc function:

  • As you can see, if the passed allocator type is not ALLOCATOR_SEARCH, then the allocator under the allocator index (pIRadMemoryAllocator) is taken from the g_AllocatorTreeNodes array, and the GetMemory method is called for the taken pIRadMemoryAllocator, which allocates the required amount of memory. If the passed allocator type is ALLOCATOR_SEARCH, then the radMemoryAllocSearch function is called, the operation of which is no different from that described in (1*).

  • Creating allocators

  • Allocators are created (as noted earlier) in the PrepareHeapsStartup, PrepareHeapsFeSetup, PrepareHeapsInGame and PrepareHeapsSuperSprint methods of the HeapManager class. To create each individual allocator, the CreateHeap function (from the createheap.cpp file) is called, which is responsible for creating the allocator.

IRadMemoryHeap

All GameMemoryAllocators (which have an allocator) are allocators derived from the IRadMemoryHeap class (despite the name Heap, there is no heap as a data structure implemented there).

IRadMemoryHeap is an allocator that supports memory allocation for objects of different sizes. However, the memory allocation mechanism itself may differ in allocators derived from IRadMemoryHeap.

There are three types of Heap allocators: Static Heap, Tracking Heap and Doug Lea Heap:

void CreateHeap ( GameMemoryAllocator allocator, const unsigned int size )
{
    unsigned int index = static_cast< unsigned int >( allocator );
    rAssert( g_HeapArray[ index ] == NULL );

    HeapType type    = g_HeapCreationData[ index ].type;
    const char* name = g_HeapCreationData[ index ].name;
    GameMemoryAllocator parent = g_HeapCreationData[ index ].parent;

    rReleasePrintf ("Creating Heap: %s (%d)\n", name, size );

    #ifdef RAD_RELEASE
    if( type == HEAP_TYPE_TRACKING )
    {
        type = HEAP_TYPE_NONE;
    }
    #endif

    switch( type )
    {
        case HEAP_TYPE_STATIC :
        {
            HeapMgr()->PushHeap( GMA_DEBUG );
            g_HeapArray[ index ] = radMemoryCreateStaticHeap( size, RADMEMORY_ALLOC_DEFAULT, name );
            g_HeapArray[ index ]->AddRef();
            HeapMgr()->PopHeap( GMA_DEBUG );
            break;
        }
        case HEAP_TYPE_TRACKING :
        {
            HeapMgr()->PushHeap( GMA_DEBUG );
            g_HeapArray[ index ] = radMemoryCreateTrackingHeap( size, RADMEMORY_ALLOC_DEFAULT, name );
            HeapMgr()->PopHeap( GMA_DEBUG );
            break;
        }
        case HEAP_TYPE_DOUG_LEA :
        {
            HeapMgr()->PushHeap( GMA_DEBUG );
            g_HeapArray[ index ] = radMemoryCreateDougLeaHeap( size, RADMEMORY_ALLOC_DEFAULT, name );
            g_HeapArray[ index ]->AddRef();
            HeapMgr()->PopHeap( GMA_DEBUG );
            break;
        }
        case HEAP_TYPE_NONE :
        {
            //rAssert( false );
            return;
        }
        default:
        {
            rAssert( false );
            return;
        }
    }
    radMemoryRegisterAllocator( allocator, parent,g_HeapArray[ index ] );
}

Static Heap is a regular Pool allocator, in which memory is allocated for storage in advance, and then memory is taken from this storage. Memory release is not provided for this allocator (i.e. the FreeMemory method in StaticHeap is empty).

struct HeapCreationData
{
    HeapType            type;
    GameMemoryAllocator parent;
    char                name[ 256 ];
};

HeapCreationData g_HeapCreationData[] = 
{
    { HEAP_TYPE_DOUG_LEA, GMA_DEFAULT, "Default"              },      
    { HEAP_TYPE_DOUG_LEA, GMA_DEFAULT, "Temp"                 },      
    { HEAP_TYPE_NONE,     GMA_DEFAULT, "Gamecube VMM"         },      
#ifdef RAD_WIN32
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Persistent"           },  // no static heap for pc
#else
    { HEAP_TYPE_STATIC,   GMA_DEFAULT, "Persistent"           },
#endif // RAD_WIN32
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Level"                },
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Level Movie"          },
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Level FE"             },
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Level Zone"           },
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Level Other"          },
    { HEAP_TYPE_DOUG_LEA, GMA_DEFAULT, "Level Hud"            },
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Level Mission"        },
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Level Audio"          },
    { HEAP_TYPE_NONE,     GMA_DEFAULT, "Debug"                },
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Special"              },
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Music"                },
    { HEAP_TYPE_DOUG_LEA, GMA_DEFAULT, "Audio Persistent"     },
    { HEAP_TYPE_DOUG_LEA, GMA_DEFAULT, "Small Alloc"          },
#ifdef RAD_XBOX
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "XBOX Sound"           },
#endif
#ifdef USE_CHAR_GAG_HEAP
    { HEAP_TYPE_DOUG_LEA, GMA_DEFAULT, "Characters and Gags"  },
#endif
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Anywhere in Level"    },
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Anywhere in FE"       },
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Either Other or Zone" },
    { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Search"               },
    { HEAP_TYPE_NONE,     GMA_DEFAULT, "???"                  },
    { HEAP_TYPE_NONE,     GMA_DEFAULT, "???"                  },
};

Tracking Heap is an allocator that allocates memory via malloc, but it stores information about the allocated memory in its storage for later clearing. Unlike StaticHeap, it can clear the allocated memory.

Doug Lea Heap is an allocator that allocates memory via dlmalloc, but stores information about the allocated memory in its storage for later cleanup. Unlike StaticHeap, it can clean up the allocated memory.

Hidden text

dlmalloc has its pros and cons compared to standard malloc, but its main advantage is that dlmalloc implementation is platform independent (unlike malloc). It is also considered that dlmalloc is more efficient when allocating small amounts of memory (<= 201 bytes) than malloc.

    rReleasePrintf("%s ", pName );
    StaticHeap* pHeap = new ( allocator ) StaticHeap; // выделение памяти через radMemoryAllocatorMalloc
    pHeap->CreateHeap( size );
    return pHeap;

CreateHeap

    m_FreeingAllowed = false;
    m_TotalAllocations = 0;
    m_TotalSize = size;
    m_BasePointer = reinterpret_cast< char* >( radMemoryPlatAlloc( size ) );
    rAssert( m_BasePointer != NULL );
    m_CurrentPointer = m_BasePointer;
    m_End = reinterpret_cast< char* >( reinterpret_cast< size_t >( m_BasePointer ) + size );
    m_Overflow = 0;

    rReleasePrintf("StaticHeap Start: 0x%x, End:0x%x\n", m_BasePointer, m_End );

    #ifdef RADMEMORYMONITOR
    {
        radMemoryMonitorIdentifyAllocation( (void*)m_BasePointer, g_nameFTech, "StaticAllocator::m_StartOfMemory" );
        radMemoryMonitorDeclareSection( (void*)m_BasePointer, m_TotalSize, IRadMemoryMonitor::MemorySectionType_DynamicData );
        char szName[ 128 ];
        sprintf( szName, "[StaticAllocator]" );
        radMemoryMonitorIdentifySection( (void*)m_BasePointer, szName );
    }
    #endif // RADMEMORYMONITOR

CreateHeap function body:

g_HeapCreationData – an array of type HeapCreationData, which contains information about the created allocators, namely: type, parent, name:

At the end, after the allocator is created in the radMemoryCreateStaticHeap, radMemoryCreateTrackingHeap or radMemoryCreateDougLeaHeap functions, the radMemoryRegisterAllocator function is called, which replaces the allocator located at the allocator index in the g_AllocatorTreeNodes array with the allocator we created.

radMemoryCreateStaticHeap

    HeapMgr()->PushHeap (GMA_PERSISTENT);

	//Process any commandline options from the command.txt file
	ProcessCommandLineArgumentsFromFile();

    //
    // Instantiate all the singletons before doing anything else.
    //
    CreateSingletons();

    //
    // Construct the platform object.
    //
    PS2Platform* pPlatform = PS2Platform::CreateInstance();
    rAssert( pPlatform != NULL );

    //
    // Create the game object.
    //
    Game* pGame = Game::CreateInstance( pPlatform );
    rAssert( pGame != NULL );

    //
    // Initialize the game.
    //
    pGame->Initialize();

    HeapMgr()->PopHeap (GMA_PERSISTENT);

As an example, let's consider the creation of a Static Heap.

radMemoryCreateStaticHeap – a function that creates a Pool allocator.

Body of the radMemoryCreateStaticHeap function:

The body of the StaticHeap::CreateHeap function:

As you can see, in the StaticHeap::CreateHeap function, memory for storage is allocated by the radMemoryPlatAlloc function, which is a regular call to the malloc function with additional logging.

Code from file (ps2main.cpp):Therefore, it can be said that it is worth allocating memory via GMA_PERSISTENT mainly for heavy data types, because memory allocations via GMA_PERSISTENT occur much faster than the same allocations via other allocators.ConclusionTo allocate memory for heavy data types, it is better to use Static Heap (GMA_PERSISTENT), for light data types, Doug Lea Heap, and for medium data types, Tracking Heap.I hope this article was useful and interesting for modders and just passers-by.The full source code for The Simpsons: Hit & Run 2003 can be found here:GitHub – FlexodMR/Simpsons: Original source for “The Simpsons: Hit & Run” gamegithub.com

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *