Deferred Rendering

Describes Deferred Rendering and how to implement it on the Xbox One.

The following sections explain what Deferred Rendering is and how to implement it effectively on the Xbox One dev kit:

This tutorial presents the code required to enable deferred rendering. For a more detailed example, see the RenderTechniques ATG sample.

What is Deferred Rendering?

Deferred Rendering replaces the traditional method of rendering, which is known as ‘Immediate’ or ‘Forward’ Rendering. Deferred Rendering stores graphics commands in a Command Buffer to be played back at a later time. Render stored commands across multiple threads, breaking up complex scenes into multiple concurrent tasks.

Deferred rendering is essentially threaded rendering, with multiple simultaneous graphics threads. Each thread renders a portion of the total scene geometry to a viewport, then the viewports are combined into a single viewport to display onscreen. This is different from Deferred Shading, which renders geometry on a single thread, then uses multiple threads to handle postprocessing effects like lighting and shadows.

Initialization

The setup process for deferred rendering is very similar to a traditional threaded program. You will need one main thread to manage the other sub-threads. Each sub-thread needs to be created and initialized.

Creating a Thread

Each thread needs an ID3D11DeviceContext to record graphics commands. Call ID3D11Device::CreateDeferredContext() to initialize the device context. When calling CreateDeferredContext(), set the contextFlag value to D3D11_CREATE_DEFERRED_CONTEXT_TITLE_MANAGED_COMMAND_LIST_OBJECT_LIFETIMES.

Each thread runs indefinitely in a loop, and you will need to create some HANDLE objects to ensure that the threads remain synchronized. These handles typically take the form of a start event and finish event. The thread waits until the start event is triggered, then runs until it reaches the finish event. At this point the thread stops and waits until the next start event. Call CreateEvent() to initialize the HANDLEs.

Now you are ready to create the thread with a call to CreateThread(). CreateThread() returns a HANDLE object to reference the thread. The main thread does not need to be created, only the worker threads. For the lpParameter, you should pass an object that contains at least two pieces of data: an index number, and a pointer to any data that the thread will need access to.

Sample data type to be passed as lpParameter to the CreateThread() function:

C++

struct ThreadEntryData
{
    class Sample*    m_Sample;
    UINT32            m_ThreadIndex;
};  

Sample for() loop creating multiple threads:

C++

UINT contextFlags = D3D11_CREATE_DEFERRED_CONTEXT_TITLE_MANAGED_COMMAND_LIST_OBJECT_LIFETIMES;
for( UINT32  i = 0; i < s_NumberOfThreads; ++i )
{
    // 0 index is used for main thread, to keep GPU rendering order consistent when deferred contexts are in use
    // The main thread does not get an event or deferred context
    ThreadWorkingData &threadWorkingData = m_ThreadWorkingData[i + 1];
    threadWorkingData.m_ThreadIndex = i + 1;

    // Create an ID3D11DeviceContext object for the current thread
    XSF_ERROR_IF_FAILED( pDev->CreateDeferredContext( contextFlags, reinterpret_cast<ID3D11DeviceContext**>(threadWorkingData.m_spDeferredContext.ReleaseAndGetAddressOf()) ) );

    // Initialize HANDLEs for starting and stopping the thread
    threadWorkingData.m_StartEvent = CreateEvent( nullptr, FALSE, FALSE, nullptr );
    m_ThreadFinishedEvent[i] = CreateEvent( nullptr, FALSE, FALSE, nullptr );

    // Set pointers so we can reference this thread later
    m_ThreadEntryData[i].m_Sample = this;
    m_ThreadEntryData[i].m_ThreadIndex = i + 1;
    
    // Create the thread. StartThread is name of the function that the thread will run
    m_Thread[i] = CreateThread( nullptr, 0, StartThread, &m_ThreadEntryData[i], 0, nullptr );

    // Verify the thread has been created properly
    XSF_ASSERT( m_Thread[i] );
}  

Setting Thread Affinitys

After all your threads have been created and initialized, you will need to specify which CPU cores will run which threads. This is known as setting the thread affinity mask. To set a thread affinity, call SetThreadAffinityMask(), and pass in a pointer to the thread you wish to set and the core you wish to set that thread to. This function must be called for all threads, including the main thread. To get a HANDLE to the main thread, call GetCurrentThread(). SetThreadAffinityMask() will return ERROR_INVALID_PARAMETER if you specify an invalid processor.

C++

