- A BRIEF RECAP
- THE NEW WAY OF INITIALISING FACTORY AND DEVICE
- DESCRIPTORS, COMMAND OBJECTS, FENCES AND THE SWAPCHAIN
- A SUMMARY (FINALLY!)

A BRIEF RECAP
I’m very glad to be back on my blog after having been distracted for several months by my university work. I now however, have a long study break to look forward to (over a year). And I’ve made quite a lot of progress, and there some substantial changes to the project. I believe I discussed in the last page (which was written months ago), that basic rendering is now functioning. Albeit only showing a blank screen of a chosen colour. Nevertheless it’s working, and has no nasty errors on shutdown either. I also found a work-around for the render class in the DLL having to be placed on the heap. You may remember there were problems with the COM pointers in the render class in the DLL on shutdown. The solution (at the time) was to make a new render class on the heap, although with the risk of it being slower. That’s been (somewhat) fixed now albeit with a slightly hack feel to it.
There’ve been changes to the way the dialog box we made is called, and how the swap chain and depth buffers (more about those later) are created too. That being they now actually use the values you enter into the dialog box to initialise themselves. The dialog box is no longer just for show. In an interim project I made between this and the last page I did have a scene rendered into a window, but with the values being fixed in the render class to I think 800, 600 for width and height and a refresh rate set at 60Hz. That’s no longer the case. This feels much better and slightly more professional.
Also you may recall from previous projects the code that initialises DirectX and the Factory were both lumped into the same function. That’s also no longer the case. Now the factory has its own function to initialise it, and get the data we want off the graphics card (it’s name, displays, and refresh rates). After the factory has been created we then run the dialog box and confirm our choices.
Only then do we initialise DirectX (the device). That gets all the display options stuff out of the way before commencing with all the DirectX details. From here on I’ll discuss in some detail (not an excessive amount) the changes that have been made and some of the problems that were encountered.
THE NEW WAY OF INITIALISING FACTORY AND DEVICE
I will not post old versions here as it’s unnecessary, and you can always look back at previous pages if you wish to see older versions of how we did this. Below is the new one. It shows both functions, which were at one time all in one (although some bits are new):
bool D3DRenderer::InitFactory()
{
#if defined(DEBUG) || defined(_DEBUG)
// Enable the D3D12 debug layer.
{
ComPtr<ID3D12Debug> debugController;
ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
debugController->EnableDebugLayer();
}
#endif
// create DXGI Factory
ThrowIfFailed(CreateDXGIFactory(IID_PPV_ARGS(&mdxgiFactory)));
mdxgiFactory->EnumAdapters((UINT)0, &mAdapter);
// Try to create hardware device.
ThrowIfFailed(D3D12CreateDevice(
nullptr, // nullptr means default adapter
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&md3dDevice)));
if (!md3dDevice)
{
std::wstring _string = L"CreateDevice failed, attempting to use WARP adapter
\n\n";
OutputDebugString(_string.c_str());
}
// Fallback to WARP device.
if (!md3dDevice)
{
/* No longer used. Changed use of temporary for a data member in the rendering
class (mAdapter) - 25/8/2
*/
//ComPtr<IDXGIAdapter> pWarpAdapter;
ThrowIfFailed(mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&mAdapter)));
ThrowIfFailed(D3D12CreateDevice(
mAdapter.Get(),
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&md3dDevice)));
}
return true;
}
//----Next Function----//
bool D3DRenderer::InitDirectX()
{
if (md3dDevice != nullptr)
{
std::wstring _string = L"WARP adapter initialised \n\n";
OutputDebugString(_string.c_str());
}
ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&mFence)))
mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(
D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(
D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize(
D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
CreateCommandObjects();
CreateSwapChain();
CreateRtvAndDsvDescriptorHeaps();
CreateViews();
// added to try and stop flickering (not sure if it will) - 27/8/24
ResizeSwapChain();
return true;
}
These two functions were once wrapped up into one. It caused problems when running the display choices dialog box we made. It left me with nowhere sensible to call the dialog box from, and thus structure the code and project nicely. This way works much better.
You may notice that I slightly lied above when I said we create the factory and device in separate functions. That’s not quite true (but you could if you really wanted to). The device is actually created along with the factory in the InitFactory() function. We don’t do anything with it there however except store a pointer to it in our COM pointer data member in the render class. This is done by the function D3D12CreateDevice() which you can see above (if you scroll up far enough) it takes the COM pointer md3dDevice in our class as its last argument. Here’s where the device COM pointer is stored in our render class:
#pragma once
#include "../Shared Headers/Render Interface.h"
#include "Render Device.h"
#include "DxException.h"
// namespaces for Comptr's
using Microsoft::WRL::ComPtr;
class D3DRenderer : public D3DInterface
{
....quite some time later
// this one below
Microsoft::WRL::ComPtr<ID3D12Device> md3dDevice;
....continues
We do the exact same for the factory, and the others as well. In fact all of the COM pointers you can see above passed into the native DirectX functions (such as CreateDXGIFactory(), D3D12CreateDevice or CreateFence(), etc….) are data members in our rendering class (D3DRenderer). Starting from the top these are:
mdxgiFactory
mAdapter
md3dDevice
mFence
You may notice there is one at the very beginning which is created locally within the function, but is yet referred to later in the program. This is something some of you may remember from the early chapters of C++ learning textbooks that we were told explicitly not to do! It’s this one:
ComPtr<ID3D12Debug> debugController;
That’s not a data member of our render class and is created locally. Why it is able to persist outside the scope of this function is a mystery to me at this time, although I suspect it’s something to do with it being a COM pointer. I’m not really sure though. I’m well aware it’s there and if any problems occur with the DirectX 12 debug layer in the future I’ll know where to start looking. For now it works fine, although not necessarily recommend practice.
On the same theme, you might also notice a part which is commented out:
/* No longer used. Changed use of temporary for a data member in the rendering
class (mAdapter) - 25/8/2
*/
//ComPtr<IDXGIAdapter> pWarpAdapter;
Originally if the first attempt to create the DirectX device failed, when falling back to the WARP adapter (we’ll discuss this shortly) the device was then created using a temporary declared inside the function scope as the pointer to the adapter. You can see it just above. It’s called pWarpAdapter. I was prepared to allow the debug pointer to pass in this way, but not the one that’s responsible for being a pointer to the graphics adapter. That’s why this part of the code is now commented out and our render class has a data member mAdapter which is of the same type as the old pWarpAdapter was. That being ComPtr<IDXGIAdapter>. You’ve seen this already in older pages in this blog when we were using the adapter to acquire resolution and refresh data.
I felt this was a safer and more professional solution, rather than leaving this to a temporary. Which leads us to our next discussion. And it was quite a surprise at that.
The WARP adapter is described by the author of the textbook as:
“Also observe that if our call to D3D12CreateDevice fails, we fall back to a WARP device, which is a software adapter. WARP stands for Windows Advanced Rasterization Platform.”
“On Windows 8, the WARP device supports up to feature level 11.1.”
Microsoft describe it entirely in the following link:
https://learn.microsoft.com/en-us/windows/win32/direct3darticles/directx-warp
I haven’t read the details of that link. It’s my next priority after completing this chapter. Clearly it’s some kind of very sophisticated redundancy in case something goes wrong when attempting to create the DirectX device with your current adapter (adapter means graphics card in case you missed that in earlier chapters).
I never quite know how to interpret the word redundant or redundancy. I know we often think of it being unused or no longer needed. I’ve also heard engineers describe systems as having redundancy built in in case of failure of the main components. Either way it conjures images of padding or needless additions or plain not required.
Until you need it that is….
To my amazement every time I’ve compiled and run anything on this entire project, starting over two years ago now, the code has made use of the WARP adapter every single time. That’s because my graphics card is so old it will not work with the typical D3D12CreateDevice() function. That’s the one that uses the nullptr to indicate that we want to use the default (first) adapter that mdxgiFactory->EnumAdapters() has found. Here’s the code again to remind you, and so you don’t have to scroll up:
// create DXGI Factory
ThrowIfFailed(CreateDXGIFactory(IID_PPV_ARGS(&mdxgiFactory)));
mdxgiFactory->EnumAdapters((UINT)0, &mAdapter);
// Try to create hardware device.
ThrowIfFailed(D3D12CreateDevice(
nullptr, // nullptr means default adapter
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&md3dDevice)));
if (!md3dDevice)
{
std::wstring _string = L"CreateDevice failed, attempting to use WARP adapter
\n\n";
OutputDebugString(_string.c_str());
}
// Fallback to WARP device.
if (!md3dDevice)
{
/* No longer used. Changed use of temporary for a data member in the rendering
class (mAdapter) - 25/8/2
*/
//ComPtr<IDXGIAdapter> pWarpAdapter;
ThrowIfFailed(mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&mAdapter)));
ThrowIfFailed(D3D12CreateDevice(
mAdapter.Get(),
D3D_FEATURE_LEVEL_11_0,
IID_PPV_ARGS(&md3dDevice)));
}
Note it’s done slightly differently. The factory calls a very specific function when enumerating the WARP adapter. The device then also uses a COM pointer in the first argument rather than nullptr.
I tried everything to get the initial device create function to work, even setting feature levels as low as DirectX version 9 to get it to run. It won’t. Thankfully I can still proceed because of the WARP adapter allowing a whole lot of backwards compatibility with older hardware. Or just simply bypassing that hardware. I’m no longer sure whether this code is even using my graphics card, or if it’s using a version of the on-board renderer to do the work. Either way it still functions, and nor myself, nor I imagine anyone reading this, is intending to be making Call Of Duty level graphics programs. I was thinking more StarDew Valley level 🙂 Possibly a bit beyond that though, maybe even as good as some of the early ArmA games.
At least that’s the plan for a long time. As I get to know this subject a bit better, that may change. For now though that’s a reasonable goal, so the WARP adapter fallback is not really a problem. It was a surprise though.
Given that we have not covered any proper rendering code so far in the project, you will hopefully have been a bit bothered by some of the new stuff that’s suddenly appeared in those functions. Particularly the InitDirectX() function. Given that we’re now moving on from just getting a reasonably stable (I hope) Windows program working, and showing some stuff retrieved from the graphics card – onto actually drawing and rendering things – there will be a lot of new concepts appearing. And they’ll be appearing suddenly too. That brings us to our next part.
I will not over discuss the concepts, that’s what you have Frank Luna’s book and Microsoft documentation for. I recommend using both.
DESCRIPTORS, COMMAND OBJECTS, FENCES AND THE SWAPCHAIN
Time for some new concepts. We’ll start with descriptors. I remember a long, long time ago not needing to worry about any of this when I was writing stuff for DirectX version 9. It’s one of many reasons I was rather surprised when I saw how much more complex DirectX 12 is. I wished it was much easier to use. Nevertheless we can’t make demands of an API in that way so we’ll have to learn it.
If in doubt, quote an expert, so I shall. Here’s how the textbook describes descriptors:
“Before we issue a draw command, we need to bind (or link) the resources to the rendering pipeline that are going to be referenced in that draw call. Some of the resources may change per draw call, so we need to updates the bindings per draw call if necessary. However, GPU resources are not bound directly. Instead a resource is referenced through a descriptor object, which can be thought of as a lightweight structure that describes the resource to the GPU. Essentially, it is a level of indirection; given a resource descriptor, the GPU can get the actual resource data and know the necessary information about it. We bind resources to the rendering pipeline by specifying the descriptors that will be referenced in the draw call.“
Quite a confusing concept at first if you haven’t seen it before. Descriptors are important. Note also that the “rendering pipeline” is mentioned in the quoted text above. Let’s be clear from the start – we do not need to know much if anything about that (at least not yet). The exact way API’s (such as DirectX, Vulcan, OpenGl, etc….) perform rendering is beyond our scope for now, even as programmers intending to render scenes for games or other reasons. Whenever the “rendering pipeline” is mentioned it’s best just to consider it as some very sophisticated things happening far beneath the code we use that we don’t need to know about. We will cover some of it in later chapters, but for now it wouldn’t be helpful. It’s the machine that we will learn to interact with, in the way it wants us to interact with it.
And it wants us to use descriptors. Note also the word “resource” is mentioned several times in the quote above. What’s a resource? A resource can be a texture, a shader, or a buffer for rendering, etc…. . A render buffer is what we write stuff onto which we then present to the screen on your monitor. So essentially where we draw or “render” things. A buffer can also be used as a depth buffer (depth buffers will be discussed a little later). Often a depth buffer is referred to as a depth/stencil buffer, but do not be concerned with that now, just read it as “depth buffer”.
We can’t talk too much about descriptors without discussing resources a little more. That also leads us to a slight detour onto another concept. The swap chain. In this particular build of the project (which I’ll post a download link to later) we only use three resources. Two for the swap chain buffers, and one for the depth buffer. We aren’t using shaders or textures yet. Have a look at where these are declared in our rendering class:
static const int SwapChainBufferCount = 2;
int mCurrBackBuffer = 0;
Microsoft::WRL::ComPtr<ID3D12Resource> mSwapChainBuffer[SwapChainBufferCount];
Microsoft::WRL::ComPtr<ID3D12Resource> mDepthStencilBuffer;
Note in particular the type of both mSwapChainBuffer and mDepthStencilBuffer. They’re both the same, <ID3D12Resource>. Yet they’re being used for very different things? The swap chain (in its most simple form) is two buffers that we render (or draw/write) into. One is presented to the monitor, whilst the other behind it (the back buffer) is being drawn into. That way you never see the computer actually drawing on the screen, like you used to with computer games years ago. A good example if you’re ever interested is The Hobbit for the spectrum 48k. I remember that game very well and it was amazing at the time, and I still feel some of that even today. We don’t have time here for a discussion of the uncanny valley of advancements in computer games somehow unexpectedly losing an imaginative quality that simpler ones had, but if you’re interested in seeing a computer actually drawing then check this link out:
Needless to say it’s not done like that anymore. The swap chain hides the buffer that’s being rendered into from the monitor until we’re ready to present it in the draw function. We haven’t gotten to the draw function part yet, so don’t be concerned with that now. It’s just the function where we draw things.
The depth/stencil buffer performs depth checking. This checks the distance of each pixel from a camera (your monitor’s view) and makes sure a distant object isn’t drawn over one that is closer to the camera. So a picture of a car with a lighthouse in the background will look correct as it would to the naked eye. Not with the distant lighthouse drawn over the car in the foreground.
Two short things before we continue. In case you’re unsure what a “buffer” is, it’s just a reserved piece of memory lying around on your computer somewhere. That’s a bit over-simplified, but that’s essentially what it is. The next thing before we continue, is it’s important to note (as already mentioned) that while the depth buffer and swap chains are different things, they both are declared in the render class as a data member of type <ID3D12Resource>.
How do we tell the GPU how to use them then? How does the GPU distinguish two as buffers to be rendered into, and the other as a depth buffer?
That’s where we use descriptors. And that’s why they’re important. Also note, that from the quote some ways above, which I’ll repeat a portion of here:
“Before we issue a draw command, we need to bind (or link) the resources to the rendering pipeline that are going to be referenced in that draw call.”
Notice that it specifically says “referenced in that draw call”. My initial interpretation of that, was that we had to bind them every single time we use the draw function. That interpretation is incorrect. We do have to bind resources to descriptors, but once we have, we don’t necessarily have to do it every time we use the draw function. In fact in this project as it currently stands we only bind the three resources once, in a function that’s separate to the draw function. Perhaps this portion of the quote above is a hint:
“Some of the resources may change per draw call, so we need to update the bindings per draw call if necessary.”
So if the resources are not changing in any significant way for each use of the draw function and are not being used for multiple things, it means we only bind them once it seems. The three resources are bound initially by the CreateViews() function in the project. You can see it in the source code if you download the link at the end (or check out the code snippet below). There is one other time in this project where we need to re-bind the resources, and that’s if we change the size of the window we render into. If we do that, we need to re-size the swap chain buffers and the depth buffer, and then re-bind all three resources to descriptors. If we don’t re-size the window whilst the program is running then we do not need to do that. The project’s code will do the re-sizing and re-binding for you if you choose to re-size the window at run time.
Below I’ve posted the CreateViews() function from the project that binds the created resources to their relevant descriptors. This is called when initialising everything. Note that we have to have a heap store our descriptors in (one heap for each type), which you can see being referenced in the snippet below named mRtvHeap and mDsvHeap (Render target view heap and Depth stencil view heap). The heaps are already created in a separate function which I won’t show here. The two functions doing the binding are CreateRenderTargetView() and CreateDepthStencilView(). Note how they each take a resource as the first argument and a handle to a descriptor as their last argument (you can ignore the second argument for now). You can see the function that creates the heaps in the project source code if you wish:
void D3DRenderer::CreateViews()
{
mCommandList->Reset(mDirectCmdListAlloc.Get(), nullptr);
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(
mRtvHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < SwapChainBufferCount; i++)
{
ThrowIfFailed(mSwapChain->GetBuffer(
i,
IID_PPV_ARGS(&mSwapChainBuffer[i])));
md3dDevice->CreateRenderTargetView(mSwapChainBuffer[i].Get(),
nullptr,
rtvHeapHandle);
rtvHeapHandle.Offset(1, mRtvDescriptorSize);
}
// Create the depth/stencil buffer and view.
D3D12_RESOURCE_DESC depthStencilDesc;
depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
depthStencilDesc.Alignment = 0;
depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.DepthOrArraySize = 1;
depthStencilDesc.MipLevels = 1;
depthStencilDesc.Format = mDepthStencilFormat;
depthStencilDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
depthStencilDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
// Added to allow to compile in conformance mode - 20/3/24 CH
CD3DX12_HEAP_PROPERTIES heapProperties(D3D12_HEAP_TYPE_DEFAULT);
D3D12_CLEAR_VALUE optClear;
optClear.Format = mDepthStencilFormat;
optClear.DepthStencil.Depth = 1.0f;
optClear.DepthStencil.Stencil = 0;
ThrowIfFailed(md3dDevice->CreateCommittedResource(
&heapProperties,
D3D12_HEAP_FLAG_NONE,
&depthStencilDesc,
D3D12_RESOURCE_STATE_COMMON,
&optClear,
IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())));
// Create descriptor to mip level 0 of entire resource using the format of the
// resource.
// (Note DepthStencilView() returns a handle to mDsvHeap - 2/9/24 CH)
md3dDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(),
nullptr, DepthStencilView());
// Added to allow to compile in conformance mode - 27/8/24 CH
CD3DX12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition(
mDepthStencilBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON,
D3D12_RESOURCE_STATE_DEPTH_WRITE);
// Transition the resource from its initial state to be used as a depth buffer.
mCommandList->ResourceBarrier(1, static_cast<D3D12_RESOURCE_BARRIER*>(&barrier));
// Execute the commands.
ThrowIfFailed(mCommandList->Close());
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
FlushCommandQueue();
return;
}
I worry I may be getting ahead of myself a bit here, so that’s a good time to stop. That’s as much as I can reliably say about descriptors. I, like you, will learn more as the project progresses. This is where I will again recommend getting hold of a copy of:
Introduction to 3D Game Programming with DirectX 12 by Frank Luna.
If you’ve made it even this far, I feel you will need that book. There’s a lot more to the swap chain, depth/stencil buffer than I’ve covered here (although you may see some evidence of its complexity in the code snippets I’ve shown). They are quite complex things to setup, and if you browse the source code from the download link at the end of this chapter, you’ll be able to see that for yourself. They require many variables to be loaded into various types of structs, before calling native factory or device functions that use these structs as arguments to those native functions. And that’s far from a full summary of what is required. Lacking this insight, you will struggle to get them to work if you write code that departs at all from this project.
I feel I’ve covered descriptors and swap chains together in the previous paragraphs as well as this chapter requires. I recommend further research though. The last two things to discuss for this part are command objects and fences, and they are related. They’ll be discussed in turn.
The GPU has a command queue. You can picture it a bit like a tube full of draughts board pieces (or Connect4 pieces if you know what that is). The tube fills up and the piece at the bottom is processed first, and then removed. Then the next one is processed and so on.
We have to initialise a command queue for our project. Whether or not we’re creating a command queue that doesn’t already exist, or whether we’re just creating an interface to one that’s already on the GPU, I’m unsure at this time. It doesn’t matter much though, we just need to know what to do.
The command ‘entity’ so to speak is comprised of three parts. A queue, a list, and an allocator. We need to make all three. It’s not all that intuitive at first exactly what these different things are. I’m still a bit mystified by it. One important thing to note is that when we have finished recording commands we have to close the command list before proceeding. It’s usage is quite strict.
The ‘list’ is where we actually add commands to the ‘allocator’. According to the documentation the list does not immediately get added to the queue. The allocator stores the commands we enter from the list and the ‘queue’ then reads from the allocator later on.
What this means as far as I can tell the commands we enter through the list are actually stored in the allocator, not the list itself. Either that or the commands live on the list until we close the command list and are then sent to the allocator. The allocator could be thought of as the list’s ‘hidden integrity’. You’d think from reading the code that all the commands are stored in the list and then sent to the queue, but that’s not the case.
Pondering too long upon this will get confusing. I’ve simplified it in my mind as follows:
Commandlist
(goes to) -> CommandAllocator (hidden)
(goes to) -> CommandQueue
And you hardly ever (if at all) see anything mentioned about the allocator, except when you first create it (I believe it’s referenced once again later when it’s ready to be reset).
Most of the time you will see lines of code recording commands with the command list. When that’s finished you’ll see a line of code that closes the command list. After that you’ll see a command from the command queue interface (another COM interface – which all of the command entities are) which executes the commands we recorded in the command list. That’s when the command queue will read the data stored in the allocator.
As I write this I’m aware that was not a brilliant explanation of what it is. It’s a start however, and enough to give you some idea. Beyond what I’ve written so far I’d be guessing, and the command objects are not a good place to be doing that. So far I’ve had the least amount of trouble in this project from command objects. If I started doing anything of my own ideas with them, I suspect they’d become the most troublesome part of the project.
That’s a longwinded way of saying be careful with the command objects. Below is the code that creates the three command objects, pretty much exactly as it appears in the recommended text book:
void D3DRenderer::CreateCommandObjects()
{
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateCommandQueue(
&queueDesc, IID_PPV_ARGS(&mCommandQueue)));
ThrowIfFailed(md3dDevice->CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf())));
ThrowIfFailed(md3dDevice->CreateCommandList(
0,
D3D12_COMMAND_LIST_TYPE_DIRECT,
mDirectCmdListAlloc.Get(), // Associated command allocator
nullptr, // Initial PipelineStateObject
IID_PPV_ARGS(mCommandList.GetAddressOf())));
// Start off in a closed state. This is because the first time we refer
// to the command list we will Reset it, and it needs to be closed before
// calling Reset.
mCommandList->Close();
}
You can see the first thing declared is a struct of type D3D12_COMMAND_QUEUE_DESC. Then some fields of this struct are set, namely the command list type, and the flags field. We won’t be discussing those here, although they are discussed in the book I’ve recommended. After that you can see the queue is initialised, then the allocator, and finally the list. Notice in particular the list wants a pointer to the allocator that’s been created as the third argument to the function, demonstrating their relationship we discussed above.
So the three command objects are now created, and the list starts off in a closed state, which you can see described in the code comments in the code snippet above. And below you’ll see a sample of the list and queue being used in the draw function:
if (ChangeColour)
{
mCommandList->ClearRenderTargetView(CurrentBackBufferView(),
DirectX::Colors::LightSteelBlue, 0, nullptr);
}
else
{
mCommandList->ClearRenderTargetView(CurrentBackBufferView(),
DirectX::Colors::Coral, 0, nullptr);
}
....some time later
// Done recording commands.
ThrowIfFailed(mCommandList->Close());
// Add the command list to the queue for execution.
ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
The ClearRenderTargetView() method just instructs the current back buffer to be cleared to a specific colour. You can see here I gave the user an option to change between two colours, anytime during the running of the program. The ChangeColour variable is a boolean that the user changes by pressing the ‘F2’ key. This can done over and over again during the program to make the colour switch from light steel blue to coral. Lastly in the snippet above you can see the list being closed and then the queue calling its ExecuteCommandLists() method to begin executing commands on the GPU.
Note it treats the mCommandList object rather like an array. Perhaps that answers one of my question from above! Also note there’s no mention here of the allocator in the last few lines, despite the fact we know the command lists are stored in the allocator. This must happen “behind the scenes”, and we don’t see the allocator being referred to explicitly here.
The very last thing to discuss is the ‘fence’ object. This will be a very short description as if you wish to know more you can consult the recommended book or Microsoft Documentation online. We created it already in the InitDirectX() function near the start of this chapter.
ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&mFence)))
The fence allows us to pause until the command queue has definitely finished executing all currently recorded commands. Without it we can’t. There are times when we wish to wait until the GPU has executed all commands in the queue before proceeding. This is important before we attempt to re-use the memory in any of the command objects. If you look through the source code you will see a few times where the method FlushCommandQueue() is used. This does what it says it does. It needs the fence however to do it. Have a look at the function below:
void D3DRenderer::FlushCommandQueue()
{
// Advance the fence value to mark commands up to this fence point.
mCurrentFence++;
// Add an instruction to the command queue to set a new fence point. Because we
// are on the GPU timeline, the new fence point won't be set until the GPU
// finishes processing all the commands prior to this Signal().
ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));
// Wait until the GPU has completed commands up to this fence point.
if (mFence->GetCompletedValue() < mCurrentFence)
{
HANDLE eventHandle = CreateEventExW(nullptr, nullptr, false,
EVENT_ALL_ACCESS);
// Fire event when GPU hits current fence.
ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));
// Wait until the GPU hits current fence event is fired.
WaitForSingleObject(eventHandle, INFINITE);
CloseHandle(eventHandle);
}
}
That causes us to wait on the GPU until it’s ready. I don’t have time to discuss exactly how this works, you can do your own research here if you wish. I also don’t have time to discuss exactly when we do and don’t need to flush the command queue either.
In fact at this, the end, of this part of the chapter, now would be a good time to inform you that explanations will only continue to get sparser from here on in. If you haven’t already done so, you will need to start scrutinising the source code associated with this project. From here, things will start to get very complicated and a detailed description of each part will not be possible (I am not writing a book) so you will need to study the source code as well as reading the chapters to get a proper idea of what is happening. You will also need to accompany this with your own research.
I spend a large amount of my time in coding just studying source code. Including my own and the parts I’ve assembled from other authors. In fact there are still parts of this project I occasionally forget about, and then have to slightly re-learn when I look at them again. Personally I cannot recommend scrutinising source code highly enough, when it comes to getting properly familiar with your code and what it’s doing.
If I were to give an entire block by block explanation of this (very small) project here it would be a silly amount of pages long, and would be very boring to read too! I think by now we’ve covered everything we needed to for this chapter, and a short summary will soon follow. If you’ve no interest in scrutinising the source code, then there’s not much point you trying to follow this blog anymore – unless you just read it for pleasure, or find later more interesting parts of it engaging. At the moment it’s very heavy on code, with not much to show on screen. But that’s how it goes at the beginning 🙂
A SUMMARY (FINALLY!)
Well that was a bit more than I was intending to write. I hope however I’ve given a reasonable overview of some of the new concepts in this latest version of the project. Descriptions of them, by design, are not exhaustive however. I just did not want anyone who may be following the blog to suddenly see a load of new things with no discussion of what they are.
In summary we have:
– Made some slight changes in the way the rendering class resides in memory
– Split the factory and device creation function into two separate parts
– Changed the way we call the dialog box to make it automatic (see the
ReturnClientDimensions() function in source code)
– Made actual use of the values the user inputs into the display choices dialog box
– Created the new things the device needs to do basic rendering (command objects,
descriptors, swap chain, fence etc…)
– Added a little option to change the colour of the screen from cyan to coral
It sounds almost simple, but the source code is quite a lot bigger than previous chapters. As already stated, if you have a genuine interest and wish to follow exactly what the project is doing you will need to study the source code.
And that for now, is it. The next chapter will have a few descriptions of some things we need to know when rendering geometry, which is the only real goal we have here. No one’s going to be too impressed for too long by a screen that just shows blank colours! And the next chapter after that will (hopefully) be rendering actual geometry to the screen. Probably just a cube at first, but we’ll see 🙂 I’ll add the download link below. As always, run a virus scan on the download before opening it, just in case.
Thank you for reading.
