Skip to content

Hello D3D11

In this chapter, we'll introduce you to the basics of using D3D11; how to create a ID3D11Device and how to use it to show something in our window. In the last chapter we set up a basic implementation for an application with a window through GLFW. The implementation for Main.cpp and Application.cpp won't be shown here anymore.

If you are looking at the source code for this chapter, you will also notice that Application.cpp and Application.hpp do not exist anymore, as we have moved both of these files into a separate Framework project that creates a static library to ease development between chapters. This Framework project will include code that is shared between all chapters, so it might include a lot of other files which are not used or are not relevant within some chapters.

The Framework project can be found here.

Please note that the code for already existing files is also subject to change to accommodate newer chapters and their needs.

However, let's start by breaking down the relevant bits and pieces by showing you how the new class, which derives from Application will look like.

HelloD3D11Application.hpp

#pragma once

#include <d3d11.h>
#include <dxgi1_3.h>
#include <wrl.h>

#include <Application.hpp>

class HelloD3D11Application final : public Application
{
    template <typename T>
    using ComPtr = Microsoft::WRL::ComPtr<T>;

public:
    HelloD3D11Application(const std::string& title);
    ~HelloD3D11Application() override;

protected:
    bool Initialize() override;
    bool Load() override;
    void OnResize(
        int32_t width,
        int32_t height) override;
    void Update() override;
    void Render() override;

private:
    bool CreateSwapchainResources();
    void DestroySwapchainResources();

    ComPtr<ID3D11Device> _device = nullptr;
    ComPtr<ID3D11DeviceContext> _deviceContext = nullptr;
    ComPtr<IDXGIFactory2> _dxgiFactory = nullptr;
    ComPtr<IDXGISwapChain1> _swapChain = nullptr;
    ComPtr<ID3D11RenderTargetView> _renderTarget = nullptr;
};

HelloD3D11Application.cpp

And the implementation side

#include "HelloD3D11Application.hpp"

#include <GLFW/glfw3.h>
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3native.h>

#include <DirectXMath.h>
#include <d3dcompiler.h>

#include <iostream>

#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3dcompiler.lib")
#pragma comment(lib, "winmm.lib")
#pragma comment(lib, "dxguid.lib")

HelloD3D11Application::HelloD3D11Application(const std::string& title)
    : Application(title)
{
}

HelloD3D11Application::~HelloD3D11Application()
{
}

bool HelloD3D11Application::Initialize()
{
    return true;
}

bool HelloD3D11Application::Load()
{
    return true;
}

bool HelloD3D11Application::CreateSwapchainResources()
{
    return true;
}

void HelloD3D11Application::DestroySwapchainResources()
{
}

void HelloD3D11Application::OnResize(
    const int32_t width,
    const int32_t height)
{
}

void HelloD3D11Application::Update()
{
}

void HelloD3D11Application::Render()
{
}

HelloD3D11Application.hpp

#include <d3d11.h>
#include <dxgi1_3.h>
#include <wrl.h>

HelloD3D11Application.cpp

#include <GLFW/glfw3.h>
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3native.h>

#include <d3dcompiler.h>
#include <DirectXMath.h>

We need to include the following headers, here's what each of these headers includes:

  • d3d11.h: The core of D3D11, it contains all the ID3D11XXX types and most of the enums we will be using with D3D11
  • dxgi1_3.h: The core of DXGI, it contains all the IDXGIXXX types and additional enums that are required for DXGI structures
  • d3dcompiler.h: Contains all the functions necessary to compiler our HLSL shaders into bytecode that will be fed into the GPU
  • DirectXMath.h: DirectX's own math library, it contains all the types and math functions we will be using throughout the series
  • wrl.h: Is used for Microsoft::WRL::ComPtr<T>, to manage COM resources automatically.
#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3dcompiler.lib")
#pragma comment(lib, "winmm.lib")
#pragma comment(lib, "dxguid.lib")

Of course just including the headers isn't enough, we must also link against D3D11 & friends to be able to actually use the stuff declared in the headers, put these #pragma comment(lib, "PATH_TO_LIB") in HelloD3D11Application.cpp right below the includes to link these libraries.

ComPtr<IDXGIFactory2> _dxgiFactory = nullptr;
ComPtr<ID3D11Device> _device = nullptr;
ComPtr<ID3D11DeviceContext> _deviceContext = nullptr;
ComPtr<IDXGISwapChain1> _swapChain = nullptr;
ComPtr<ID3D11RenderTargetView> _renderTarget = nullptr;

