Previously, we only have the framework initialization of the Direct3D 12 API. We are now ready to get started to have something displayed on screen, a solid color.
We will work only in the Update()
, which is our function call that gets called once per frame. At a high level, these are the typical steps we need to for each frame:
- Wait for the previous frame to finish drawing
- Reset our
ID3D12CommandAllocator
andID3D12GraphicsCommandList
- Transition our Barrier to Render Target state
- Draw a color
- Transition our Barrier to the Present state
- Execute the
ID3D12GraphicsCommandList
Present
the frame- Update
m_frameIndex
to the next back buffer index
Resource Barrier Transitions
The steps that we planned out for our command list seem logical. From context, it all makes sense. We need to reset our command list, draw something, and then presenting it.
But what are barriers? and why do we need to transition states?
Barriers act as a synchronization tool, much like the ID3D12Fence
we created. The difference here is that barriers happen only on the GPU, where as the fence communicates between the CPU and GPU.
This is important as ID3D12Resource
can be used in many ways, but we have to make sure we do not start to use them if it is not ready. We do not want to cause any hazards if we are reading a resource while it has not even been produced yet.
For example, if we are in the D3D12_RESOURCE_STATE_PRESENT
state, we do not want to start drawing into that buffer, so we set it to a more suitable state D3D12_RESOURCE_STATE_RENDER_TARGET
in order to draw.
We will make extensive use of the d3dx12.h
helpers from Microsoft to make these function calls less verbose.
Updating Every Frame
In our last post, we created a fence helper function that waits for the GPU to finish it's frame before we move on. This will be the first function call we need to do.
Immediately after, we will call Reset
on the allocator and list so we start fresh.
void Update(float deltaTime) override {
WaitForPreviousFrame();
m_commandAllocator->Reset();
m_commandList->Reset(m_commandAllocator.Get(), nullptr);
}
Now, we want to start working on our render target. In order words, it is time to draw. But first, recall we have to use a barrier to transition our state.
Using the helper function, we create a barrier for the ID3D12Resource
to transition from one state to another, and then we tell the m_commandList
about this transition.
void Update(float deltaTime) override {
WaitForPreviousFrame();
m_commandAllocator->Reset();
m_commandList->Reset(m_commandAllocator.Get(), nullptr);
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(
m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET);
m_commandList->ResourceBarrier(1, &barrier);
}
It is time to draw a color. Remember that we do not work with our render target ID3D12Resource
directly. We use a descriptor
or view
as mentioned in the previous post. It is the information that tells us what the blob of ID3D12Resource
data consists of. Thus, we need a handle to thar render target descriptor.
The helper function needs some parameters in addition to the heap's start. We are working on a single, so we need to get the m_frameIndex
, which is our current index of the back buffer (usually 0 or 1). We also need the size of the descriptor, which can differ in size depending on the GPU.
void Update(float deltaTime) override {
WaitForPreviousFrame();
m_commandAllocator->Reset();
m_commandList->Reset(m_commandAllocator.Get(), nullptr);
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(
m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET);
m_commandList->ResourceBarrier(1, &barrier);
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
m_rtvHeap->GetCPUDescriptorHandleForHeapStart(),
m_frameIndex, m_rtvDescriptorSize);
}
Drawing
Since we are only doing a single background color, we only have a single function call. But it is worth explaining why we are starting out with this draw color.
The clearColor
is just an array of float
representing a color. We use this color to wipe whatever was in the previous buffer so we start at a clean state with nothing in it.
Let us create a gray color, and then use that in our ClearRenderTargetView
call in our command list.
void Update(float deltaTime) override {
WaitForPreviousFrame();
m_commandAllocator->Reset();
m_commandList->Reset(m_commandAllocator.Get(), nullptr);
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(
m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET);
m_commandList->ResourceBarrier(1, &barrier);
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
m_rtvHeap->GetCPUDescriptorHandleForHeapStart(),
m_frameIndex, m_rtvDescriptorSize);
const float clearColor[] = {0.33f, 0.33f, 0.33f, 1.0f};
m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
}
We are done drawing, so you guessed it (hopefully), we have to tell the GPU we are finished and to transition back to a Present
state so we can see our creation. The function call is almost identical to our first transition we did.
void Update(float deltaTime) override {
WaitForPreviousFrame();
m_commandAllocator->Reset();
m_commandList->Reset(m_commandAllocator.Get(), nullptr);
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(
m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET);
m_commandList->ResourceBarrier(1, &barrier);
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
m_rtvHeap->GetCPUDescriptorHandleForHeapStart(),
m_frameIndex, m_rtvDescriptorSize);
const float clearColor[] = {0.33f, 0.33f, 0.33f, 1.0f};
m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(
m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT);
m_commandList->ResourceBarrier(1, &barrier);
}
Executing the Command List
Almost there. We have now finished drawing, and transitioned back into the Present
state. Now we have to tell the m_commandList
that we are done, and to Close()
itself so no more instructions can be performed.
Recall that the ID3D12CommandQueue
is the object that performs the instructions, so we bring everything on our list over to the queue now. Let's write both of these.
void Update(float deltaTime) override {
WaitForPreviousFrame();
m_commandAllocator->Reset();
m_commandList->Reset(m_commandAllocator.Get(), nullptr);
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(
m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET);
m_commandList->ResourceBarrier(1, &barrier);
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
m_rtvHeap->GetCPUDescriptorHandleForHeapStart(),
m_frameIndex, m_rtvDescriptorSize);
const float clearColor[] = {0.33f, 0.33f, 0.33f, 1.0f};
m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(
m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT);
m_commandList->ResourceBarrier(1, &barrier);
m_commandList->Close();
ID3D12CommandList* ppCommandLists[] = {m_commandList.Get()};
m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
}
At this point, we have done everything for the GPU to create the frame, and it has! But we need to tell the swapchain to show it to us with the Present
method call.
The Present
function has several parameters depending on v-sync, but for now we are keeping it basic and just setting it to Present(1,0)
.
void Update(float deltaTime) override {
WaitForPreviousFrame();
m_commandAllocator->Reset();
m_commandList->Reset(m_commandAllocator.Get(), nullptr);
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(
m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET);
m_commandList->ResourceBarrier(1, &barrier);
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
m_rtvHeap->GetCPUDescriptorHandleForHeapStart(),
m_frameIndex, m_rtvDescriptorSize);
const float clearColor[] = {0.33f, 0.33f, 0.33f, 1.0f};
m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(
m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT);
m_commandList->ResourceBarrier(1, &barrier);
m_commandList->Close();
ID3D12CommandList* ppCommandLists[] = {m_commandList.Get()};
m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
m_swapChain->Present(1, 0);
}
Remember to tell our m_frameIndex
that our new back buffer index from the swapchain! Add this as the last function call in the Update()
.
Update(float deltaTime) override {
//previous code omitted
m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();
}
Clean Up
The COM
interfaces that Windows uses does reference counting, so many of the objects we were using will be cleaned up automatically, but there are some handles we still need to manage. In our example, we need to just close the m_fenceEvent
handle we created earlier. This can be done in the ShutDown()
method in our class.
void Shutdown() override {
WaitForPreviousFrame();
if (m_fenceEvent) {
CloseHandle(m_fenceEvent);
m_fenceEvent = nullptr;
}
}
It is good practice to let the GPU finish whatever it is doing before we start to deallocate and cleanup resources.
Conclusion
We now have successfully created frames using Direct3D 12!
This was a very minimal example to get started with the API, but it gives us many of the tools to add features later on. In later posts, we will start to draw geometry.