Implementing an ESRAM Memory Manager

Enhanced Static Random Access Memory (ESRAM) is 32 MB of dedicated RAM that can only be directly accessed by the GPU. This memory has extremely high bandwidth, enabling the GPU to read/write data quickly. ESRAM can be used for just about any Direct3D resource, such as shadow maps, buffers, and textures. To get the biggest performance boost out of ESRAM, use it to store data that is accessed most frequently.

The following sections explain how to manage ESRAM effectively on the Xbox One dev kit. The tutorial section describes useful data types and functionality for an ESRAM memory manager.

Kinds of data that can be resident in ESRAM

ESRAM Manager

Choosing what data to store in ESRAM is important. You want to store the most frequently accessed data in ESRAM, and that data will likely change as your game progresses - for example, during level transitions. It is therefore advisable to implement an ESRAM memory manager. There is no pre-defined ESRAM manager; you will need to create your own ESRAM manager for your project and tailor it to your specific needs.

An ESRAM manager works similarly to a traditional memory manager. The ESRAM manager should perform at least the following functions: allocating memory, loading and unloading data from DRAM, and releasing memory that is no longer needed. Listed below are examples of common data types and functions that your ESRAM manager is likely to use.

ESRAMAllocation class - An allocation in ESRAM

ESRAM Allocation: This class defines a contiguous chunk of memory that has been allocated in ESRAM, and a pointer to that memory.

C++

// An allocation in ESRAM
class ESRAMAllocation
{
public:
    ESRAMAllocation() : m_esramPtr(INVALID_PTR), m_beginBlock(INVALID_BLOCK), m_endBlock(INVALID_BLOCK) {}

    BOOL        IsValid() const { return m_beginBlock != INVALID_BLOCK; }
    void        Invalidate() { m_esramPtr = INVALID_PTR; m_beginBlock = m_endBlock = INVALID_BLOCK; }

    ESRAMPtr    m_esramPtr;     // The start location of this allocation in ESRAM
    USHORT      m_beginBlock;   // The first 4K block of this allocation
    USHORT      m_endBlock;     // One past the end, like STL
};  

ESRAMResource class - A resource allocated in ESRAM

ESRAM Resource: This class is essentially a wrapper for the ESRAMAllocation class, with an additional fence value. Fence values are unique ID values assigned to specific GPU or DMA operations. Use fence values to ensure that resources are only used at the appropriate time.

C++

// A resource allocated in ESRAM
class ESRAMResource
{
public:
    ESRAMResource() : m_fence(INVALID_FENCE) { } 

    // The allocation in ESRAM
    ESRAMAllocation                     m_allocation;

    // A fence value for the last DMA or GPU operation that touched this resource. The 
    // resource should not be touched again until this fence has been passed.
    UINT64                              m_fence;
};  

ESRAMTexture class - A texture allocated in ESRAM

ESRAM Texture: A texture allocated in ESRAM. Contains the ESRAM resource, a texture pointer, and various view pointers associated with that texture.

C++

// A texture allocated in ESRAM
class ESRAMTexture
{
public:
    // The texture resource in ESRAM
    XSF::D3DTexture2DPtr                m_spTexture;

    // Views: null if the texture does not support the given View
    XSF::D3DShaderResourceViewPtr       m_spSRV;
    XSF::D3DUnorderedAccessViewPtr      m_spUAV;
    XSF::D3DRenderTargetViewPtr         m_spRTV;
    XSF::D3DDepthStencilViewPtr         m_spDSV;

    ESRAMResource                       m_esramResource;
};  

ESRAMBuffer class - A buffer allocated in ESRAM

ESRAM Buffer: An example of a buffer allocated in ESRAM. Contains the ESRAM resource and a pointer to the buffer.

C++

// A buffer allocated in ESRAM
class ESRAMBuffer
{
public:
    // The buffer resource in ESRAM
    XSF::D3DBufferPtr                   m_spBuffer;

    ESRAMResource                       m_esramResource;
};  

FreeSpace class - An area of free space in ESRAM

Free Space: Most memory managers allocate large chunks of memory at once, and then subdivide that chunk into smaller pieces for assignment. This approach is considerably faster, but tends to produce small unused chunks of memory as a result. Use the FreeSpace class to keep track of any chunks of unused memory. It is advisable, but not required, to allocate all ESRAM as a single FreeSpace during initialization of your ESRAM manager.

C++

// An area of free space in ESRAM
class FreeSpace
{
public:
    FreeSpace(USHORT beginBlock, USHORT endBlock) : m_beginBlock(beginBlock), m_endBlock(endBlock) {}

