Drawing a Color in Direct3D 12

Jul 17, 2025
7 min read

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 and ID3D12GraphicsCommandList
  • 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();
}
This is our last line of the Update()

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!

Direct3D 12 Window

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.

Further Reading

Graphics