XG Memory Overview

XG Memory is an auxiliary runtime library for creating specialized memory mappings for Xbox One titles. It was designed as a successor to several different ESRAM mapping methods introduced with the Direct3D 11.X API. For Direct3D 12.X on Xbox One, XG Memory is the recommended way to access high-speed ESRAM memory.

This overview document describes the components of XG Memory and how they work together and with the system memory mapping APIs. XG Memory is provided in source code form (all inline) in xgmemory.h and xgmemory.inl in the Xbox One XDK, so it can be customized if necessary.

Design Philosophy

Fundamentally, XG Memory is one solution of how to map physical memory pages (both system and ESRAM pages) to a virtual address range. The Xbox One game OS and graphics driver stack provide low level APIs such as VirtualAlloc and AllocateTitlePhysicalPages to perform these operations, but they can be difficult to use effectively. XG Memory is built on top of the low level APIs, and was designed with the following principles in mind:

  1. Provide a high-level, easy to use API that all titles can adopt for D3D12.X and even D3D11.X development. At the same time, ship source code so the underlying complexity can be inspected and improved if necessary.
  2. Design with support for dynamic memory layouts in mind; modern titles are increasingly adapting dynamic resolution and other techniques that require frequent runtime remapping of memory layouts.
  3. Design with support for easy memory remapping within the same frame. Efficient ESRAM utilization requires the use of strategies where different resources can share the same memory pages if they are not being used at the same time.
  4. For easier tooling, ensure that memory aliasing and data aliasing between resources are better delineated. Memory aliasing is when two resources share the same memory, but data within the memory is never viewed by the two resources simultaneously for reinterpretation. Data aliasing is when two resources share the same memory but there is an intent to reinterpret the data within in two different ways.

The legacy ESRAM APIs for Direct3D 11.X always treated ESRAM as a contiguous block of memory, which greatly limited flexibility for sharing ESRAM between two or more resources. The legacy APIs also increased complexity when adjustments had to be made to any resource’s ESRAM consumption; other resources had to be fixed up to match the changes. XG Memory is designed specifically to address these issues, as it operates on pages, not ranges, and furthermore it does the math for you to correctly allocate pages where they are needed.

XG Memory API Objects

The XG Memory API is comprised of three API objects: the engine, the layout, and the mapping.

  1. The engine ( XGMemoryLayoutEngine class) is an object that represents your memory budget that you are assigning to one or more memory layouts. It stores a list of ESRAM and/or system memory physical pages that will be mapped to layouts. It is also where layout objects are created.
  2. The layout (XGMemoryLayout class) is an object that represents a single memory layout, comprised of a reserved virtual address range and multiple mappings within. While each engine can create as many layouts as you wish, only one layout from each engine should be in use at any given time, because layouts from the same engine share the same physical memory pages.
  3. The mapping (XGMemoryLayoutMapping struct) represents a single mapping of a set of physical pages to a virtual address range within a layout. After a mapping is constructed, one or more graphics resources can be created with placement at the virtual address of the mapping.

XG Memory Walkthrough and Usage Examples

Preparing to use XG Memory

Before you start using XG Memory, think about your title and where XG Memory would fit in. Do you have just one or two resources that need to use ESRAM? Do you have an entire subsystem that needs to reduce memory footprint via sharing? Can you configure all of your engine’s rendertargets in one place? XG Memory becomes more useful as more resources are placed into the same layout, as more memory sharing opportunities become evident. XG Memory’s layout design is also best designed for engines where the rendertarget resources are configured serially (as a sequential series of steps on the same thread).

Step 1: Create an XG Memory Layout Engine

To start, create an engine containing the maximum amount of physical memory that you would like to use for all of the resources that will be created using XG Memory. For example, you may want to create an engine with 32MB (512 pages) of ESRAM and another 64MB (1024 pages) of system memory. When creating the engine, you can either pass in a pre-allocated list of physical page numbers from AllocateTitlePhysicalPages, or you can instruct XG Memory to call AllocateTitlePhysicalPages on your behalf. ESRAM page numbers can be trivially generated using indices 0-511 to refer to the 512 64KB pages that comprise all of ESRAM.

Step 2: Create an XG Memory Layout

Next consider the total virtual size of all of the resources you plan to configure, including sharing (i.e. double count overlaps). For example, we can create a layout with 128MB of virtual address space to span approximately 10 1080p 32bpp targets plus a bit extra. This number can be overestimated with very little penalty, as layout creation will simply reserve virtual address space, and virtual address space reservation is very inexpensive (and you have hundreds of GB of virtual address space available). The virtual address reservation is done via calling VirtualAlloc with MEM_RESERVE.

In addition to reserving virtual address space, the layout contains free lists of physical memory pages obtained from the engine. As mappings are created, pages are taken from the free list in a LIFO (last in first out) fashion.

Step 3: Create Mappings and Placement Resources

For each resource you wish to configure, determine the resource’s size in virtual address space using the XG library, and round up and divide to obtain a 64KB page count. The XG memory layout class has several methods for creating mappings, but they all share a common concept; with each mapping, you specify a number of ESRAM and a number of system memory pages to use when committing physical pages to the mapping. The various map creation APIs differ only in the complexity of the mapping pattern to generate (the specific ordering of ESRAM and system pages across the mapping). Internally, XG Memory uses a combination of MapTitlePhysicalPages and D3DMapEsramPages to commit system and ESRAM pages respectively to the reserved virtual address space from the layout.

