Skip to content

Using DeviceResources

Chuck Walbourn edited this page Jun 21, 2020 · 39 revisions

In the basic game loop lesson, we made use of the Direct3D12 Game VS template which has all the relevant Direct3D code in the Game class including creating the device and swap chain. This makes it easy to teach with, and for the reader to see all the code in one place. This, however, does result in a lot of 'boiler-plate' code in the main Game class which could be distracting.

There is therefore a "DR" variant of each of the Direct3D Game VS template in the VS 2017/2019 VSIX package which adds DeviceResources.

The standard Universal Windows Platform app DirectX12App VS templates make use of a similar DeviceResources abstraction.

Creating a new project

Visual Studio 2017

  • From the drop-down menu, select File and then New -> Project...
  • Select "Visual C++" on the left-hand tree-view.
  • Select "Direct3D12 Win32 Game DR" or "Direct3D12 UWP Game DR".
  • Fill in the "Name" and "Location" fields as desired.
  • Optional: Uncheck "Create directory for solution" to keep the directory structure as bit flatter.
  • Select "OK".

Create New Project

Visual Studio 2019

  • From the drop-down menu, select File and then New -> Project... or on the startup dialog select Create a new project
  • Select "Games" on the project types filter. You can optionally type "Win32" or "UWP" in the search box as well.
  • Select "Direct3D12 Win32 Game" or "Direct3D12 UWP Game".
  • Select "Next"
  • Fill in the "Project name" and "Location" fields as desired.
  • Optional: Check "Place solution and project in the same directory" to keep the directory structure as bit flatter.
  • Select "Create".

Create New Project

Windows 10 SDK Selection

Using DirectX 12 APIs requires the Windows 10 SDK, so the project wizard will trigger this dialog to select the Windows 10 SDK version to use. Note that for Win32 Windows desktop apps, the "Minimum version" setting is ignored.

Windows 10 SDK Selection

Newly Created Project

The newly created project contains the following files:

Open Game Cpp

  • Precompiled header files
    • pch.h
    • pch.cpp
  • Main application entry-point and classic Windows procedure function or CoreWindow class
    • Main.cpp
  • Device resources abstraction
    • DeviceResources.h
    • DeviceResources.cpp
  • Timer helper class
    • StepTimer.h
  • D3DX12 Utility Header
    • d3dx12.h
  • The Game class
    • Game.h
    • Game.cpp

The Win32 version will have the following files as well:

  • Resources
    • directx.ico
    • resource.rc
    • settings.manifest

While the UWP version will have:

  • Package.appxmanifest
  • Name_TemporaryKey.pfx
  • Assets
    • logo PNG files

CMake projects

If you prefer to make use of VS 2019's integrated CMake support and/or the clang/LLVM for Windows compiler, there are CMakeLists.txt and CMakeSettings.json files available for download on directx-vs-templates.

Running the application

Visual Studio will default to the x64 platform / Debug configuration which builds an x64 (64-bit) application with debugging enabled. The template contains both Debug and Release configurations for both x86 (32-bit) and x64 (x64 native 64-bit) platforms, with UWP also including the ARM platforms.

Press F5 to build and run the application It displays the following window:

Running Project

Troubleshooting: If the base template fails to start, there are a few possibilities. First, if your system doesn't have any Direct3D 12 capable device of any feature level, it will fail. Second if it runs fine in Release but fails in Debug, then you likely do not have the Graphics Tools Windows feature enabled which is required for the Debug Device. Third, if it runs in Debug but not in Release, then you probably don't have a Direct3D 12 capable hardware device and are only able to run the software WARP12 device--remember WARP12 is only supported on development systems.

Tour of the code

Constructor

The Game class constructor is where you can do first initialization of member variables, as well as where we create the DeviceResources instance.

Game::Game() noexcept(false)
{
    m_deviceResources = std::make_unique<DX::DeviceResources>();
    m_deviceResources->RegisterDeviceNotify(this);
}

The DeviceResources constructor takes a number of defaulted parameters to control backBufferFormat, depthBufferFormat, backBufferCount, minFeatureLevel, and option flags. You can provide specific values to change them as needed.