void Sample::SetThreadAffinitys( const BOOL fixed )
{
    UINT threadAffinityMask = s_AnyCoreAffinity;
    UINT shift = 0;

    if( fixed )
    {
        threadAffinityMask = 1;
        shift = 1;
    }
    // Set thread affinity for main thread
    if ( !SetThreadAffinityMask( GetCurrentThread(), threadAffinityMask) )
    {
        DebugBreak();
    }
    threadAffinityMask <<= shift;
    // Set thead affinity for secondary threads
    for( UINT32  i = 0; i < s_NumberOfThreads; ++i )
    {
        if ( !SetThreadAffinityMask( m_Thread[i], threadAffinityMask ) )
        {
            DebugBreak();
        }
        threadAffinityMask <<= shift;
    }
}  

Thread Functions

The threads need to run indefinitely, yet still be synchronized and manageable. Use the start event and finish event HANDLEs created earlier to accomplish this. The threads start running as soon as you call CreateThread(), so choose the function you pass to the thread carefully.

Starting a Worker Thread

When you call CreateThread(), you must pass a function for the thread to process. Typically, when a thread is created, it is passed a wrapper function that calls a secondary looping function. The looping function runs indefinitely, and performs whatever task the thread is assigned to do. When the program is closing, the looping function is terminated, and the original wrapper function is able to close.

A sample thread wrapper function:

C++

DWORD __stdcall StartThread( LPVOID param )
{
    Sample::ThreadEntryData *threadEntryData = (Sample::ThreadEntryData *)param;

    // WorkerThreadProcess() is a function that runs in an indefinite loop until the thread is terminated.
    threadEntryData->m_Sample->WorkerThreadProcess( threadEntryData->m_ThreadIndex );

    return 0;
}  

Looping Worker Function

This function runs on the thread in an indefinite loop until the thread is terminated. The purpose of the worker function is to wait until a start event has been sent, execute the command list, tell the main thread that the command list is empty, and wait until the next start event. WaitForSingleObject() tells the thread to pause until a start event has been set. After the thread has completed it’s assigned task, call SetEvent() to inform the main thread that the process is complete.

Note This technique allows you to assign multiple functions to the threads. For example, you could call a function that tells each thread to update the data it is working with, then have each thread run a function that renders the updated information.

A sample thread worker function:

C++

void Sample::WorkerThreadProcess( const UINT32 threadIndex)
{
    ThreadWorkingData &threadWorkingData = m_ThreadWorkingData[threadIndex];
    do
    {
        // Wait for main thread to signal ready
        WaitForSingleObject( threadWorkingData.m_StartEvent, INFINITE );

        // Perform the currently desired function. (Skip if thread is being terminated)
        if( !m_ShutdownThreads )
        {
            (this->*m_WorkerThreadFunction)( threadIndex );
        }

        // Tell main thread command list is finished
        SetEvent( m_ThreadFinishedEvent[threadIndex - 1] );
    }
    while( !m_ShutdownThreads );
}  

Simultaneous Thread Execution

The threads are properly initialized and awaiting instructions. Now you need to manage them so that the threads run in a synchronized manner. Walk through the list of sub-threads and call SetEvent() to start each thread. Once the sub-threads are started, you may tell the main thread to perform the same function. Call WaitForMultipleObjects() to tell the main thread to wait until every sub-thread has completed its task.

A sample function that executes all worker threads simultaneously:

C++

float Sample::ExecuteFunctionConcurrently( WorkerThreadFunction function, UINT32 numberOfWorkerThreads )
{
    numberOfWorkerThreads = std::min( numberOfWorkerThreads, s_NumberOfThreads );

    // m_WorkerThreadFunction is a function pointer. You may call any function on all threads
    m_WorkerThreadFunction = function;

    // Signal all worker threads, then wait for completion
    // 0 index is used for main thread, to keep GPU rendering order consistent when deferred contexts are in use
    for ( UINT32 thread = 1; thread <= numberOfWorkerThreads; ++thread )
    {
        SetEvent( m_ThreadWorkingData[thread].m_StartEvent );
    }
    // Send main thread in to help out
    (this->*m_WorkerThreadFunction)( 0 );

    // Timing data
    LARGE_INTEGER startTime, endTime;
    QueryPerformanceCounter( &startTime );

    // wait for all worker threads to complete
    WaitForMultipleObjects( numberOfWorkerThreads, m_ThreadFinishedEvent, TRUE, INFINITE );

    // More timing
    QueryPerformanceCounter( &endTime );

    // finished
    m_WorkerThreadFunction = NULL;

    // Calculate how long it took for all the threads to execute
    return float( endTime.QuadPart - startTime.QuadPart ) * m_TimerToMilliseconds;
}  

See also

DirectX