Skip to content

01 ClearScreen

Jeremiah van Oosten edited this page Oct 27, 2020 · 3 revisions

Clear Screen Sample

The simplest sample is the Clear Screen sample.

Clear Screen.

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.

Preamble

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.

wWinMain

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 the spdlog::(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 the SwapChain 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.

OnUpdate

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.

OnKeyPressed

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.

OnWindowResized

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.

OnWindowClose

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();
}

Conclusion

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.