Inicialização do Pipeline Gráfico com DirectX 12

Criação do Dispositivo D3D

O dispositivo Direct3D atua como uma abstração do adaptador de exibição, que geralmente é a placa de vídeo, mas também pode ser emulado por software. A interface a seguir é utilizada para instanciar o dispositivo:

HRESULT WINAPI D3D12CreateDevice(
    IUnknown* pAdapter,
    D3D_FEATURE_LEVEL MinFeatureLevel, 
    REFIID riid, 
    void** ppDevice
);

O primeiro parâmetro define o adaptador alvo. É possível enumerar os adaptadores disponíveis através de IDXGIFactory::EnumAdapters. Se nullptr for passado, o adaptador padrão é selecionado. Em caso de falha na criação do dispositivo de hardware, o adaptador WARP (software) pode ser obtido via IDXGIFactory4::EnumWarpAdapter. Diferente do DX11, onde o tipo de driver era especificado diretamente na criação, o DX12 delega a gestão de adaptadores de software à fábrica DXGI. Os dois últimos parâmetros da função podem ser preenchidos convenientemente com a macro IID_PPV_ARGS:

#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)

Esta macro extrai o UUID da interface COM e converte o ponteiro para void** de forma segura.

Após instanciar o dispositivo, é fundamental configurar um mecanismo de sincronização entre a CPU e a GPU. Como o DX12 opera com renderização deferred e um modelo de listas e filas de comandos, a sincronização é feita através de cercas (fences). A criação do objeto de cerca é feita assim:

HRESULT CreateFence(
    UINT64 InitialValue,
    D3D12_FENCE_FLAGS Flags,
    REFIID riid,
    void** ppFence
);

Além disso, é necessário consultar o tamanho dos descritores, pois varia conforme a arquitetura da GPU. Para otimizar, invoque ID3D12Device::GetDescriptorHandleIncrementSize uma única vez e armazene o resultado em cache.

Filas, Listas e Alocadores de Comandos

Para orquestrar a execução paralela, o DX12 utiliza a interface ID3D12CommandQueue para representar a fila de hardware, enquanto ID3D12CommandList (geralmente a derivada ID3D12GraphicsCommandList) agrupa as instruções. A fila é criada da seguinte forma:

HRESULT CreateCommandQueue(
    const D3D12_COMMAND_QUEUE_DESC* pDesc,
    REFIID riid,
    void** ppCommandQueue
);

As listas de comandos são instanciadas através do método abaixo:

HRESULT CreateCommandList(
    UINT nodeMask,
    D3D12_COMMAND_LIST_TYPE type,
    ID3D12CommandAllocator* pCommandAllocator,
    ID3D12PipelineState* pInitialState,
    REFIID riid,
    void** ppCommandList
);

Por padrão, uma lista recém-criada ou resetada encontra-se no estado "aberto". É imperativo chamar ID3D12GraphicsCommandList::Close antes de submetê-la à fila via ID3D12CommandQueue::ExecuteCommandLists. A execução na GPU é assíncrona, reforçando a necessidade das cercas (fences) para controle de fluxo.

Os comandos gravados não residem na lista em si, mas sim no alocador de comandos (ID3D12CommandAllocator), que gerencia a memória subjacente. O alocador é criado assim:

HRESULT CreateCommandAllocator(
    D3D12_COMMAND_LIST_TYPE type,
    REFIID riid,
    void** ppCommandAllocator
);

Ao reutilizar uma lista de comandos, tanto a lista quanto o alocador devem ser resetados. Contudo, nunca resete o alocador enquanto a GPU ainda estiver processando comandos oriundos dele, sob pena de corrupção de memória ou falhas catastróficas.

Abaixo está uma implementação robusta para sincronizar a CPU com a GPU, aguardando a conclusão dos comandos submetidos:

// Incrementa o valor da cerca para marcar o fim da submissão atual
currentFenceValue++;