You might have noticed that we are not using raw pointers for those pieces, but ComPtr. DirectX is built on top of COM (Component Object Model) and with that COM objects utilize reference counting to manage object lifetimes, in form of AddRef and Release methods. ComPtr<T> wraps that functionlity for us, by creating a smart pointer. You can find more information about it here.

IDXGIFactory2 helps us find an adapter we can use to run our graphics on. It can enumerate all existing adapters (GPUs), of which there could be several installed in your system. If you have a laptop there is most likely an integrated one coming with your cpu, but often these days laptops also have a dedicated GPU as well, or your PC might have more than one dedicated GPUs installed. With IDXGIFactory2 we can pick one. It also creates the swapchain for us, a surface to store rendered data before presenting it to an output (or screen).

ID3D11Device is the object which we use to create all sorts of things, buffers, textures, samplers, shaders.

ID3D11DeviceContext is the one we use to issue draw and compute commands to the GPU.

IDXGISwapChain1 The aforementioned surface, which stores rendered data which it can present to an output (or screen).

ID3D11RenderTargetView Is a fancy pointer to a texture, this tells D3D11 that the texture this points to, is drawable within the subresource of the referenced texture

DXGI stands for DirectX Graphics Infrastructure, in case you are wondering.

Let's go in to Initialize

if (!Application::Initialize())
{
    return false;
}

if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&_dxgiFactory))))
{
    std::cout << "DXGI: Unable to create DXGIFactory\n";
    return false;
}

The first part calls the parent class, where GLFW is initialized and setup.

IID_PPV_ARGS(ppType) Is a compile-time macro that is defined as

