-
Notifications
You must be signed in to change notification settings - Fork 81
01 ClearScreen
The simplest sample is the Clear Screen sample.
This example consists of a single main.cpp file. This sample uses the GameFramework to create a window and receives window messages through signals.
The sample also uses the Device
and the SwapChain
classes from the DX12Lib library to display a clear screen.
First, we'll add a few header files from the GameFramework library.
#include <GameFramework/GameFramework.h>
#include <GameFramework/Window.h>
The GameFramework.h header files gives us access to the GameFramework
singleton class that is used to create the window and to manage the window's message pump.
The [Window.h] header file provides the declarations for the Window
class that is used to connect callback functions to the main application and for switching to fullscreen mode.
We also need access to a few classes from the DX12Lib.
#include <dx12lib/CommandList.h>
#include <dx12lib/CommandQueue.h>
#include <dx12lib/Device.h>
#include <dx12lib/SwapChain.h>
The CommandList
class is used to issue a ClearTexture
on the swapchain's backbuffer. The CommandQueue
class is used to get a new CommandList
that can be used to record new commands and the CommandQueue
is also required to execute those commands on the GPU. These classes are wrappers for the ID3D12GraphicsCommandList and ID3D12CommandQueue interfaces respectively.
The Device
class is a wrapper for a ID3D12Device interface and the SwapChain
class is a wrapper for the IDXGISwapChain interface.
All of the classes of the DX12Lib are in the dx12lib
namesapace. To reduce typing, let's include that namespace.
using namespace dx12lib;
We'll declare a few callback functions that the Window
will invoke when the corresponding window message is sent to the window.
void OnUpdate( UpdateEventArgs& e );
void OnKeyPressed( KeyEventArgs& e );
void OnWindowResized( ResizeEventArgs& e );
void OnWindowClose( WindowCloseEventArgs& e );
We also need a few global objects.
std::shared_ptr<Window> pGameWindow = nullptr;
std::shared_ptr<Device> pDevice = nullptr;
std::shared_ptr<SwapChain> pSwapChain = nullptr;
Logger logger;
I already mentioned the Window
, Device
, and SwapChain
classes. The Logger
class is actually an alias of std::shared_ptr<spdlog::logger>
(defined in GameFramework.h). We'll use the GameFramework
class to get a named logger that can be used to log messages for the sample.
The wWinMain
function is the main entry-point for the Win32 application. The w
prefix indicates that we want to recevie the command-line arguments as wide-character strings. This is needed to parse wide-character file paths on the command-line.
int WINAPI wWinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR lpCmdLine, int nCmdShow )
{
#if defined( _DEBUG )
// Always enable the Debug layer before doing anything with DX12.
Device::EnableDebugLayer();
#endif
The first thing we need to do (in Debug mode) is to enable the debug layer. This will allow our application to receive error messages from the DirectX 12 runtime if we make a mistake in the application.
Note: Always enable the debug layer before you create the device. Enabling the debug layer after you create the device will invalidate the device.
int retCode = 0;
auto& gf = GameFramework::Create( hInstance );
{
The GameFramework
singleton is created using the GameFramwork::Create
method. It takes a handle to the instance of the application as its only argument. It uses the instance handle to register the window class.
Note: Using the
GameFramework
library is completely optional, but makes the sample much easier to create since it removes a lot of boiler-plate code and provides a few extra features (like logging and keyboard, mouse, and joystick handling).
// Create a logger for logging messages.
logger = gf.CreateLogger( "ClearScreen" );
We can use the GameFramework
instance to create a new logger. The logger provided by the GameFramwork
provides debug logging to a file, console, and the debug output in Visual Studio by default.
Note: The
GameFramework
library includes the spdlog header files. Feel free to create your own loggers or just use thespdlog::(info|warn|error|critical)
log variants to use the default logger.
Creating the ID3D12Device object requires only a single line of code:
// Create a GPU device using the default adapter selection.
pDevice = Device::Create();
The Device::Create
method will create a Device
object using the first adapter returned from IDXGIFactory6::EnumAdapterByGpuPreference using DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE as the GPU preference.
Note: You can also use the
Adapter::GetAdapters
method to get a list of all DirectX 12 capable display adapters available on the system.
auto description = pDevice->GetDescription();
logger->info( L"Device Created: {}", description );
The description of the returned Device
is logged. This will display the name of the GPU that was used to create the device.
// Create a window:
pGameWindow = gf.CreateWindow( L"Clear Screen", 1920, 1080 );
The GameFramwork
instance is used to create a render window. The name of the window can be used to get a pointer to the window elsewhere in the program and it is also used for the text in the title of the window.
The initial size of the window is 1080p.
Note: The window will not be automatically shown. The window can be shown using the
Window::Show
method, but this will be done later to ensure that the first resize event is triggered after theSwapChain
is created.
After creating the window, we can create the swap chain.
// Create a swap chain for the window
pSwapChain = pDevice->CreateSwapChain( pGameWindow->GetWindowHandle() );
pSwapChain->SetVSync( false );
The swap chain only needs the OS window handle for the window which can be retrieved from the window using the Window::GetWindowHandle
method.
Setting the v-sync property of the swap chain to false
will ensure that the swap chain will present as fast as possible, but you should be aware that disabling v-sync will often cause screen tearing.
The window defines several events that you can register a callback function for.
// Register events.
pGameWindow->KeyPressed += &OnKeyPressed;
pGameWindow->Resize += &OnWindowResized;
pGameWindow->Update += &OnUpdate;
pGameWindow->Close += &OnWindowClose;
- The
KeyPressed
event is invoked whenever a key is pressed on the keyboard while the window has focus. - The
Resize
event is invoked whenever the size of the window changes. - The
Update
event is invoked whenever the game logic should be updated and the window screen should be redrawn (rendered). - The
Close
event is called when the window is closed.
The Window
class defines more events that you can register callbacks for but this is suffient for this simple application.
Now that the SwapChain
has been created and the callback functions have been registered with the window events, the window can be shown and the handling of the window messages can be started:
pGameWindow->Show();
retCode = GameFramework::Get().Run();
The window's message pump is started using the GameFramework::Run
method. To stop the message pump and exit the Run
method, use the GameFramework::Stop
method. This will be shown later.
When the application stops, the Run
method will end and we need to clean up any allocated resources.
// Release globals.
pSwapChain.reset();
pGameWindow.reset();
pDevice.reset();
}
// Destroy game framework resource.
GameFramework::Destroy();
Since we are using std::shared_ptr
for the resources, we can release their resouces by using the reset
method on the shared pointer.
The GameFramework::Destroy
method is used to destroy the GameFramework
singleton instance.
To ensure there are no leaked resources, we can use the Device::ReportLiveObjects
method. We can register this function to be invoked just before the process is terminiated by use the atexit
function.
atexit( &Device::ReportLiveObjects );
return retCode;
}
So that's the main entry point function. Next, we'll define the callback functions for the application.
The OnUpdate
callback function takes an UpdateEventArgs
class as its only argument. All callback functions have some kind of EventArgs
associated with them. All of the arguments that are needed by the callback function can be found in the EventArgs
. For example, the UpdateEventArgs
contains the change in time (in seconds) since the last time the OnUpdate
method was invoked. It also contains the total running time since the window was created.
You may have noticed that the window does not define a Render
event. The Update
event is invoked whenever the window should be redrawn, but usually this is combined with updating the game logic so I decided not to seperate the two actions. It's up to you if you want to create a seperate OnRender
method, but you must call this method explicitly.
void OnUpdate( UpdateEventArgs& e )
{
static uint64_t frameCount = 0;
static double totalTime = 0.0;
totalTime += e.DeltaTime;
++frameCount;
if ( totalTime > 1.0 )
{
auto fps = frameCount / totalTime;
frameCount = 0;
totalTime = 0.0;
logger->info( "FPS: {:.7}", fps );
wchar_t buffer[256];
::swprintf_s( buffer, L"Clear Screen [FPS: %f]", fps );
pGameWindow->SetWindowTitle( buffer );
}
The first part of this function just computes the frames per second (FPS) and outputs the FPS counter to the logger as well as update the window title with the FPS counter.
auto& commandQueue = pDevice->GetCommandQueue( D3D12_COMMAND_LIST_TYPE_DIRECT );
auto commandList = commandQueue.GetCommandList();
auto& renderTarget = pSwapChain->GetRenderTarget();
const FLOAT clearColor[] = { 0.4f, 0.6f, 0.9f, 1.0f };
commandList->ClearTexture( renderTarget.GetTexture( AttachmentPoint::Color0 ), clearColor );
commandQueue.ExecuteCommandList( commandList );
pSwapChain->Present();
}
The second half of the OnUpdate
method gets a direct (D3D12_COMMAND_LIST_TYPE_DIRECT) CommandQueue
from the Device
class which is used to get a CommandList
which is used to perform commands on the GPU.
The current back buffer of the SwapChain
can be retrieved using the SwapChain::GetRenderTarget
method. This method returns a RenderTarget
class by const references. This class can be safely copied and mainipulated (for example, if you need to add a depth buffer - see the next example). It is important to note that since the swap chain's back buffer may be different each frame, you must request the swap chain's render target each frame. Do not store a pointer to the swap chain's back buffer for longer than the current frame.
The CommandList::ClearTexture
method is used to clear a texture. This method can be used to clear any texture that is created with the D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET resource flag.
There is no need to manually transition the back buffer's texture to D3D12_RESOURCE_STATE_RENDER_TARGET as this will be done automatically for you.
In DirectX 12, all GPU commands are deferred. That is, they are not executed immediatly (like OpenGL or DirectX 11 using an immediate context). The CommandList
must be executed on a CommandQueue
before you will see anything happening on the screen.
After executing the CommandList
on the CommandQueue
, the swap chain must be presented using the SwapChain::Present
method for the rendered image to be presented on the window.
That's all we need to do to get a clear screen working. In the next section, we'll handle KeyPressed
events.
Whenever a key is pressed on the keyboard while the window has the keyboard focus, the KeyPressed
event is invoked. The KeyEventArgs
argument is used to describe the event and provides information like which KeyCode
was pressed, and whether the Shift
, Control
or Alt
keys were pressed.
For this application, we'll handle the following keys:
Key | Action |
---|---|
Esc |
Quit the application. |
V |
Toggle swap chain v-sync. |
Alt +Enter
|
Toggle fullscreen |
F11 |
Toggl fullscreen |
void OnKeyPressed( KeyEventArgs& e )
{
logger->info( L"KeyPressed: {}", (wchar_t)e.Char );
switch ( e.Key )
{
case KeyCode::V:
pSwapChain->ToggleVSync();
break;
case KeyCode::Escape:
// Stop the application if the Escape key is pressed.
GameFramework::Get().Stop();
break;
case KeyCode::Enter:
if ( e.Alt )
{
[[fallthrough]];
case KeyCode::F11:
pGameWindow->ToggleFullscreen();
break;
}
}
}
What is interesting to note about this function is that either F11
or Alt
+Enter
can be used to toggle the fullscreen state of the window. To handle both key combinations to do to the same thing, we can use a "fallthrough" case (using the fallthrough C++ attribute is optional but can be used to indicate an intentional fallthrough an silence annoying compiler warnings.)
Handling window resize events is equally easy to implement.
The Window's Resize
event is invoked whenver the window is resized (by dragging the corners of the window or toggling window fullscreen mode). The ResizeEventArgs
contains the new width and height of the window.
void OnWindowResized( ResizeEventArgs& e )
{
logger->info( "Window Resize: {}, {}", e.Width, e.Height );
pSwapChain->Resize( e.Width, e.Height );
}
The SwapChain::Resize
method will cause the swap chain's back buffers to be resized. It is important to note that there may not be any references to the SwapChain's backbuffer textures when this method is called. For this reason, you should not store a reference to the render target textures that you get using the SwapChain::GetRenderTarget
method, but instead just query this method each frame and store the result in a temporary variable that does not exist longer than the update/render method.
The final event that we need to handle is the window's Close
event. The Close
event sends a WindowCloseEventArgs
structure that contains a single member (boolean) variable ConfirmClose
that you can set to false
if you want to keep the window open (for example, there are unsaved file changes that would be lost if the window is closed).
In this case, we just want to quite the application if the window is closed.
void OnWindowClose( WindowCloseEventArgs& e )
{
// Stop the application if the window is closed.
GameFramework::Get().Stop();
}
That's the "Hello World" of the DX12 application framework and DX12Lib library. But of course there is much more to explore. In the next sample, we'll create a vertex and index buffer to render a cube. We'll also use a constant buffer to animate the cube and to provide a simple (static) camera that is used to project the cube on the screen.