// Insere um sinal na fila de comandos. A GPU atualizará a cerca 
// apenas quando terminar de processar todos os comandos anteriores.
ThrowIfFailed(cmdQueue->Signal(syncFence.Get(), currentFenceValue));

// Verifica se a GPU já alcançou o valor da cerca
if (syncFence->GetCompletedValue() < currentFenceValue) {
    HANDLE waitEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
    
    // Configura a cerca para sinalizar o evento quando a GPU atingir o valor
    ThrowIfFailed(syncFence->SetEventOnCompletion(currentFenceValue, waitEvent));
    
    // Bloqueia a thread da CPU até que o evento seja sinalizado
    if (waitEvent != NULL) {
        WaitForSingleObject(waitEvent, INFINITE);
        CloseHandle(waitEvent);
    }
}

Configuração e Criação da Swap Chain

Para evitar tearing e flickering, a renderização ocorre em buffers de fundo (back buffers) que são posteriormente trocados com o bufffer de frente. O DX12 recomenda o uso de IDXGIFactory2::CreateSwapChainForHwnd em vez das APIs legadas. Esta função utiliza a estrutura DXGI_SWAP_CHAIN_DESC1:

HRESULT CreateSwapChainForHwnd(
    IUnknown* pDevice,
    HWND hWnd,
    const DXGI_SWAP_CHAIN_DESC1* pDesc,
    const DXGI_SWAP_CHAIN_FULLSCREEN_DESC* pFullscreenDesc,
    IDXGIOutput* pRestrictToOutput,
    IDXGISwapChain1** ppSwapChain
);

No DX12, o primeiro parâmetro deve ser um ponteiro para a fila de comandos, não para o dispositivo. A estrutura de descrição é definida como:

typedef struct DXGI_SWAP_CHAIN_DESC1 {
    UINT Width;
    UINT Height;
    DXGI_FORMAT Format;
    BOOL Stereo;
    DXGI_SAMPLE_DESC SampleDesc;
    DXGI_USAGE BufferUsage;
    UINT BufferCount;
    DXGI_SCALING Scaling;
    DXGI_SWAP_EFFECT SwapEffect;
    DXGI_ALPHA_MODE AlphaMode;
    UINT Flags;
} DXGI_SWAP_CHAIN_DESC1;

É crucial notar que o DX12 não suporta MSAA diretamente na swap chain. Os valores de SampleDesc.Count e SampleDesc.Quality devem ser estritamente 1 e 0. Se multisampling for necessário, a resolução deve ser feita renderizando para uma textura MSAA separada e resolvendo-a para o back buffer.

Descritores e Views de Recursos

Recursos de GPU são blocos de memória opacos. Para que o pipeline os interprete corretamente, utilizamos descritores (Resource Views). Eles atuam como metadados que informam à GPU como ler ou escrever em um recurso específico. Os descritores são armazenados em Descriptor Heaps, criados através da estrutura:

typedef struct D3D12_DESCRIPTOR_HEAP_DESC {
    D3D12_DESCRIPTOR_HEAP_TYPE Type;
    UINT NumDescriptors;
    D3D12_DESCRIPTOR_HEAP_FLAGS Flags;
    UINT NodeMask;
} D3D12_DESCRIPTOR_HEAP_DESC;

Redimensionamento e Recriação de Recursos

Quando a janela é redimensionada, a swap chain e os buffers de profundidade devem ser recriados. Este processo geralmente ocorre em um método de callback de redimensionamento:

// Garante que a GPU terminou de processar os frames anteriores
FlushCommandQueue();

ThrowIfFailed(cmdList->Reset(cmdAllocator.Get(), nullptr));

// Libera as referências dos buffers antigos
for (UINT i = 0; i < backBufferCount; ++i) {
    backBuffers[i].Reset();
}
depthStencilBuffer.Reset();