    USHORT m_beginBlock;
    USHORT m_endBlock;   // One past the end, like STL
};  

ESRAMManager::Allocate - Allocates memory in ESRAM by finding first-fit gap

The Allocate function checks all FreeSpaces until it finds a gap large enough to contain the data you want to allocate. Once a suitable gap is found, Allocate passes back the memory location and updates the FreeSpace list accordingly.

C++

//--------------------------------------------------------------------------------------
// Name: Allocate
// Desc: Allocate memory in ESRAM using a simple first-fit freelist allocator
//--------------------------------------------------------------------------------------
_Use_decl_annotations_
void ESRAMManager::Allocate(UINT numBytes, UINT alignment, ESRAMAllocation& alloc)
{
    XSFScopedNamedEvent( m_spImmediateContext.Get(), XSF_COLOR_RENDER, L"ESRAMManager Allocate" );

    // Make sure the allocation comes back invalid if we can't fit
    alloc.Invalidate();

    // Try to find contiguous free space big enough to fit the aligned allocation
    for( auto iter = m_freeSpaces.rbegin(); iter != m_freeSpaces.rend(); ++iter )
    {
        FreeSpace& freeSpace = *iter;

        // See if we can fit the aligned allocation inside this free space
        ESRAMPtr beginPtr = freeSpace.m_beginBlock * BLOCK_SIZE;
        ESRAMPtr endPtr = freeSpace.m_endBlock * BLOCK_SIZE;
        ESRAMPtr alignedPtr = Align(beginPtr, alignment);

        if(alignedPtr + numBytes <= endPtr)
        {
            // Our allocation will fit in this free space
            alloc.m_beginBlock = freeSpace.m_beginBlock;
            alloc.m_esramPtr = alignedPtr;

            // Shrink the free space to only the bit left after the allocation
            freeSpace.m_beginBlock = (USHORT) ( Align(alignedPtr + numBytes, BLOCK_SIZE) / BLOCK_SIZE );
            alloc.m_endBlock = freeSpace.m_beginBlock;
            if(freeSpace.m_beginBlock == freeSpace.m_endBlock)
            {
                // We consumed this entire piece of free space
                m_freeSpaces.erase( (++iter).base() ); // Need to increment to get the correct iterator for erase
            }

            break;
        }
    }
}  

ESRAMManager::Free - Frees memory in ESRAM

The Free function does the opposite of the Allocate function. Call Free on the data that you no longer want to store in ESRAM. Free erases the data, and reintegrates that memory back into the FreeSpace list.

C++

//--------------------------------------------------------------------------------------
// Name: Free
// Desc: Free memory in ESRAM
//--------------------------------------------------------------------------------------
_Use_decl_annotations_
void ESRAMManager::Free(ESRAMAllocation& alloc)
{
    XSFScopedNamedEvent( m_spImmediateContext.Get(), XSF_COLOR_RENDER, L"ESRAMManager Free" );

    // See if we can coalesce the freed memory with an existing free space
    FreeSpace* pCoalesced = nullptr;
    for(auto iter = m_freeSpaces.rbegin(); iter != m_freeSpaces.rend(); ++iter)
    {
        FreeSpace& freeSpace = *iter;
        if(freeSpace.m_endBlock == alloc.m_beginBlock)
        {
            // This free space comes immediately before the allocation
            if(pCoalesced == nullptr)
            {
                // Grow the free space to encompass the allocation
                freeSpace.m_endBlock = alloc.m_endBlock;
                pCoalesced = &freeSpace;
            }
            else
            {
                // We already found an area of free space that comes immediately after the allocation,
                //  and now we found an area of free space that comes immediately before the allocation.
                //  Coalesce the two free spaces.
                pCoalesced->m_beginBlock = freeSpace.m_beginBlock;
                m_freeSpaces.erase( (++iter).base() ); // Need to increment to get the correct iterator for erase
                break;
            }
        }
        else if(freeSpace.m_beginBlock == alloc.m_endBlock)
        {
            // This free space comes immediately after the allocation
            if(pCoalesced == nullptr)
            {
                // Grow the free space to encompass the allocation
                freeSpace.m_beginBlock = alloc.m_beginBlock;
                pCoalesced = &freeSpace;
            }
            else
            {
                // We already found an area of free space that comes immediately before the allocation,
                //  and now we found an area of free space that comes immediately after the allocation.
                //  Coalesce the two free spaces.
                pCoalesced->m_endBlock = freeSpace.m_endBlock;
                m_freeSpaces.erase( (++iter).base() ); // Need to increment to get the correct iterator for erase
                break;
            }
        }
    }

    if( pCoalesced == nullptr )
    {
        // We weren't able to coalesce this allocation with any existing free space, so add it to the list
        m_freeSpaces.emplace_back(alloc.m_beginBlock, alloc.m_endBlock);
    }

    alloc.Invalidate();
}  

