Learning D3D12 from D3D11 - Part 3: Textures
Intro
Out of all the new topics that need to be learned when you're upgrading from D3D11 to D3D12, loading textures is without a doubt one of the most complicated things you'll come across when dealing with the new API, especially from what you're used to. You've no doubt run into this already if you've searched for the D3D12 equivalent of D3DXCreateTextureFromFile. Surprise! It doesn't exist! Luckily there's a fairly well-established way to do this, so we'll be covering each part: file loads, gpu uploads, and resource and view creation.Texture File Load
First thing's first, we need to be able to load a .dds file into memory in a structure we can work with. We may not have D3DXCreateTextureFromFile, but we do have DirectXTex, an open project on github for managing DDS files. It is a bit odd that this doesn't appear to be included in the Windows SDK, but we'll roll with it. You can download the whole thing if you like, but all you need in order to load is in the DirectXTex folder. Grab all those files and include them in your project (you don't need the shaders for this), and #include DirectXTex.h in whatever file you plan on doing texture management. Have a DDS file handy that you want to load, and we'll start by having DXTex load it from disk:WCHAR* filePath = L"YourPathHere"; DirectX::ScratchImage *imageData = new DirectX::ScratchImage(); HRESULT loadResult = DirectX::LoadFromDDSFile(filePath, DirectX::DDS_FLAGS_NONE, nullptr, *imageData); assert(loadResult == S_OK);
Simple enough, we just have to feed it the file path, flags, and an initialized ScratchImage. You don't have to worry about the flags for now, they're generally for managing legacy stuff. Now we've got some parsed DDS data we can work with, so let's start by grabbing some of the more important bits:
const DirectX::TexMetadata& textureMetaData = imageData->GetMetadata(); DXGI_FORMAT textureFormat = textureMetaData.format; bool is3DTexture = textureMetaData.dimension == DirectX::TEX_DIMENSION_TEXTURE3D;
First we pull the metadata out, which will give us all the info we need to know how to manage the texture data we have. We'll grab the texture format and store off whether or not this is a 3D texture (right now I'm only handling 2D and 3D because I haven't needed 1D). This is also where you'll want to store your texture resource information in whatever resource structure you have that represents a "Texture". I cover some examples of other resource types in Part 2 of this series, and you could do something similar to that. Generally it's handy to store the format, dimensions, mip count, array size, etc, from the metadata to keep alongside the texture resources we'll be creating. Since this is up to your implementation, I'll leave that to your discretion. Here's an example of what this looks like for me:
Texture* newTexture = new Texture(); newTexture->SetDimensions(textureMetaData.width, textureMetaData.height, textureMetaData.depth)); newTexture->SetMipCount(textureMetaData.mipLevels); newTexture->SetArraySize(textureMetaData.arraySize); newTexture->SetFormat(textureMetaData.format); newTexture->SetIsCubeMap(textureMetaData.IsCubemap());
Alternatively, you could just store the D3D12_RESOURCE_DESC we're about to set up if you wanted. Before doing this, I wanted to point out the helpful utility function DirectX::MakeSRGB(textureFormat). You can use this function if, for example, you wanted to force your texture to be used as SRGB, and it'll return the SRGB version of the format you provide.
Texture Resource Creation
Let's begin this stage by setting up our resource description:D3D12_RESOURCE_DESC textureDesc{}; textureDesc.Format = textureFormat; textureDesc.Width = (uint32)textureMetaData.width; textureDesc.Height = (uint32)textureMetaData.height; textureDesc.Flags = D3D12_RESOURCE_FLAG_NONE; textureDesc.DepthOrArraySize = is3DTexture ? (uint16)textureMetaData.depth : (uint16)textureMetaData.arraySize; textureDesc.MipLevels = (uint16)textureMetaData.mipLevels; textureDesc.SampleDesc.Count = 1; textureDesc.SampleDesc.Quality = 0; textureDesc.Dimension = is3DTexture ? D3D12_RESOURCE_DIMENSION_TEXTURE3D : D3D12_RESOURCE_DIMENSION_TEXTURE2D; textureDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN; textureDesc.Alignment = 0;
Nothing particularly special in the resource desc setup. The only thing of note is to make sure you set DepthOrArraySize appropriately based on the texture dimensions you have. We'll also set up a default heap to create our texture resource. This is covered in part 2 as well, so I won't cover it here. We'll use this, and the resource desc, to create a committed texture resource. Long-term for a large project, using placed resources is the way you'd want to go, but for simplicity we'll stick with committed resources.
D3D12_HEAP_PROPERTIES defaultProperties; defaultProperties.Type = D3D12_HEAP_TYPE_DEFAULT; defaultProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; defaultProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; defaultProperties.CreationNodeMask = 0; defaultProperties.VisibleNodeMask = 0; ID3D12Resource *newTextureResource = NULL; mDirect3DManager->GetDevice()->CreateCommittedResource(&defaultProperties, D3D12_HEAP_FLAG_NONE, &textureDesc, D3D12_RESOURCE_STATE_COPY_DEST, NULL, IID_PPV_ARGS(&newTextureResource));
Notice I set the initial state to D3D12_RESOURCE_STATE_COPY_DEST, and that's because this resource will be the destination for our texture data when we upload it with copy commands. Since we created it on the default heap (which is where basically all textures should go in order to get maximum GPU bandwidth), we can't copy to it directly. We must copy the texture data to an upload heap, which we'll then use to copy to this new resource. For the next step we'll create a shader resource view for the texture. If you're just dealing with a regular 2D texture, you can leave the D3D12_SHADER_RESOURCE_VIEW_DESC pointer as NULL, and it will use the texture resource information to correctly set up your SRV. If you have a cube map, you'll want to specify the SRV desc yourself like this:
D3D12_SHADER_RESOURCE_VIEW_DESC shaderResourceViewDesc = {}; if (textureMetaData.IsCubemap()) { assert(textureMetaData.arraySize == 6); shaderResourceViewDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE; shaderResourceViewDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; shaderResourceViewDesc.TextureCube.MostDetailedMip = 0; shaderResourceViewDesc.TextureCube.MipLevels = (uint32)textureMetaData.mipLevels; shaderResourceViewDesc.TextureCube.ResourceMinLODClamp = 0.0f; } mDevice()->CreateShaderResourceView(newTextureResource, textureMetaData.IsCubemap() ? &shaderResourceViewDesc : NULL, srvHandle);
Texture Uploads
Alrighty, to recap we now have a DDS texture loaded from disk, a GPU resource to put that data in, and a shader resource view to bind it later on. All that's left now is to upload the CPU data we have to that GPU resource. A bit easier said than done, but we'll be fine. The first step in uploading the texture is to query for what we call "footprints" of the texture description we made earlier. This gives us all the information we'll need to properly upload the texture data, including how much memory we need to reserve for the upload.uint64 textureMemorySize = 0; UINT numRows[MAX_TEXTURE_SUBRESOURCE_COUNT]; UINT64 rowSizesInBytes[MAX_TEXTURE_SUBRESOURCE_COUNT]; D3D12_PLACED_SUBRESOURCE_FOOTPRINT layouts[MAX_TEXTURE_SUBRESOURCE_COUNT]; const uint64 numSubResources = textureMetaData.mipLevels * textureMetaData.arraySize; mDevice->GetCopyableFootprints(&textureDesc, 0, (uint32)numSubResources, 0, layouts, numRows, rowSizesInBytes, &textureMemorySize);
MAX_TEXTURE_SUBRESOURCE_COUNT is whatever makes sense for your maximum supported number of subresources (array count x mip count). Now we have all the information we need to set up our upload. I'm not going to fully cover how you should wrap your upload system here, but see the resource section at the bottom for a link to a good example. I also covered upload heaps in Part 2 in case you need information on how those work and why we need them. The short of it is that you can't directly upload data to a non-CPU-visible resource (like our texture resource). You have to copy your data to a resource created on an upload heap (which is CPU visible), and then schedule copy commands from that resource to your destination resource. So here, you're going to see me use my upload context to grab me a chunk of a large upload buffer, and grab a pointer to the start of that chunk of memory:
Direct3DUploadInfo uploadInfo = uploadContext->BeginUpload(textureMemorySize, mQueueManager); uint8* uploadMemory = reinterpret_cast<uint8*>(uploadInfo.Memory);
If you don't have something like this and just need to get up and running, you can simply create a resource on an upload heap instead(see Part 2 of this series), large enough to fit textureMemorySize. Map it, and grab the pointer to the CPU memory just like I have. One caveat you should be aware of is that any time you're uploading a texture, the memory needs to be aligned against D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT, in order to upload properly. The debug layer will yell at you as well if you're not doing this. If you're not using a managed upload buffer and created a simple one like I described, you won't need to worry about this for the moment because you're starting at byte 0 of the upload buffer anyway, but be aware of it for the future. Next, we're going to copy our texture data into this upload buffer using the information we got from the footprints. Shoutout to MJP for this bit. At the time when I was learning D3D12 initially, his was the best example I could find for properly setting up texture data.
for (uint64 arrayIndex = 0; arrayIndex < textureMetaData.arraySize; arrayIndex++) { for (uint64 mipIndex = 0; mipIndex < textureMetaData.mipLevels; mipIndex++) { const uint64 subResourceIndex = mipIndex + (arrayIndex * textureMetaData.mipLevels); const D3D12_PLACED_SUBRESOURCE_FOOTPRINT& subResourceLayout = layouts[subResourceIndex]; const uint64 subResourceHeight = numRows[subResourceIndex]; const uint64 subResourcePitch = MathHelper::AlignU32(subResourceLayout.Footprint.RowPitch, D3D12_TEXTURE_DATA_PITCH_ALIGNMENT); const uint64 subResourceDepth = subResourceLayout.Footprint.Depth; uint8* destinationSubResourceMemory = uploadMemory + subResourceLayout.Offset; for (uint64 sliceIndex = 0; sliceIndex < subResourceDepth; sliceIndex++) { const DirectX::Image* subImage = imageData->GetImage(mipIndex, arrayIndex, sliceIndex); const uint8* sourceSubResourceMemory = subImage->pixels; for (uint64 height = 0; height < subResourceHeight; height++) { memcpy(destinationSubResourceMemory, sourceSubResourceMemory, MathHelper::Min(subResourcePitch, subImage->rowPitch)); destinationSubResourceMemory += subResourcePitch; sourceSubResourceMemory += subImage->rowPitch; } } } }
Our upload memory is now ready to go. We can now start issuing upload commands to copy this data to our destination texture. Here you'll see me using my "uploadContext" again, but you can just replace that bit with mCommandList->CopyTextureRegion(destination, 0, 0, 0, source, NULL); where mCommandList is whatever command list you want to submit the uploads on. Replace uploadInfo.Resource with your upload resource, and uploadInfo.Offset with 0 if you're not using a managed upload buffer with other things in it. Generally, you want to submit these commands to a command list that is executed on a copy queue (D3D12_COMMAND_LIST_TYPE_COPY), so that it can execute in parallel with your graphics work. If you don't have this set up yet, you can just submit it to your graphics command list for now until you're ready, that's fine.
for (uint64 subResourceIndex = 0; subResourceIndex < numSubResources; subResourceIndex++) { D3D12_TEXTURE_COPY_LOCATION destination = {}; destination.pResource = newTextureResource; destination.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; destination.SubresourceIndex = (uint32)subResourceIndex; D3D12_TEXTURE_COPY_LOCATION source = {}; source.pResource = uploadInfo.Resource; source.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; source.PlacedFootprint = layouts[subResourceIndex]; source.PlacedFootprint.Offset += uploadInfo.Offset; uploadContext->CopyTextureRegion(&destination, &source); }
Now you can execute this command list whenever that's appropriate in your renderer and the copy commands will be processed. After that happens, your texture should be ready to go! Just be sure not to try and use the resource until you know those commands have been executed, which can be done either by waiting N frames (which is whatever your frame buffer count is), or checking the fence on your command queue to see if it has processed that command list execution.
Aaaaand that's it! We're done!
Resources
The following are a list of resources from which I pull the vast majority of my information and design from. It's thanks to these resources that I feel comfortable with DX12 enough to be able to share, so thank you to everyone who contributes to these!The Microsoft DirectX Graphics Sample Github, especially the MiniEngine core. I borrow heavily from this.
The DirectXTech Forum "Getting Started" Section.
MJP's awesome bindless deferred rendering github. His resource upload style is great.
Microsoft's Direct3D 12 Programming Guide.
The D3D12 Reference Docs.
Nvidia's DX12 Do's and Don'ts, especially useful at answering "how should I do this?" questions.
Intel's DX12 Migration Tutorials.