// Redimensiona a swap chain
ThrowIfFailed(dxgiSwapChain->ResizeBuffers(
    backBufferCount, 
    clientWidth, 
    clientHeight, 
    backBufferFormat, 
    DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH));

currentBackBufferIndex = 0;

Em seguida, as Render Target Views (RTVs) são recriadas e vinculadas aos novos buffers:

CD3DX12_CPU_DESCRIPTOR_HANDLE rtvDescriptor(rtvHeap->GetCPUDescriptorHandleForHeapStart());
for (UINT i = 0; i < backBufferCount; ++i) {
    ThrowIfFailed(dxgiSwapChain->GetBuffer(i, IID_PPV_ARGS(&backBuffers[i])));
    d3dDevice->CreateRenderTargetView(backBuffers[i].Get(), nullptr, rtvDescriptor);
    rtvDescriptor.Offset(1, rtvDescriptorSize);
}

O buffer de profundidade e estêncil (Depth/Stencil) também exige recriação:

D3D12_RESOURCE_DESC depthDesc = {};
depthDesc.MipLevels = 1;
depthDesc.Format = DXGI_FORMAT_R24G8_TYPELESS;
depthDesc.Width = clientWidth;
depthDesc.Height = clientHeight;
depthDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;
depthDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
depthDesc.DepthOrArraySize = 1;
depthDesc.SampleDesc.Count = 1;
depthDesc.SampleDesc.Quality = 0;
depthDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;

D3D12_CLEAR_VALUE depthClearValue = {};
depthClearValue.Format = depthStencilFormat;
depthClearValue.DepthStencil.Depth = 1.0f;
depthClearValue.DepthStencil.Stencil = 0;

CD3DX12_HEAP_PROPERTIES defaultHeap(D3D12_HEAP_TYPE_DEFAULT);

ThrowIfFailed(d3dDevice->CreateCommittedResource(
    &defaultHeap,
    D3D12_HEAP_FLAG_NONE,
    &depthDesc,
    D3D12_RESOURCE_STATE_COMMON,
    &depthClearValue,
    IID_PPV_ARGS(&depthStencilBuffer)));

A Depth Stencil View (DSV) é então configurada:

D3D12_DEPTH_STENCIL_VIEW_DESC dsvDesc = {};
dsvDesc.Flags = D3D12_DSV_FLAG_NONE;
dsvDesc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D;
dsvDesc.Format = depthStencilFormat;

d3dDevice->CreateDepthStencilView(
    depthStencilBuffer.Get(), 
    &dsvDesc, 
    dsvHeap->GetCPUDescriptorHandleForHeapStart());

Por fim, uma barreira de recurso é inserida para transicionar o buffer de profundidade para o estado de escrita, e a lista de comandos é submetida:

CD3DX12_RESOURCE_BARRIER depthBarrier = CD3DX12_RESOURCE_BARRIER::Transition(
    depthStencilBuffer.Get(),
    D3D12_RESOURCE_STATE_COMMON,
    D3D12_RESOURCE_STATE_DEPTH_WRITE);

cmdList->ResourceBarrier(1, &depthBarrier);
ThrowIfFailed(cmdList->Close());

ID3D12CommandList* submitLists[] = { cmdList.Get() };
cmdQueue->ExecuteCommandLists(1, submitLists);

FlushCommandQueue();

Para concluir a configuração da viewport e do retângulo de corte (scissor rect) com as novas dimensões:

mainViewport.TopLeftX = 0.0f;
mainViewport.TopLeftY = 0.0f;
mainViewport.Width = static_cast<float>(clientWidth);
mainViewport.Height = static_cast<float>(clientHeight);
mainViewport.MinDepth = 0.0f;
mainViewport.MaxDepth = 1.0f;

mainScissorRect = { 0, 0, static_cast<LONG>(clientWidth), static_cast<LONG>(clientHeight) };

Tags: DirectX12 C++ DXGI D3D12 GraphicsProgramming

Publicado em 7-27 17:40