#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)
Which means that typing IID_PPV_ARGS(&_dxgiFactory) it is expanded by the compiler into __uuidof(**(&_dxgiFactory)), IID_PPV_ARGS_Helper(_dxgiFactory). This functionally means that for functions that have a parameter setup as REFIID and functionally after a [out] void** parameter, this macro will expand the IID_PPV_ARGS(ppType) expression into these parameters for ease of use — this can be seen with the used CreateDXGIFactory1 method where the parameters are a REFIID and void**:
HRESULT CreateDXGIFactory1(
        REFIID riid,
  [out] void   **ppFactory
);
REFIID is a typedef that is a Reference (REF) to an Interface Identifier type (IID) — this means that it is a reference to a type that uniquely identifies a COM object. [more information like underlying memory organization can be read about IID's at https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/iid]

What the parts of the IID_PPV_ARGS(ppType) macro are:

[the ppType in IID_PPV_ARGS(ppType)] - a pointer to a pointer of an object.

[the __uuidof(**(ppType)) part of IID_PPV_ARGS(ppType)] - at compile time retrieves a UUID from ppType type which represents a GUID, which is returned as a REFIID — which means that the type returned is a reference to an identifier to a specific type of COM object.

Explain DXGI

https://docs.microsoft.com/en-us/windows/win32/direct3ddxgi/d3d10-graphics-programming-guide-dxgi

CreateDXGIFactory1 is the entry point to create a factory for us, a IDXGIFactory1 to be precise. There are various implementations of it, depending on what version you aim for, you get additional functionality.

DXGI 1.0 up to 1.6 More information can be found here We will stick with IDXGIFactory1 for now.

constexpr D3D_FEATURE_LEVEL deviceFeatureLevel = D3D_FEATURE_LEVEL::D3D_FEATURE_LEVEL_11_0;
if (FAILED(D3D11CreateDevice(
    nullptr,
    D3D_DRIVER_TYPE::D3D_DRIVER_TYPE_HARDWARE,
    nullptr,
    0,
    &deviceFeatureLevel,
    1,
    D3D11_SDK_VERSION,
    &_device,
    nullptr,
    &_deviceContext)))
{
    std::cout << "D3D11: Failed to create device and device Context\n";
    return false;
}

This block is the entry point into D3D11, where we ask for a device and its device context to be created. The input parameters are:

We want a LEVEL_11_0, hardware accelerated device, which has support for a specific color format. Feature levels are a concept that has been introduced with D3D11, it is a way to specify which set of features we would like to use. Each GPU may support different feature levels (for example a very old GPU might only support LEVEL_9_1, while a more modern one may support every feature level up to, and including LEVEL_11_0), this is a way to avoid rewriting our application in D3D9 just because our GPU doesn't support D3D11.

If D3D11CreateDevice succeeds we will get a ID3D11Device and a ID3D11DeviceContext back.

DXGI_SWAP_CHAIN_DESC1 swapChainDescriptor = {};
swapChainDescriptor.Width = GetWindowWidth();
swapChainDescriptor.Height = GetWindowHeight();
swapChainDescriptor.Format = DXGI_FORMAT::DXGI_FORMAT_B8G8R8A8_UNORM;
swapChainDescriptor.SampleDesc.Count = 1;
swapChainDescriptor.SampleDesc.Quality = 0;
swapChainDescriptor.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDescriptor.BufferCount = 2;
swapChainDescriptor.SwapEffect = DXGI_SWAP_EFFECT::DXGI_SWAP_EFFECT_FLIP_DISCARD;
swapChainDescriptor.Scaling = DXGI_SCALING::DXGI_SCALING_STRETCH;
swapChainDescriptor.Flags = {};

DXGI_SWAP_CHAIN_FULLSCREEN_DESC swapChainFullscreenDescriptor = {};
swapChainFullscreenDescriptor.Windowed = true;

if (FAILED(_dxgiFactory->CreateSwapChainForHwnd(
    _device.Get(),
    glfwGetWin32Window(GetWindow()),
    &swapChainDescriptor,
    &swapChainFullscreenDescriptor,
    nullptr,
    &_swapChain)))
{
    std::cout << "DXGI: Failed to create swapchain\n";
    return false;
}

After we successfully create device and device context, the next step is to create a swapchain, that storage containing the rendered images which we can present to the screen.

The majority of values should make some sense without explanation, like width and height, and whether we want it to support a windowed window or not.

BufferUsage tells the swapchain's buffers their usage, something we render to, and can present.

Scaling tells DXGI how to scale the buffer's contents to fit the presentation's target size.

BufferCount is 2, because we want double buffering. Double buffering is an age-old technique to avoid presenting an image that is being used by the GPU, instead we work on the "back buffer", while the GPU is happy presenting the "front buffer", then, as soon as we are done with the back buffer, we swap front and back, and begin working on the former front buffer present that one and render to the other one again in the meantime. That process is supposed to reduce flicker or tearing.

SwapEffect specifies if the contents of the back buffer should be preserved or discarded after a swap, here we don't care about preserving the back buffer, so we just discard everything.

AlphaMode specifies how DXGI should handle transparency, we don't care about that (yet), so we'll just say it's unspecified and rely on default behaviour

if (!CreateSwapchainResources())
{
    return false;
}

return true;

And the last bits of the Initialize method.

We need to create a few more things. Those are based on the swapchain, hence their name. These resources need to be destroyed and recreated whenever we want to resize the window. When that happens, the swapchain needs to be resized as well (since that is a prameter in its descriptor as you can see above)

bool HelloD3D11Application::CreateSwapchainResources()
{
    ComPtr<ID3D11Texture2D> backBuffer = nullptr;
    if (FAILED(_swapChain->GetBuffer(
        0,
        IID_PPV_ARGS(&backBuffer))))
    {
        std::cout << "D3D11: Failed to get Back Buffer from the SwapChain\n";
        return false;
    }

    if (FAILED(_device->CreateRenderTargetView(
        backBuffer.Get(),
        nullptr,
        &_renderTarget)))
    {
        std::cout << "D3D11: Failed to create RTV from Back Buffer\n";
        return false;
    }

    return true;
}

When we render things, the GPU simply writes color values to a texture, which you can picture as a buffer which holds color information Swapchain is a container to manage those buffers we want to present on screen. To do that we have to create a special kind of texture called a "Render Target View" or an RTV. First off we have to grab a texture from the swapchain's main buffer (index 0), from that texture, we now have to create an RTV from that, which specifies the subresource of the texture that we will be drawing to. We won't keep the actual texture around, we just need the render target view, which we will refer to as render target.

void HelloD3D11Application::DestroySwapchainResources()
{
    _renderTarget.Reset();
}

The render target needs to be disposed when we want to resize (or cleanup in general), it will be recreated via CreateSwapchainResources when we resize the window as shown here:

void HelloD3D11Application::OnResize(
    const int32_t width,
    const int32_t height)
{
    Application::OnResize(width, height);
    _deviceContext->Flush();

    DestroySwapchainResources();

    if (FAILED(_swapChain->ResizeBuffers(
        0,
        width,
        height,
        DXGI_FORMAT::DXGI_FORMAT_B8G8R8A8_UNORM,
        0)))
    {
        std::cout << "D3D11: Failed to recreate SwapChain buffers\n";
        return;
    }

    CreateSwapchainResources();
}

When we resize, let the base application know about it, and make sure the device context has done all its work (Flush)

Before we can resize the swapchain, make sure all resources based on it are disposed. Afterwards recreate them with the new dimensions of the swapchain

void HelloD3D11Application::Render()
{
    D3D11_VIEWPORT viewport = {};
    viewport.TopLeftX = 0;
    viewport.TopLeftY = 0;
    viewport.Width = static_cast<float>(GetWindowWidth());
    viewport.Height = static_cast<float>(GetWindowHeight());
    viewport.MinDepth = 0.0f;
    viewport.MaxDepth = 1.0f;

    constexpr float clearColor[] = { 0.1f, 0.1f, 0.1f, 1.0f };

    _deviceContext->ClearRenderTargetView(
        _renderTarget.Get(),
        clearColor);
    _deviceContext->RSSetViewports(
        1,
        &viewport);
    _deviceContext->OMSetRenderTargets(
        1,
        _renderTarget.GetAddressOf(),
        nullptr);
    _swapChain->Present(1, 0);
}

Now we can actually use those things we have created before. We just set it up so that we tell D3D11 that we want to render into the render target, and when we clear we want to use a dark gray.

We also have to specify an area in form of a rectangle, in this case, its equivalent to the window size.

Last but not least, we Present the content of the swapchain to the window, using Present. The first argument defines which vblanks to synchronize with presentation, 0 means: no synchronization (unlimited FPS), 1 means: sync every v-blank (regular v-sync), 2 means: sync every other v-blank and so on, up to 4. The second are optional flags, we don't need them so 0 is passed.

Application also defines an abstract method Update which we have to define here as well, so we will add:

void HelloD3D11Application::Update()
{
}

But keep it empty for now.

Same applies for Application's Load method.

bool HelloD3D11Application::Load()
{
    return true;
}

Finally, we need to modify Appplication.hpp and Application.cpp. Since we want to handle resizing as well.

Application.hpp

Find protected: and add the following lines

static void HandleResize(
    GLFWwindow* window,
    const int32_t width,
    const int32_t height);
virtual void OnResize(
    const int32_t width,
    const int32_t height);

[[nodiscard]] GLFWwindow* GetWindow() const;
[[nodiscard]] int32_t GetWindowWidth() const;
[[nodiscard]] int32_t GetWindowHeight() const;

HandleResize will be the callback from GLFW which handles resize events and OnResize will be executed when GLFW runs HandleResize, so that we can handle our custom things we want to execute when resizing the window, like changing the size of the swapchain in our example.

GetWindow() is used to derive the actual native window handle from, which is needed when we create the swapchain. GetWindowWidth() and GetWindowHeight() do what they say :) Also required for swapchain creation.

Application.cpp

Add the following lines

void Application::OnResize(
    const int32_t width,
    const int32_t height)
{
    _width = width;
    _height = height;
}

void Application::HandleResize(
    GLFWwindow* window,
    const int32_t width,
    const int32_t height)
{
    Application* application = static_cast<Application*>(glfwGetWindowUserPointer(window));
    application->OnResize(width, height);
}

GLFWwindow* Application::GetWindow() const
{
    return _window;
}

int32_t Application::GetWindowWidth() const
{
    return _width;
}

int32_t Application::GetWindowHeight() const
{
    return _height;
}

Find the Initialize method and add the following two lines before return true;

glfwSetWindowUserPointer(_window, this);
glfwSetFramebufferSizeCallback(_window, HandleResize);

glfwSetWindowUserPointer will set our application instance as a custom variable, so that we can retrieve it using glfwGetWindowUserPointer in the HandleResize callback.

glfwSetFramebufferSizeCallback will tell GLFW what to do when we resize the window, in this case execute HandleResize which will fetch our application instance and all OnResize on it, where we can handle resizing in our application code.

Project on GitHub

Next chapter