If doing gamma-correct rendering, you should use DXGI_FORMAT_*_UNORM_SRGB or a supported HDR format for the backBufferFormat. Be sure to update Clear below accordingly to use a linear clear color.

// Use gamma-correct rendering.
m_deviceResources = std::make_unique<DX::DeviceResources>(DXGI_FORMAT_B8G8R8A8_UNORM_SRGB);

If you do not want DeviceResources to create a depth/stencil buffer, you can use DXGI_FORMAT_UNKNOWN for depthBufferFormat. This is useful for 2D only rendering or when doing MSAA which requires handling your own render target and depth buffer with SampleDesc.Count > 1. Be sure to update Clear below to avoid referencing a null depth buffer object.

// Renders only 2D, so no need for a depth buffer.
m_deviceResources = std::make_unique<DX::DeviceResources>(DXGI_FORMAT_B8G8R8A8_UNORM,
    DXGI_FORMAT_UNKNOWN);

The minFeatureLevel defaults to 11 which is the lowest Direct3D Feature Level supported by DirectX 12 drivers. You can specify a higher hardware level if you want to take a hard dependency on additional capabilities.

Initialize

When the application first starts, execution is passed to the Initialize method. The TODO here by default leaves the applications StepTimer in the 'variable length' mode. You uncomment the code if you want StepTimer in the 'fixed-step' mode. We'll explain this more once we get to Update.

void Game::Initialize(HWND window, int width, int height)
{
    m_deviceResources->SetWindow(window, width, height);

    m_deviceResources->CreateDeviceResources();
    CreateDeviceDependentResources();

    m_deviceResources->CreateWindowSizeDependentResources();
    CreateWindowSizeDependentResources();

    // TODO: Change the timer settings if you want something other than the default variable timestep mode.
    // e.g. for 60 FPS fixed timestep update logic, call:
    /*
    m_timer.SetFixedTimeStep(true);
    m_timer.SetTargetElapsedSeconds(1.0 / 60);
    */
}

The first Game method Initialize calls is CreateDeviceDependentResources for the creation of objects that depend on the device, but do not care about the size of the rendering window.

void Game::CreateDeviceDependentResources()
{
    auto device = m_deviceResources->GetD3DDevice();

    // TODO: Initialize device dependent objects here (independent of window size).
    device; // This is only here to avoid a warning. You can remove it once you make use of device!
}

Instead of using the class variable m_d3dDevice we have to obtain the device interface from the DeviceResources object. See Render for how you get the graphics command list from DeviceResources.

The second Game method Initialize calls is CreateWindowSizeDependentResources for creation of objects that depend on the size of the rendering window. Note that this function could be creating these objects for the first time, it could be re-creating already existing objects due to a window-size change, or could be creating 'fresh' objects after a Direct3D device-removed or device-reset case.

void Game::CreateWindowSizeDependentResources()
{
    // TODO: Initialize windows-size dependent objects here.
}

Update

The Update method is intended to handle game-world state modification which is typically driven by time passing, simulation, and/or user-input. By default, Update is called once per 'frame' and can have an arbitrary delta-time. This is called a 'variable-step' mode.

If in the Initialize method above you uncomment the TODO code, then each Update will be for a fixed time-step (1/60th of a second), with Update called as many time in a single 'frame' as needed to keep it up-to-date. This is called a 'fixed-step' mode and potentially be more stable for many kinds of simulations.

void Game::Update(DX::StepTimer const& timer)
{
    PIXBeginEvent(PIX_COLOR_DEFAULT, L"Update");

    float elapsedTime = float(timer.GetElapsedSeconds());

    // TODO: Add your game logic here.
    elapsedTime;

    PIXEndEvent();
}

PIXBeginEvent and PIXEndEvent provide annotations for the Visual Studio Graphics Diagnostics tool (aka VSPIX).

Render

The Render function which should render a single 'frame' of the scene, which starts with a DeviceResources::Prepare and Clear of the render target, and setting the rendering viewport & scissors. It ends with a call to DeviceResources::Present to show the rendered frame.