After a mapping is created, you can use the MappingBaseAddress member of the XGMemoryLayoutMapping struct as input to a placement resource creation API, such as CreatePlacedResourceX. Remember that graphics virtual addresses on Xbox One are uniform between CPU and GPU, so you can cast a UINT64 address value to void* or vice versa.

Step 4 (optional): Relinquish Mappings to Reuse Physical Pages

If you wish to share pages between two or more resources, you can “relinquish” one mapping to place its physical pages back on the free list within the layout. Note that the relinquish operation does not decommit the memory for the mapping; it allows the pages committed to that mapping to be reassigned to new mappings. As a result, once you relinquish a mapping, you cannot use that mapping’s memory once the memory associated with subsequent mappings is in use. Relinquish can be thought of as a barrier between one use of memory and another use of the same memory. Relinquish also assists tooling and derivation of intent because memory is only reused at the physical level, and not the virtual level; the two different mappings have unique virtual addresses. If you wish to intentionally alias memory (for example, overlaying a color target onto a stencil buffer to fast-fill stencil), you should do that by creating two placement resources using the same virtual address from the same mapping.

Scenario Pseudocode

Consider the following sequence of pseudo API calls, assuming a memory layout engine has already been created:

  1. CreateLayout(“Rendertargets”, WorstCaseTotalPageCount, &layout)
  2. CreateMapping(“Shadow Map”, &mapping1)
  3. Map(mapping1, ShadowMapPageCount, ShadowMapEsramPageCount)
  4. CreatePlacedResourceX(mapping1.address, ShadowMapDesc, &pShadowMapResource)
  5. CreateMapping(“Color Target”, &mapping2)
  6. Map(mapping2, ColorTargetPageCount, ColorTargetEsramPageCount)
  7. CreatePlacedResourceX(mapping2.address, ColorTargetDesc, &pColorTargetResource)
  8. RelinquishMapping(mapping1)
  9. CreateMapping(“Post Effects Target”, &mapping3)
  10. Map(mapping3, PostEffectsPageCount, PostEffectsEsramPageCount)
  11. CreatePlacedResourceX(mapping3.address, PostEffectsDesc, &pPostEffectsResource)

This sequence creates three rendertargets: a shadow map, a color target, and a post effects target. Using the relinquish feature, the shadow map and the post effects target share the same memory. Note that there are no dependent calculations done by the title here; all of the allocations are computed by the layout as each mapping is created in order.

Now let’s say that you have done some profiling, and decide that the ESRAM page balance between the resources needs to be updated; the color target needs more ESRAM. All that has to be done is reduce the ShadowMapEsramPageCount value and increase the ColorTargetEsramPageCount value. XG Memory takes care of the rest – ESRAM pages are flexibly reallocated from the shadow map and the post effects target to the color target. Note that at any time while creating the layout, the current counts of free ESRAM pages and free system pages can be obtained.

The same technique can be used if you need to insert a new rendertarget into the sequence. For example, we can add a fourth mapping (a depth target) into the layout as follows:

  1. CreateLayout(“Rendertargets”, WorstCaseTotalPageCount, &layout)
  2. CreateMapping(“Shadow Map”, &mapping1)
  3. Map(mapping1, ShadowMapPageCount, ShadowMapEsramPageCount)
  4. CreatePlacedResourceX(mapping1.address, ShadowMapDesc, &pShadowMapResource)
  5. CreateMapping(“Color Target”, &mapping2)
  6. Map(mapping2, ColorTargetPageCount, ColorTargetEsramPageCount)
  7. CreatePlacedResourceX(mapping2.address, ColorTargetDesc, &pColorTargetResource)
  8. CreateMapping(“Depth Target”, &mapping4)
  9. Map(mapping4, DepthTargetPageCount, DepthTargetEsramPageCount)
  10. CreatePlacedResourceX(mapping4.address, DepthTargetDesc, &pDepthTargetResource)
  11. RelinquishMapping(mapping1)
  12. CreateMapping(“Post Effects Target”, &mapping3)
  13. Map(mapping3, PostEffectsPageCount, PostEffectsEsramPageCount)
  14. CreatePlacedResourceX(mapping3.address, PostEffectsDesc, &pPostEffectsResource)

Note that no changes needed to be made to the surrounding code. When adding a new mapping, you may need to rebalance ESRAM page counts amongst the other mappings, but you never need to worry about contiguous ranges, virtual address overlaps, or any other dependent calculations.

Next, let’s say that you need to dynamically change the size of the rendertargets, for a dynamic resolution change. However, you have already well balanced the ESRAM usage between all of the resources, and you don’t want to have to rework that balance. This is no problem with XG Memory – simply adjust the page count variables (ShadowMapPageCount, ColorTargetPageCount, etc) and XG Memory will take care of the rest, keeping your ESRAM allocations intact. All you have to do is ensure the WorstCaseTotalPageCount value is large enough to handle the largest configuration of all of your resources.

Conclusion

XG Memory can greatly reduce the complexity of using ESRAM on Xbox One. Additionally, it can prove useful even with memory layouts where page reuse is desirable and yet are 100% comprised of system pages.

Feedback on XG memory is welcome, through Game Developer Support.

See also

Direct3D 12 Xbox One Reference