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