void Game::Render()
{
    // Don't try to render anything before the first Update.
    if (m_timer.GetFrameCount() == 0)
    {
        return;
    }

    // Prepare the command list to render a new frame.
    m_deviceResources->Prepare();
    Clear();

    auto commandList = m_deviceResources->GetCommandList();
    PIXBeginEvent(commandList, PIX_COLOR_DEFAULT, L"Render");

    // TODO: Add your rendering code here.

    PIXEndEvent(commandList);

    // Show the new frame.
    PIXBeginEvent(m_deviceResources->GetCommandQueue(), PIX_COLOR_DEFAULT, L"Present");
    m_deviceResources->Present();
    PIXEndEvent(m_deviceResources->GetCommandQueue());
}

Instead of using the class variable m_commandList we have to obtain the graphics command list interface from the DeviceResources object. See CreateDeviceDependentResources for how you get the device from DeviceResources.

Prepare

In the non-DR versions of the template, the Clear method would perform the initial command list and allocator reset, as well as transition the render target to the proper state which is required to begin rendering a new frame. In the DeviceResources version, this is handled by the DeviceResources::Prepare method while Clear still performs the actual render target and depth/stencil buffer clear.

Clear

The Clear method defaults to a background color of the classic "Cornflower blue".

void Game::Clear()
{
    auto commandList = m_deviceResources->GetCommandList();

    // Clear the views.
    auto rtvDescriptor = m_deviceResources->GetRenderTargetView();
    auto dsvDescriptor = m_deviceResources->GetDepthStencilView();

    commandList->OMSetRenderTargets(1, &rtvDescriptor, FALSE, &dsvDescriptor);
    commandList->ClearRenderTargetView(rtvDescriptor, Colors::CornflowerBlue, 0, nullptr);
    commandList->ClearDepthStencilView(dsvDescriptor, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);

    // Set the viewport and scissor rect.
    auto viewport = m_deviceResources->GetScreenViewport();
    auto scissorRect = m_deviceResources->GetScissorRect();
    commandList->RSSetViewports(1, &viewport);
    commandList->RSSetScissorRects(1, &scissorRect);
}

If you are using gamma-correct rendering with a sRGB or HDR backbuffer format, you need to ensure you are using a linear RGB clear color. DirectXMath colors are defined in sRGB colorspace since they are .NET color constants, so you need to replace ClearRenderTargetView in Clear with:

// Use linear clear color for gamma-correct rendering.
XMVECTORF32 color;
color.v = XMColorSRGBToRGB(Colors::CornflowerBlue);
commandList->ClearRenderTargetView(rtvDescriptor, color, 0, nullptr);

If you chose to not have DeviceResources create a depth-stencil buffer (see the Constructor section), you need to update Clear to avoid reference to a null depth buffer object.

void Game::Clear()
{
    auto commandList = m_deviceResources->GetCommandList();

    // Clear the views.
    auto rtvDescriptor = m_deviceResources->GetRenderTargetView();

    commandList->OMSetRenderTargets(1, &rtvDescriptor, FALSE, nullptr);
    commandList->ClearRenderTargetView(rtvDescriptor, Colors::CornflowerBlue, 0, nullptr);

    // Set the viewport and scissor rect.
    auto viewport = m_deviceResources->GetScreenViewport();
    auto scissorRect = m_deviceResources->GetScissorRect();
    commandList->RSSetViewports(1, &viewport);
    commandList->RSSetScissorRects(1, &scissorRect);
}

Present

In DirectX 12, the application is responsible for implementing the swapping of the back buffer. The DeviceResources::Present method of inserts a barrier to transition the render target to the present state, closes and executes the command list, presents the current frame, and then advances to the next frame. If needed, any fencing or blocking of rendering due to too many queued frames takes place here as well.

Events

The template includes a number of message handlers that are called for process state changes: OnActivated, OnDeactivated, OnSuspending, OnResuming, and OnWindowSizeChanged. The UWP version also includes ValidateDevice, and display orientation is provided long with the window size.

