Jump to content

  • Log In with Google      Sign In   
  • Create Account

We need your feedback on a survey! Each completed response supports our community and gives you a chance to win a $25 Amazon gift card!






Entry 1: Direct3D Initialization, Multithreading, Full Screen Support

Posted by mi-imi, 08 October 2012 · 905 views

Here I will chronicle the development of a simple multithreaded Direct3D 11 engine in C++. Note that this is just something I am doing for fun, and is based on the results of a lot of experimentation. It is not the "official" way to use Direct3D 11. However it does show how to use the API in a practical setting.

If anyone wants the source code please message me.

Legal:
imiEngine - A 3D Video Game Engine
Copyright © 2012 William Brosi
This document is part of imiEngine.

imiEngine is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
imiEngine is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
A copy of the GNU General Public License may be found at
<http://www.gnu.org/licenses/>.



Basic Application Setup

First, I wrote code to register a window class:

WNDCLASSEX wcex;
ZeroMemory( &wcex, sizeof( wcex ) );
wcex.cbSize = sizeof( wcex );
wcex.lpfnWndProc = WinProc;
wcex.hInstance = hInstance;
wcex.hCursor = LoadCursor( NULL, IDC_ARROW );
wcex.hbrBackground = ( HBRUSH )GetStockObject( BLACK_BRUSH );
wcex.lpszClassName = L"imie";
if( !RegisterClassEx( &wcex ) )
{
//an error occured
MessageBox( GetDesktopWindow( ), L"RegisterClassEx() failed.",
L"Game Application", MB_OK );
return 0;
}


and create a window:

if( !( g_hWnd = CreateWindowEx( 0, L"imie", L"imie", WS_POPUP, 0, 0,
IMIEMAIN_SETTINGS_OUTPUTWIDTH, IMIEMAIN_SETTINGS_OUTPUTHEIGHT,
GetDesktopWindow( ), NULL, hInstance, NULL ) ) )
{
//error
MessageBox( GetDesktopWindow( ), L"CreateWindowEx() failed.",
L"Game Application", MB_OK );
return 0;
}
ShowWindow( g_hWnd, SW_SHOWNORMAL );


To enable the app to exit, I then added the following to the window procedure:

if( uMsg == WM_DESTROY )
{
//shut down
PostQuitMessage( 0 );
return 0;
}
return DefWindowProc( hWnd, uMsg, wParam, lParam );


Next, a basic message loop:

while( b = GetMessage( &msg, NULL, 0, 0 ) )
{
if( b == -1 )
{
//an error occured
//set an error code and force the app to terminate
g_MainError = IMIEMAIN_ERROR_GETMESSAGE_FAILED;
DestroyWindow( g_hWnd );
}
else DispatchMessage( &msg );
}



Direct3D Initialization

The following, in WinMain(), calls a routine to initialize Direct3D. It also displays an error message and exits
if necessary:

IMIEGRAPHICS_ERROR graphicsError;
if( graphicsError = GraphicsInitialize( ) )
{
//an error occured
GraphicsUninitialize( );
MessageBox( GetDesktopWindow( ), g_wcGraphicsErrorMessages[graphicsError],
L"Game Application", MB_OK );
return 0;
}


I also added a GraphicsUninitialize( ) in the WM_DESTROY part of the window procedure to be called when the app
shuts down.

Now, to release interfaces when the app exits:

if( g_pd3dRenderTargetView ) g_pd3dRenderTargetView->Release( );
g_pd3dRenderTargetView = NULL;
if( g_pd3dDeviceContext ) g_pd3dDeviceContext->Release( );
g_pd3dDeviceContext = NULL;
if( g_pd3dDevice ) g_pd3dDevice->Release( );
g_pd3dDevice = NULL;
if( g_pdxgiSwapChain ) g_pdxgiSwapChain->Release( );
g_pdxgiSwapChain = NULL;


To create a Direct3D device and swap chain during initialization:

DXGI_SWAP_CHAIN_DESC dxgiSCD;
ZeroMemory( &dxgiSCD, sizeof( dxgiSCD ) );
dxgiSCD.BufferDesc.Width = IMIEMAIN_SETTINGS_OUTPUTWIDTH;
dxgiSCD.BufferDesc.Height = IMIEMAIN_SETTINGS_OUTPUTHEIGHT;
dxgiSCD.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
dxgiSCD.SampleDesc.Count = 1;
dxgiSCD.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
dxgiSCD.BufferCount = 2;
dxgiSCD.OutputWindow = g_hWnd;
dxgiSCD.Windowed = TRUE;
if( FAILED( D3D11CreateDeviceAndSwapChain( NULL, D3D_DRIVER_TYPE_HARDWARE,
NULL, 0, NULL, 0, D3D11_SDK_VERSION, &dxgiSCD, &g_pdxgiSwapChain,
&g_pd3dDevice, NULL, &g_pd3dDeviceContext ) ) )
{
return IMIEGRAPHICS_ERROR_D3D11CREATEDEVICEANDSWAPCHAIN_FAILED;
}


We also need to create a render target view to produce a visible result:

ID3D11Texture2D *pd3dTexture;
if( FAILED( g_pdxgiSwapChain->GetBuffer( 0, __uuidof( ID3D11Texture2D ),
( LPVOID* )&pd3dTexture ) ) )
{
return IMIEGRAPHICS_ERROR_IDXGISWAPCHAIN_GETBUFFER_FAILED;
}
if( FAILED( g_pd3dDevice->CreateRenderTargetView( pd3dTexture, NULL,
&g_pd3dRenderTargetView ) ) )
{
pd3dTexture->Release( );
return IMIEGRAPHICS_ERROR_D3D11DEVICE_CREATERENDERTARGETVIEW_FAILED;
}
pd3dTexture->Release( );



Multithreading

The following code create a worker thread to handle the rendering:

if( !( g_hGraphicsThread = CreateThread( NULL, 0, GraphicsThread, NULL, 0,
NULL ) ) )
{
return IMIEGRAPHICS_ERROR_CREATETHREAD_FAILED;
}


The new thread starts by calling this function:

DWORD WINAPI GraphicsThread( LPVOID )
{
//sleep until there is something to do
while( g_pdxgiSwapChain ) SleepEx( INFINITE, TRUE );
//if we are here the app is shutting down so terminate
return 0;
}


The main thread calls Windows' QueueUserAPC() function once each frame. This causes the thread to "wake up,"
process the frame, and go back to sleep. To shut down this thread, the following code can be found in
GraphicsUninitialize(), after g_pdxgiSwapChain is released:

if( g_hGraphicsThread )
{
//wake up the thread so it terminates
QueueUserAPC( GraphicsThreadCloser, g_hGraphicsThread, NULL );
CloseHandle( g_hGraphicsThread );
}
g_hGraphicsThread = NULL;


This breaks the while loop and terminates the thread.


Render Loop

After Direct3D is initialized, the code

SetTimer( g_hWnd, 1, 10, NULL );

starts the rendering loop. Each WM_TIMER event in the window procedure begins a new frame:

if( !g_bGraphicsBusy && g_bActive )
{
//put graphics thread in busy state
g_bGraphicsBusy = TRUE;
//wake up the graphics thread and start rendering
QueueUserAPC( GraphicsRender, g_hGraphicsThread, NULL );
}


After ensuring the rendering thread is done with the previous frame, the rendering thread is directed to call
GraphicsRender(), which contains this code:

g_pdxgiSwapChain->Present( 0, 0 );
//indicate we're done
g_bGraphicsBusy = FALSE;


This displays the new frame on the monitor, and the rendering thread is now ready for the next frame.

I now implemented some run-time rendering error checking. I replaced the above with:

if( FAILED( g_pdxgiSwapChain->Present( 0, 0 ) ) )
{
//if we're here, there a problem; set the flag
//and the next timer event will try to recover
g_MainError = IMIEMAIN_ERROR_GRAPHICSADAPTERPROBLEM;
}
//indicate we're done
g_bGraphicsBusy = FALSE;


and, back in the WM_TIMER section of the window procedure:

if( g_MainError == IMIEMAIN_ERROR_GRAPHICSADAPTERPROBLEM )
{
//if we're here something went wrong on the last frame
//attempt to recover by releasing and reinitializing
//the graphics hardware
GraphicsUninitialize( );
if( GraphicsInitialize( ) )
{
//if we're here, there is a serious problem
//so force an exit
DestroyWindow( g_hWnd );
return 0;
}
//successful recovery so clear the error code
g_MainError = IMIEMAIN_ERROR_SUCCESS;
}


If there's a problem, the flag set in GraphicsRender() causes the WM_TIMER code to release and reinitialize the
graphics hardware on the next frame. If successful, the g_MainError code is cleared and the app continues
normally. If not, the app is forced to terminate.

The following code in GraphicsRender() now creates a glowing effect in the application window to show everything
is working correctly:

static FLOAT color[4] = { 0, 0, 0, 1 };
color[0] += 0.01f;
if( color[0] > 1 ) color[0] = 0;
g_pd3dDeviceContext->ClearRenderTargetView( g_pd3dRenderTargetView, color );



Full Screen Support

The following changes to GraphicsInitialize() allow full screen mode to be used:

dxgiSCD.Windowed = !IMIEMAIN_SETTINGS_FULLSCREEN;
if( IMIEMAIN_SETTINGS_FULLSCREEN )
dxgiSCD.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;


Note that this requires a change to GraphicsUninitialize():

if( g_pdxgiSwapChain )
{
//in case we're in full screen we need to return to windowed
g_pdxgiSwapChain->SetFullscreenState( FALSE, NULL );
g_pdxgiSwapChain->Release( );
}
g_pdxgiSwapChain = NULL;


When the user ALT+TABs out of our app, the following code, in the WM_ACTIVATEAPP section of the window
procedure, switches back to windowed mode and hides our window:

if( IMIEMAIN_SETTINGS_FULLSCREEN && !wParam )
{
//we're comming out of full screen and losing focus
g_pdxgiSwapChain->SetFullscreenState( FALSE, NULL );
//get rid of our window
ShowWindow( hWnd, SW_MINIMIZE );
}


Now, to handle switching back to our app, the following code, also in the WM_ACTIVATEAPP section, switches back
to full screen and releases the render target view:

if( FAILED( g_pdxgiSwapChain->SetFullscreenState( TRUE, NULL ) ) )
{
g_MainError = IMIEMAIN_ERROR_IDXGISWAPCHAIN_SETFULLSCREENSTATE_FAILED;
DestroyWindow( hWnd );
return 0;
}
//need to release the render target view before
//resetting the swap chain
g_pd3dRenderTargetView->Release( );
g_pd3dRenderTargetView = NULL;


Finally, the transition to full screen is completed by creating a new swap chain. A new render target view is
also created:

if( FAILED( g_pdxgiSwapChain->ResizeBuffers( 2,
IMIEMAIN_SETTINGS_OUTPUTWIDTH, IMIEMAIN_SETTINGS_OUTPUTHEIGHT,
DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH ) ) )
{
g_MainError = IMIEMAIN_ERROR_IDXGISWAPCHAIN_RESIZEBUFFERS_FAILED;
DestroyWindow( hWnd );
return 0;
}
//obtain a new render target view from the new swap chain
if( GraphicsGetRenderTargetView( ) )
{
g_MainError = IMIEMAIN_ERROR_GRAPHICSGETRENDERTARGETVIEW_FAILED;
DestroyWindow( hWnd );
return 0;
}


and the app can now continue normally.

I'll try to have more next Monday (October 15).




December 2014 »

S M T W T F S
 123456
78910111213
14151617181920
21222324252627
28 29 3031   
PARTNERS