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.
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.
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
};
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;
};
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;
};
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;
};
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
};
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;
}
}
}
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();
}
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 );
}
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() ) );
}
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 );
}
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 );
}
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;
}
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();
}