Since we are using ComPtr, most cleanup is automatic when the Game class is destroyed. If Present encounters a device-removed or device-reset, then the application needs to release all Direct3D objects and recreate the device, swapchain, and all Direct3D objects again. Therefore, the TODO in OnDeviceLost should be updated to release your application's Direct3D objects.

void Game::OnDeviceLost()
{
    // TODO: Add Direct3D resource cleanup here

...
}

You will not get "device lost" all that often. In legacy Direct3D 9, you would routinely get a 'device lost' if you just ALT+TAB away from the application because the GPU used to be an 'exclusive' rather than 'shared' resource. The situation where you'd get DXGI_ERROR_DEVICE_RESET is if the driver crashes or the video hardware hangs. You get DXGI_ERROR_DEVICE_REMOVED if a new driver is installed while your application is running, or if you are running on a 'GPU is in the dock' style laptop and the laptop is undocked. You can test this case by opening the Developer Command Prompt for Visual Studio as an administrator, and typing dxcap -forcetdr which will immediately cause all currently running Direct3D apps to get a DXGI_ERROR_DEVICE_REMOVED event.

Smart-pointer

We make use of the Microsoft::WRL::ComPtr smart-pointer for managing the lifetime of the Direct3D 12 COM objects. See ComPtr for more information and usage.

Error handling

Many Direct3D functions return an HRESULT which is the standard for COM APIs. For robustness and easier debugging, it is important that you always check the result of every function that return an HRESULT. If you really can safely assume there is no error condition for a particular function, the function itself will return void instead of HRESULT.

The Win32 game template makes use of the helper function ThrowIfFailed in the DX C++ namespace. This is the same helper that is used by the Windows Store and Windows phone VS templates. This helper throws a C++ exception if the standard FAILED macro returns true for a given HRESULT.

DX::ThrowIfFailed(evice->CreateCommandQueue(&queueDesc,
    IID_PPV_ARGS(m_commandQueue.ReleaseAndGetAddressOf())));

Do not use hr == S_OK to check for success. Use SUCCEEDED(hr) instead.

The DR VS template variants include the enhanced version of ThrowIfFailed.

Tutorial series

You can interchange the DR and non-DR variants by using these instructions:

When directed to add something to CreateDevice, add it to CreateDeviceDependentResources instead.

When asked to add something to CreateResources, add it to CreateWindowSizeDependentResources instead.

Anywhere you are asked to use m_d3dDevice.Get(), use m_deviceResources->GetD3DDevice() instead:

auto device = m_deviceResources->GetD3DDevice();
m_states = std::make_unique<CommonStates>(device);

Anywhere you are asked to use m_d3dCommandList.Get(), use m_deviceResources->GetCommandList() instead:

auto commandList = m_deviceResources->GetCommandList();
m_spriteBatch->Begin(commandList);

Anywhere you are asked to use m_commandQueue.Get(), use m_deviceResources->GetCommandQueue() instead:

m_graphicsMemory->Commit(m_deviceResources->GetCommandQueue());

When asked to use backBufferWidth or backBufferHeight, use m_deviceResources->GetOutputSize() instead:

auto size = m_deviceResources->GetOutputSize();
m_screenPos.x = size.right / 2.f;
m_screenPos.y = size.bottom / 2.f;

When asked to use backBufferCount, use m_deviceResources->GetBackBufferCount() instead.

Next lesson: Adding the DirectX Tool Kit

Further reading

Direct3D Win32 Game Visual Studio template
Direct3D Game Visual Studio templates (Redux)
Manifest Madness
64-bit programming for Game Developers

For Use

  • Universal Windows Platform apps
  • Windows desktop apps
  • Windows 11
  • Windows 10
  • Xbox One
  • Xbox Series X|S

Architecture

  • x86
  • x64
  • ARM64

For Development

  • Visual Studio 2022
  • Visual Studio 2019 (16.11)
  • clang/LLVM v12 - v18
  • MinGW 12.2, 13.2
  • CMake 3.20

Related Projects

DirectX Tool Kit for DirectX 11

DirectXMesh

DirectXTex

DirectXMath

Tools

Test Suite

Model Viewer

Content Exporter

DxCapsViewer

See also

DirectX Landing Page

Clone this wiki locally