In the new graphics APIs - Direct3D 12 and Vulkan - creation of resources (textures and buffers) is a multi-step process. You need to allocate some memory and place your resource in it. In D3D12 there is a convenient function
ID3D12Device::CreateCommittedResource that lets you do it in one go, allocating the resource with its own, implicit memory heap, but it's recommended to allocate bigger memory blocks and place multiple resources in them using
When placing a resource in the memory, you need to know and respect its required size and alignment. Size is basically the number of bytes that the resource needs. Alignment is a power-of-two number which the offset of the beginning of the resource must be multiply of (
offset % alignment == 0). I'm thinking about writing a separate article for beginners explaining the concept of memory alignment, but that's a separate topic...
Back to graphics, in Vulkan you first need to create your resource (e.g.
vkCreateBuffer) and then pass it to the function (e.g.
vkGetBufferMemoryRequirements) that will return required size of alignment of this resource (
alignment). In DirectX 12 it looks similar at first glance or even simpler, as it's enough to have a structure
D3D12_RESOURCE_DESC describing the resource you will create to call
ID3D12Device::GetResourceAllocationInfo and get
D3D12_RESOURCE_ALLOCATION_INFO - a similar structure with
Alignment. I've described it briefly in my article Differences in memory management between Direct3D 12 and Vulkan.
But if you dig deeper, there is more to it. While using the mentioned function is enough to make your program working correctly, applying some additional knowledge may let you save some memory, so read on if you want to make your GPU memory allocator better. First interesting information is that alignments in D3D12, unlike in Vulkan, are really fixed constants, independent of a particular GPU or graphics driver that the user may have installed.
- Alignment required for buffers and normal textures is always 64 KB (65536), available as constant
- Alignment required for MSAA textures is always 4 MB (4194304), available as constant
So, we have these constants and we also have a function to query for actual alignment. To make things even more complicated, structure
Alignment member, so you have one alignment on the input, another one on the output! Fortunately,
GetResourceAllocationInfo function allows to set
D3D12_RESOURCE_DESC::Alignment to 0, which causes default alignment for the resource to be returned.
Now, let me introduce the concept of "small textures". It turns out that some textures can be aligned 4 KB and some MSAA textures can be aligned to 64 KB. They call this "small" alignment (as opposed to "default" alignment) and there are also constants for it:
- Alignment allowed for small textures is 4 KB (4096), available as constant
- Alignment allowed for small MSAA textures is 64 MB (65536), available as constant
|Texture||64 KB||4 KB|
|MSAA texture||4 MB||64 KB|
Using this smaller alignment allows to save some GPU memory that would otherwise be unused as padding between resources. Unfortunately, it's unavailable for buffers and available only for small textures, with a very convoluted definition of "small". The rules are hidden in the description of Alignment member of D3D12_RESOURCE_DESC structure:
- It must have
- It must not be
- Its most detailed mip level (considering texture width, height, depth, pixel format, and number of samples), aligned up to some imaginary "tiles", must require no more bytes than the "larger alignment restriction". So for a normal texture, when this calculated size is <= 64 KB, you can use the alignment of 4 KB. For an MSAA texture, when this calculated size is <= 4 MB, you can use the alignment of 64 KB.
GetResourceAllocationInfo calculate all this automatically and just return optimal alignment for a resource, like Vulkan function does? Possibly, but this is not what happens. You have to ask for it explicitly. When you pass
D3D12_RESOURCE_DESC::Alignment = 0 on the input, you always get the default (larger) alignment on the output. Only when you set
D3D12_RESOURCE_DESC::Alignment to the small alignment value, this function returns the same value if the small alignment has been "granted".
There are two ways to use it in practice. First one is to calculate the eligibility of a texture to use small alignment on your own and pass it to the function only when you know the texture fulfills the conditions. Second is to try the small alignment always. When it's not granted,
GetResourceAllocationInfo returns some values other than expected (in my test it's
Alignment = 64 KB and
SizeInBytes = 0xFFFFFFFFFFFFFFFF). Then you should call it again with the default alignment. That's the method that Microsoft shows in their "Small Resources Sample". It looks good, but a problem with it is that calling this function with an alignment that is not accepted generates D3D12 Debug Layer error #721 CREATERESOURCE_INVALIDALIGNMENT. Or at least it used to, because on one of my machines the error no longer occurs. Maybe Microsoft fixed it in some recent update of Windows or Visual Studio / Windows SDK?
Here comes the last quirk of this whole D3D12 resource alignment topic: Alignment is applied to offset used in
CreatePlacedResource, which we understand as relative to the beginning of an
ID3D12Heap, but the heap itself has an alignment too!
D3D12_HEAP_DESC structure has
Alignment member. There is no equivalent of this in Vulkan. Documentation of D3D12_HEAP_DESC structure says it must be 64 KB or 4 MB. Whenever you predict you might create an MSAA textures in a heap, you must choose 4 MB. Otherwise, you can use 64 KB.
Thank you, Microsoft, for making this so complicated! ;) This article wouldn't be complete without the advertisement of open source library: D3D12 Memory Allocator (and similar Vulkan Memory Allocator), which automatically handles all this complexity. It also implements both ways of using small alignment, selectable using a preprocessor macro.