ESRAMManager::InsertGPUWait - Tells GPU to wait on a DMA operation involving the resource

The InsertGPUWait function prevents the GPU from accessing ESRAM data while that data is being manipulated. Fences tell the GPU to wait. This must be done after performing a Prefetch or Writeback.

C++

//--------------------------------------------------------------------------------------
// Name: InsertGPUWait
// Desc: Tell the GPU to wait on a DMA operation involving the given resource. You must 
//  do this after performing a Prefetch or a Writeback, before accessing the resource on the GPU.
//--------------------------------------------------------------------------------------
_Use_decl_annotations_
void ESRAMManager::InsertGPUWait( ESRAMResource const & esramResource )
{
    m_spImmediateContext->InsertWaitOnFence( D3D11_INSERT_FENCE_NO_KICKOFF, esramResource.m_fence );
}  

ESRAMManager::Create - Creates a buffer in ESRAM without any initial contents

The Create function takes data as input, converts that data into ESRAM manageable formats, and then calls the Allocate function to get a chunk of memory. If a large enough memory chunk is available, create the resources using ID3D11Device.

C++

//--------------------------------------------------------------------------------------
// Name: Create
// Desc: Create a buffer in ESRAM without any initial contents
//--------------------------------------------------------------------------------------
_Use_decl_annotations_
void ESRAMManager::Create( ID3D11Device* const pDevice, D3D11_BUFFER_DESC desc, ESRAMBuffer& esramDest )
{

    desc.MiscFlags |= D3D11X_RESOURCE_MISC_ESRAM_RESIDENT;

    // Calculate the size and alignment required to create the buffer in ESRAM
    XG_BUFFER_DESC xgDesc;
    ZeroMemory( &xgDesc, sizeof(xgDesc) );
    xgDesc.ByteWidth = desc.ByteWidth;
    xgDesc.Usage = (XG_USAGE)desc.Usage;
    xgDesc.BindFlags = desc.BindFlags;
    xgDesc.CPUAccessFlags = desc.CPUAccessFlags;
    xgDesc.MiscFlags = desc.MiscFlags;
    xgDesc.StructureByteStride = desc.StructureByteStride;
    xgDesc.ESRAMOffsetBytes = 0;
    xgDesc.ESRAMUsageBytes = 0;

    XG_RESOURCE_LAYOUT layout;
    XSF_ERROR_IF_FAILED( XGComputeBufferLayout( &xgDesc, &layout ) );

    // Try to allocate the proper amount of space in ESRAM
    ESRAMResource& esramResource = esramDest.m_esramResource;
    Allocate( (UINT)layout.SizeBytes, (UINT)layout.BaseAlignmentBytes, esramResource.m_allocation );
    if( !esramResource.m_allocation.IsValid() )
    {
        // The allocation failed. Try freeing any discarded resources, and try again
        GarbageCollect();
        Allocate( (UINT)layout.SizeBytes, (UINT)layout.BaseAlignmentBytes, esramResource.m_allocation );
    }
    if( !esramResource.m_allocation.IsValid() )
    {
        // The allocation failed. Crash.
        XSF_ERROR_IF_FAILED( E_FAIL );
        return;
    }

    // Create the buffer resource in ESRAM
    desc.ESRAMOffsetBytes = esramResource.m_allocation.m_esramPtr;
    desc.ESRAMUsageBytes = 0;
    XSF_ERROR_IF_FAILED( pDevice->CreateBuffer( &desc, nullptr, esramDest.m_spBuffer.ReleaseAndGetAddressOf() ) );
}  

ESRAMManager::Prefetch - Prefetches data from DRAM buffer

The Prefetch function Copies data from DRAM into ESRAM.

C++

