I'm making my first attempt at learning DX11, and the goal is to add the API to my rendition of The Cherno's Hazel Engine. Along the OpenGL track, I've got Dear ImGui working and I can transform a textured quad. Huzzah.
I've got Direct3D up and running, but something is awry. My window has the appropriate clear color, but doesn't even display the FPS/resource utilization OSD from RivaTuner. It seems reasonable to me that means I'm swapping the buffers before drawing to the back buffer, but for the life of me I can't isolate the mistake. I've based what code I do have on the Microsoft Getting Started tutorials. I'll post as much meaningful code as I can without going overboard, hopefully.
There are five classes which matter here:
Application: owns the window, the rendering API, and runs the main loopWindow: owns the rendering context and handles the Win32 callbacksRenderingContext: holds all of the device/display/etc information, and is responsible for swapping buffersRenderAPI: clears the back buffer and receives draw callsLayer: a means by which to order drawing different elements
The main loop (in Application) runs through the layer stack and calls everyone's update() function, then calls update() on Window and repeats. The "client" is expected to push their own Layer subclass onto the stack as well.
It's convoluted for how simple it is, but this is design/architecture practice as much as it's about graphics or games. Anyway, all of the above works with OpenGL, but with D3D I get the aforementioned cleared-but-blank screen.
I'll start with the main loop and post the more complete setup code below.
void Application::run() {
while(_running) {
for(auto layer : _layers) {
layer->update();
}
_window->update();
}
}
When the client's Layer subclass is updated, it asks its RenderAPI to set the clear color and clear the back buffer. Those requests call this code:
void DX11RenderAPI::set_clear_color(const float color[4]) {
_clear_color[0] = color[0];
_clear_color[1] = color[1];
_clear_color[2] = color[2];
_clear_color[3] = color[3];
}
void DX11RenderAPI::clear() {
_dc = _context->device_context();
_rtv = _context->render_target_view();
_dsv = _context->depth_stencil_view();
_dc->ClearRenderTargetView(_rtv.Get(), _clear_color);
_dc->ClearDepthStencilView(_dsv.Get(),
D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL,
1.0f, 0);
_dc->OMSetRenderTargets(1, _rtv.GetAddressOf(), _dsv.Get());
_dc->RSSetViewports(1, &_context->viewport());
}
Then the main loop updates its associated Window, which results in the following:
void DX11Window::update() {
MSG msg{ };
while(::PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
_context->swap_buffers();
}
And finally, Window's RenderingContext swaps the buffers:
void DX11Context::swap_buffers() {
_swapchain->Present(1, 0);
_device_context->DiscardView(_render_target_view.Get());
if(_depth_stencil_view) {
_device_context->DiscardView(_depth_stencil_view.Get());
}
}
For completeness, here's the Win32 setup code:
void DX11Window::init() {
ENGINE_INFO("Creating Win32 Window {}: {}x{}", _data._title,
_data._width,
_data._height);
if(!DirectX::XMVerifyCPUSupport()) {
ENGINE_CRITICAL("CPU does not support DirectXMath.");
return;
}
HRESULT hr = ::CoInitializeEx(nullptr, COINITBASE_MULTITHREADED);
if (FAILED(hr)) {
ENGINE_CRITICAL("CoInitializeEx() failed.");
return;
}
WNDCLASSEX wcex{ };
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = window_proc;
wcex.hIcon = ::LoadIcon(0, "IDI_ICON");
wcex.hCursor = ::LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);
wcex.lpszClassName = _data._title.c_str();
wcex.hIconSm = ::LoadIcon(wcex.hInstance, "IDI_ICON");
wcex.cbWndExtra = sizeof(void *);
if(!::RegisterClassEx(&wcex)) {
ENGINE_CRITICAL("Failed to register window class.");
return;
}
RECT rc = {
0, 0,
static_cast<LONG>(_data._width),
static_cast<LONG>(_data._height)
};
::AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);
_window = ::CreateWindowEx(
0,
_data._title.c_str(),
_data._title.c_str(),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
rc.right - rc.left,
rc.bottom - rc.top,
nullptr, nullptr, 0, this);
if(_window == nullptr) {
ENGINE_ERROR("CreateWindowExW failed: ",
get_last_error_as_string());
::UnregisterClass(wcex.lpszClassName, 0);
return;
}
::SetWindowLongPtr(_window, 0, reinterpret_cast<LONG_PTR>(this));
_context = new DX11Context(_window);
_context->init();
set_vsync(_data._vsync);
::ShowWindow(_window, SW_SHOWNORMAL);
}
And the D3D setup code. It's pulled nearly whole cloth from the Microsoft tutorials, but I've already caught a couple errors in my transcription:
void DX11Context::init() {
create_device_resources();
create_window_dependent_resources();
}
void DX11Context::create_device_resources() {
HRESULT hr = E_FAIL;
create_factory();
Microsoft::WRL::ComPtr<IDXGIAdapter1> adapter;
get_hardware_adapter(adapter.GetAddressOf());
// Create the Direct3D 11 API device object and a corresponding context.
Microsoft::WRL::ComPtr<ID3D11Device> device;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> context;
UINT device_flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#ifdef DEBUG
device_flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
if(adapter) {
hr = D3D11CreateDevice(
adapter.Get(),
D3D_DRIVER_TYPE_UNKNOWN,
nullptr,
device_flags,
feature_level_list,
1,
D3D11_SDK_VERSION,
device.GetAddressOf(), // Returns the Direct3D device created.
&_feature_level, // Returns feature level of device created.
context.GetAddressOf() // Returns the device immediate context.
);
}
#ifndef NDEBUG
Microsoft::WRL::ComPtr<ID3D11Debug> d3d_debug;
if(SUCCEEDED(device.As(&d3d_debug))) {
Microsoft::WRL::ComPtr<ID3D11InfoQueue> d3d_info_queue;
if (SUCCEEDED(d3d_debug.As(&d3d_info_queue)))
{
#ifdef DEBUG
d3d_info_queue->SetBreakOnSeverity(D3D11_MESSAGE_SEVERITY_CORRUPTION, true);
d3d_info_queue->SetBreakOnSeverity(D3D11_MESSAGE_SEVERITY_ERROR, true);
#endif
D3D11_MESSAGE_ID hide [] =
{
D3D11_MESSAGE_ID_SETPRIVATEDATA_CHANGINGPARAMS,
};
D3D11_INFO_QUEUE_FILTER filter = {};
filter.DenyList.NumIDs = static_cast<UINT>(std::size(hide));
filter.DenyList.pIDList = hide;
d3d_info_queue->AddStorageFilterEntries(&filter);
}
}
#endif
hr = device.As(&_device);
if(FAILED(hr)) {
ENGINE_CRITICAL("Failed to create DX11 device: ({}) {}",
hr, DX11Window::get_last_error_as_string());
return;
}
hr = context.As(&_device_context);
if(FAILED(hr)) {
ENGINE_CRITICAL("Failed to create DX11 device context: ({}) {}",
hr, DX11Window::get_last_error_as_string());
return;
}
ENGINE_INFO("Initialized DirectX 11.4 device and device context");
hr = context.As(&_annotation);
if(FAILED(hr)) {
ENGINE_CRITICAL("Failed to create DX11 annotation: ({}) {}",
hr, DX11Window::get_last_error_as_string());
return;
}
}
void DX11Context::create_factory() {
HRESULT hr = E_FAIL;
#if defined(_DEBUG) && (_WIN32_WINNT >= 0x0603 /*_WIN32_WINNT_WINBLUE*/)
bool debugDXGI = false;
{
Microsoft::WRL::ComPtr<IDXGIInfoQueue> info_queue;
if(SUCCEEDED(::DXGIGetDebugInterface1(0,
IID_PPV_ARGS(info_queue.GetAddressOf())))) {
debugDXGI = true;
hr = ::CreateDXGIFactory2(
DXGI_CREATE_FACTORY_DEBUG,
IID_PPV_ARGS(_factory.ReleaseAndGetAddressOf())
);
if(FAILED(hr)) {
ENGINE_CRITICAL("CreateDXGIFactory2() failed: ({}) {}",
hr, DX11Window::get_last_error_as_string());
return;
}
info_queue->
SetBreakOnSeverity(DXGI_DEBUG_ALL,
DXGI_INFO_QUEUE_MESSAGE_SEVERITY_ERROR,
true);
info_queue->
SetBreakOnSeverity(DXGI_DEBUG_ALL,
DXGI_INFO_QUEUE_MESSAGE_SEVERITY_CORRUPTION,
true);
DXGI_INFO_QUEUE_MESSAGE_ID hide[] =
{
80 /* IDXGISwapChain::GetContainingOutput:
The swapchain's adapter does not control the output on
which the swapchain's window resides. */,
};
DXGI_INFO_QUEUE_FILTER filter{ };
filter.DenyList.NumIDs = static_cast<UINT>(std::size(hide));
filter.DenyList.pIDList = hide;
info_queue->AddStorageFilterEntries(DXGI_DEBUG_DXGI, &filter);
}
}
if (!debugDXGI)
#endif
{
hr = ::CreateDXGIFactory1(
IID_PPV_ARGS(_factory.ReleaseAndGetAddressOf())
);
if(FAILED(hr)) {
ENGINE_CRITICAL("CreateDXGIFactory1() failed: ({}) {}",
hr, DX11Window::get_last_error_as_string());
return;
}
}
}
void DX11Context::create_window_dependent_resources() {
HRESULT hr;
// Clear the previous window size specific context.
ID3D11RenderTargetView* null_views[] = {nullptr};
_device_context->OMSetRenderTargets(static_cast<UINT>(std::size(null_views)),
null_views, nullptr);
_render_target_view.Reset();
_depth_stencil_view.Reset();
_render_target.Reset();
_depth_stencil.Reset();
_device_context->Flush();
// Determine the render target size in pixels.
const UINT back_buffer_width =
std::max<UINT>(
static_cast<UINT>(_output_size.right - _output_size.left),
1u
);
const UINT back_buffer_height =
std::max<UINT>(
static_cast<UINT>(_output_size.bottom - _output_size.top),
1u
);
if (_swapchain) {
// If the swap chain already exists, resize it.
hr = _swapchain->ResizeBuffers(
_back_buffer_count,
back_buffer_width,
back_buffer_height,
_back_buffer_format,
0u
);
if(FAILED(hr)) {
ENGINE_CRITICAL("Could not resize swapchain: ({}) {}",
hr, DX11Window::get_last_error_as_string());
return;
}
}
else
{
// Create a descriptor for the swap chain.
DXGI_SWAP_CHAIN_DESC1 swapchain_desc{ };
swapchain_desc.Width = back_buffer_width;
swapchain_desc.Height = back_buffer_height;
swapchain_desc.Format = _back_buffer_format;
swapchain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapchain_desc.BufferCount = _back_buffer_count;
swapchain_desc.SampleDesc.Count = 1;
swapchain_desc.SampleDesc.Quality = 0;
swapchain_desc.Scaling = DXGI_SCALING_STRETCH;
swapchain_desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
swapchain_desc.AlphaMode = DXGI_ALPHA_MODE_IGNORE;
swapchain_desc.Flags = 0;
DXGI_SWAP_CHAIN_FULLSCREEN_DESC fs_swapchain_desc{ };
fs_swapchain_desc.Windowed = TRUE;
// Create a SwapChain from a Win32 window.
hr = _factory->CreateSwapChainForHwnd(
_device.Get(),
_window,
&swapchain_desc,
&fs_swapchain_desc,
nullptr, _swapchain.ReleaseAndGetAddressOf()
);
if(FAILED(hr)) {
ENGINE_CRITICAL("Could not create swapchain for HWND: ({}) {}",
hr, DX11Window::get_last_error_as_string());
}
// This class does not support exclusive full-screen mode and prevents
// DXGI from responding to the ALT+ENTER shortcut
hr = _factory->MakeWindowAssociation(_window, DXGI_MWA_NO_ALT_ENTER);
if(FAILED(hr)) {
ENGINE_CRITICAL("Unable to make window association: ({}) {}",
hr, DX11Window::get_last_error_as_string());
return;
}
}
// Create a render target view of the swap chain back buffer.
hr = _swapchain->
GetBuffer(0, IID_PPV_ARGS(_render_target.ReleaseAndGetAddressOf()));
if(FAILED(hr)) {
ENGINE_CRITICAL("Could not get back buffer from swapchain: ({}) {}",
hr, DX11Window::get_last_error_as_string());
return;
}
CD3D11_RENDER_TARGET_VIEW_DESC
rt_view_desc(D3D11_RTV_DIMENSION_TEXTURE2D, _back_buffer_format);
hr = _device->CreateRenderTargetView(
_render_target.Get(),
&rt_view_desc,
_render_target_view.ReleaseAndGetAddressOf()
);
if(FAILED(hr)) {
ENGINE_CRITICAL("Unable to create render target view: ({}) {}",
hr, DX11Window::get_last_error_as_string());
return;
}
if (_depth_buffer_format != DXGI_FORMAT_UNKNOWN) {
// Create a depth stencil view for use with 3D rendering if needed.
CD3D11_TEXTURE2D_DESC depth_stencil_desc(
_depth_buffer_format,
back_buffer_width,
back_buffer_height,
1, // This depth stencil view has only one texture.
1, // Use a single mipmap level.
D3D11_BIND_DEPTH_STENCIL
);
hr = _device->CreateTexture2D(
&depth_stencil_desc,
nullptr,
_depth_stencil.ReleaseAndGetAddressOf()
);
if(FAILED(hr)) {
ENGINE_CRITICAL("Unable to create depth stencil texture: ({}) {}",
hr, DX11Window::get_last_error_as_string());
return;
}
CD3D11_DEPTH_STENCIL_VIEW_DESC dsv_desc(D3D11_DSV_DIMENSION_TEXTURE2D);
hr = _device->CreateDepthStencilView(
_depth_stencil.Get(),
&dsv_desc,
_depth_stencil_view.ReleaseAndGetAddressOf()
);
if(FAILED(hr)) {
ENGINE_CRITICAL("Failed to create depth stencil view: ({}) {}",
hr, DX11Window::get_last_error_as_string());
return;
}
}
// Set the 3D rendering viewport to target the entire window.
_viewport = CD3D11_VIEWPORT(
0.0f,
0.0f,
static_cast<float>(back_buffer_width),
static_cast<float>(back_buffer_height)
);
}
void DX11Context::get_hardware_adapter(IDXGIAdapter1 **adapter) {
HRESULT hr;
*adapter = nullptr;
Microsoft::WRL::ComPtr<IDXGIAdapter4> possible_adapter;
Microsoft::WRL::ComPtr<IDXGIFactory6> factory6;
hr = _factory.As(&factory6);
if(SUCCEEDED(hr)) {
for(UINT adapter_index = 0;
SUCCEEDED(factory6->EnumAdapterByGpuPreference(
adapter_index,
DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE,
IID_PPV_ARGS(possible_adapter.ReleaseAndGetAddressOf())));
adapter_index++)
{
DXGI_ADAPTER_DESC1 desc;
possible_adapter->GetDesc1(&desc);
if(desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) {
// Don't select the Basic Render Driver adapter.
continue;
}
break;
}
}
if(!possible_adapter) {
ENGINE_CRITICAL("Could not locate IDXGIAdapter4");
return;
}
*adapter = possible_adapter.Detach();
}
And finally, the client's Layer subclass:
class ExampleLayer : public Layer {
public:
void on_event(Event &event) override {
if(event._type == EventType::KeyPressed) {
if(event.key() == KEY_ESCAPE) {
CLIENT_INFO("Closing the window");
_app->window()->trigger_close_event();
}
}
}
void update() {
_render_api->set_clear_color(clear_color);
_render_api->clear();
}
ExampleLayer(Application * app) :
Layer("Gameplay"),
_app{ app }
{
_render_api = RenderAPI::create();
_render_api->set_context(_app->window()->context());
}
~ExampleLayer() {
delete _render_api;
}
private:
Application *_app;
RenderAPI *_render_api;
float clear_color[4]{ 0.1f, 0.15f, 0.1f, 1.0f };
};
I am sorry to not have more to go on. The debug output from Visual Studio is silent on matters relating to DirectX (though it wasn't always - it helped me catch an error yesterday, so at least I know it works). I'm not sure what else I can do to gather evidence or hunt for clues, so here I am.
Thank you in advance to anyone who bothers to read this, let alone those noble souls who offer help. =)