//--------------------------------------------------------------------------------------
// Name: Prefetch
// Desc: Load a DRAM buffer into ESRAM
//--------------------------------------------------------------------------------------
_Use_decl_annotations_
void ESRAMManager::Prefetch( ID3D11Device* const pDevice, ID3D11Buffer* const pDRAMSource, ESRAMBuffer& esramDest )
{
    D3D11_BUFFER_DESC desc;
    pDRAMSource->GetDesc( &desc );

    Create(pDevice, desc, esramDest);

    // Prefetch should be used for resources not written to by the GPU, so we don't need to sync 
    //  with the GPU like we do in Writeback
    m_spDmaContext->CopyResource( esramDest.m_spBuffer.Get(), pDRAMSource, 0 );

    // Insert a fence after the copy and kickoff the DMA engine
    esramDest.m_esramResource.m_fence = m_spDmaContext->InsertFence( 0 );
}  

ESRAMManager::Writeback - Copies an ESRAM buffer into DRAM

The Writeback function does the opposite of the Prefetch function; it copies data from ESRAM into DRAM.

C++

//--------------------------------------------------------------------------------------
// Name: Writeback
// Desc: Copy an ESRAM buffer into DRAM
//--------------------------------------------------------------------------------------
_Use_decl_annotations_
void ESRAMManager::Writeback( ESRAMBuffer& esramSource, ID3D11Buffer* pDRAMDest )
{
    // Writeback is typically used for resources that are written by the GPU. Thus we need to ensure that 
    //  the GPU is done using this resource before we start the DMA operation. We insert a GPU fence and wait
    //  on it with the DMA engine. Once we get an API to access the internal write fence for a given resource,  
    //  we can wait on that instead.
    m_spImmediateContext->FlushGpuCaches( esramSource.m_spBuffer.Get() );
    UINT64 fence = m_spImmediateContext->InsertFence( 0 );
    m_spDmaContext->InsertWaitOnFence( 0, fence );

    m_spDmaContext->CopyResource( pDRAMDest, esramSource.m_spBuffer.Get(), 0 );

    // Insert a fence after the copy and kickoff the DMA engine
    esramSource.m_esramResource.m_fence = m_spDmaContext->InsertFence( 0 );
}  

ESRAMManager::Discard - Marks an ESRAM resource as no longer in use, so its memory can be reclaimed

The Discard function discards data stored in ESRAM when the data is no longer needed. When discarding data, insert a fence to ensure that the data isn’t removed until the GPU is finished with it. Afterwards, place the resource on a list of discarded objects for the garbage collector to free at a later time.

C++

//--------------------------------------------------------------------------------------
// Name: Discard
// Desc: Mark an ESRAM resource as no longer in use, so its memory can be reclaimed
//--------------------------------------------------------------------------------------
_Use_decl_annotations_
void ESRAMManager::Discard( ESRAMBuffer& esramBuffer )
{
    // Flush the GPU caches to ensure the result of any GPU writes takes effect before we reclaim
    //  the memory for another purpose
    m_spImmediateContext->FlushGpuCaches( esramBuffer.m_spBuffer.Get() );

    DiscardInternal( esramBuffer.m_esramResource );
}  

C++

void ESRAMManager::DiscardInternal( _In_ ESRAMResource& esramResource )
{
    // Insert a fence on the GPU to mark the last time the GPU touched this resource. This fence
    //  will be used later to ensure the DMA engine doesn't trample over memory that is still in use.
    esramResource.m_fence = m_spImmediateContext->InsertFence( 0 );

    // If we waited on the fence and freed the resource memory immediately, every discard operation
    //  would block the DMA engine until the GPU caught up. That would be bad. So instead we just
    //  add the discarded resource to a list, and "garbage collect" later when we actually need 
    //  to use the free space.
    m_discardedResources.push_back( esramResource );

    esramResource.m_allocation.Invalidate();
    esramResource.m_fence = INVALID_FENCE;
}  

ESRAMManager::GarbageCollect - Frees memory belonging to discarded resources

The GarbageCollect function goes through the list of discarded resources, waits until they are no longer being used by the GPU, and calls the Free function on them. The Free function erases the data and reinserts the now empty space into the FreeSpace list.

C++

//--------------------------------------------------------------------------------------
// Name: GarbageCollect
// Desc: Free memory belonging to discarded resources
//--------------------------------------------------------------------------------------
void ESRAMManager::GarbageCollect()
{
    for(int i = 0; i < m_discardedResources.size(); ++i)
    {
        // At this point the resource fence indicates the last time the GPU used the resource.
        //  Have the DMA engine wait on this fence to ensure that we can't copy a new resource onto
        //  the reclaimed memory until the GPU is done with it.
        m_spDmaContext->InsertWaitOnFence( D3D11_INSERT_FENCE_NO_KICKOFF, m_discardedResources[i].m_fence );
        Free( m_discardedResources[i].m_allocation );
    }
    m_discardedResources.clear();
}  

See also

DirectX