Jorge
Jorge Autor de Aprende Unreal Engine 4.

DirectX 12: Swapchain

DirectX 12: Swapchain

Swapchain permite mostrar por pantalla los render target generados por la GPU. En este tutorial ¡por fin mostraremos algo por pantalla!

Tabla de contenidos

Parte 1. Window

Parte 2. Adapter

Parte 3. Conceptos

Parte 4. Queue

Parte 5. Resources

Parte 6. Descriptores

Parte 7. Pipeline State

» Parte 8. Swapchain

Parte 9. Triángulo

Parte 10. Root Signature

Parte 11. Texturas

Swapchain

El resultado final del pipeline es un render target que queremos mostrar por pantalla.

Si creamos un único recurso render target tendremos el problema del flickering. Básicamente es que lo que mostramos en pantalla se está calculando al mismo tiempo.

Para ello, normalmente usamos dos render targets. Solo uno de ellos se muestra por pantalla y el otro se usa como salida del pipeline.

Cuando el render target está calculado entonces hacemos flip y mostramos el render target recién calculado mientras que usamos el que previamente se mostraba por pantalla para calcular el siguiente frame.

A este técnica se le suele denominar double buffering. Podemos usar más de dos render targets (con tres sería triple buffering).

La buena noticia es que todo este proceso de gestionar los render target y hacer los flip lo hace DX12 por nosotros con IDXGISwapChain.

DXGI_SWAP_CHAIN_DESC1 SwapchainDesc{};

SwapchainDesc.BufferCount = 2;
SwapchainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;

SwapchainDesc.Width = Width;
SwapchainDesc.Height = Height;
SwapchainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;

SwapchainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;

SwapchainDesc.SampleDesc.Count = 1;

ComPtr<IDXGISwapChain1> SwapchainTemp;
Factory->CreateSwapChainForHwnd(
    CommandQueue.Get(), // swap chain forces flush when does flip
    hWnd,
    &SwapchainDesc,
    nullptr, // pFullscreenDesc
    nullptr, // pRestrictToOutput
    &SwapchainTemp
);

ComPtr<IDXGISwapChain3> Swapchain;
SwapchainTemp.As(&Swapchain);

Render Target

Swapchain ha creado por nosotros los recursos render target. Podemos usar el método GetBuffer para obtenerlos.

ComPtr<ID3D12Resource> RenderTargets[kFrameCount]; // extracted from Swapchain

for (UINT FrameIndex = 0; FrameIndex < kFrameCount; ++FrameIndex)
{
  Swapchain->GetBuffer(FrameIndex, IID_PPV_ARGS(&RenderTargets[FrameIndex]));        
}

Completando el código del proyecto

En el código del proyecto tenemos el siguiente código pendiente del anterior tutorial sobre descriptores:

void DemoApp::CreateRenderTargets()
{
  /* RenderTargetHeap */
  // ...

  for (UINT FrameIndex = 0; FrameIndex < kFrameCount; ++FrameIndex)
  {
    /*
    @TODO:
    Crearemos el recurso RenderTargets[FrameIndex]
    en siguientes tutoriales. En concreto, el relacionado
    a Swapchain.
    */

    /* Crear el descriptor para RenderTargets[FrameIndex] */
    // ...
  }
}

Ahora podemos completarlo con:

void DemoApp::CreateRenderTargets()
{
  /* RenderTargetHeap */
  // ...

  for (UINT FrameIndex = 0; FrameIndex < kFrameCount; ++FrameIndex)
  {
    // Obtener el resource
    Swapchain->GetBuffer(FrameIndex, IID_PPV_ARGS(&RenderTargets[FrameIndex])); 

    /* Crear el descriptor para RenderTargets[FrameIndex] */
    // ...
  }
}

Presentar por pantalla

Para presentar por pantalla el back render target del swapchain se usa el método Present.

El siguiente código de ejemplo ejemplifica como se usaría:

// para cada frame
while(true)
{
  // grabar los comandos para renderizar
  RecordCommandList();

  // subir los comandos a la GPU para que se ejecuten
  ID3D12CommandList* ppCommandLists[] = { CommandList.Get() };
  CommandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);

  // encolar comando presentar por pantalla
  Swapchain->Present(1, 0);    

  FlushAndWait(); // Sync CPU/GPU
}

Para que el pipeline escriba en el back render target del swapchain usamos el método GetCurrentBackBufferIndex()

void RecordCommandList() {
  UINT BackFrameIndex = Swapchain->GetCurrentBackBufferIndex();
  // ...
  // CommandList->...., CommandList->...., CommandList->....
  // ...
    
  // Obtener el render target descriptor para este BackFrameIndex
  D3D12_CPU_DESCRIPTOR_HANDLE RenderTargetDescriptor = RenderTargetViewHeap->GetCPUDescriptorHandleForHeapStart();
  RenderTargetDescriptor.ptr += ((SIZE_T)BackFrameIndex) * Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

  CommandList->OMSetRenderTargets(
    1, 
    &RenderTargetDescriptor, 
    FALSE, 
    nullptr // depth/stencil
  );
}

Añadir Swapchain al proyecto

Y después de ocho tutoriales nuestro proyecto por fin hace algo. Que la pantalla se vea de color rojo. ¡Bravísimo!

El código completo sería:

// DemoApp.h

#pragma once

// ComPtr<T>
#include <wrl.h>
using namespace Microsoft::WRL;

// DirectX 12 specific headers.
#include <d3d12.h>
#include <dxgi1_6.h>
#include <d3dcompiler.h>
#include <DirectXMath.h>

using namespace DirectX;

static const UINT8 kFrameCount = 2;

class DemoApp
{
public:	
  DemoApp(HWND hWnd, UINT Width, UINT Height);

  /* Run */
  void Tick();

private:
  /* Device */
  ComPtr<IDXGIFactory4> Factory;
  ComPtr<ID3D12Device> Device;
	
  void CreateDevice();

  /* Queue */
  ComPtr<ID3D12CommandQueue> CommandQueue;
  ComPtr<ID3D12CommandAllocator> CommandAllocator;
  ComPtr<ID3D12GraphicsCommandList> CommandList;
	
  void CreateQueues();

  /* Fences */
  ComPtr<ID3D12Fence> Fence;
  HANDLE FenceEvent;
  UINT64 FenceValue;

  void CreateFence();
  void FlushAndWait();

  /* Pipeline */
  /* -- para este ejemplo no necesitamos pipeline ni root signature */
  ComPtr<ID3DBlob> LoadShader(LPCWSTR Filename, LPCSTR EntryPoint, LPCSTR Target);
  ComPtr<ID3D12RootSignature> RootSignature;
  ComPtr<ID3D12PipelineState> PipelineState;
  void CreateRootSignature();
  void CreatePipeline();

  /* Swapchain */
  ComPtr<IDXGISwapChain3> Swapchain;
  ComPtr<ID3D12DescriptorHeap> RenderTargetViewHeap;
  ComPtr<ID3D12Resource> RenderTargets[kFrameCount]; // extracted from Swapchain
  void CreateRenderTargets();
  void CreateSwapchain(HWND hWnd, UINT Width, UINT Height);

  /* Record command list for render */
  void RecordCommandList();
};

Mientras que la implementación:

#include "DemoApp.h"
#include "GPUMem.h"

DemoApp::DemoApp(HWND hWnd, UINT Width, UINT Height)
{	
  CreateDevice();
  CreateQueues();
  CreateFence();

  CreateSwapchain(hWnd, Width, Height);
  CreateRenderTargets();

  //CreateRootSignature();
  //CreatePipeline();
}

void DemoApp::CreateDevice()
{
  CreateDXGIFactory1(IID_PPV_ARGS(&Factory));

  ComPtr<IDXGIAdapter1> Adapter;
  bool bAdapterFound = false;

  for (UINT AdapterIndex = 0;
    !bAdapterFound && Factory->EnumAdapters1(AdapterIndex, &Adapter) != DXGI_ERROR_NOT_FOUND;
    ++AdapterIndex)
  {
    DXGI_ADAPTER_DESC1 AdapterDesc;
    Adapter->GetDesc1(&AdapterDesc);

    if (AdapterDesc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
    {
      continue;
    }

    HRESULT hr;
    hr = D3D12CreateDevice(Adapter.Get(), D3D_FEATURE_LEVEL_11_0, _uuidof(ID3D12Device), nullptr);
    if (SUCCEEDED(hr))
    {
      bAdapterFound = true;
    }
  }

  D3D12CreateDevice(Adapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&Device));
}

void DemoApp::CreateQueues()
{
  // CommandQueue

  D3D12_COMMAND_QUEUE_DESC CommandQueueDesc{};
	
  CommandQueueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
  CommandQueueDesc.NodeMask = 0;	
  CommandQueueDesc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL;
  CommandQueueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
  Device->CreateCommandQueue(&CommandQueueDesc, IID_PPV_ARGS(&CommandQueue));

  // Command Allocator
  Device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&CommandAllocator));

  // Command List
  ID3D12PipelineState* InitialState = nullptr;
  Device->CreateCommandList(
    0, 
    D3D12_COMMAND_LIST_TYPE_DIRECT, 
    CommandAllocator.Get(), 
    InitialState, 
    IID_PPV_ARGS(&CommandList)
  );
  CommandList->Close();
}

void DemoApp::CreateFence()
{
  FenceValue = 0;
	
  Device->CreateFence(FenceValue, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&Fence));
	
  FenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);	
}

void DemoApp::FlushAndWait()
{
  const UINT64 FenceValueToSignal = FenceValue;
  CommandQueue->Signal(Fence.Get(), FenceValueToSignal);

  ++FenceValue;

  if (Fence->GetCompletedValue() < FenceValueToSignal)
  {
    Fence->SetEventOnCompletion(FenceValueToSignal, FenceEvent);
    WaitForSingleObject(FenceEvent, INFINITE);
  }
}

ComPtr<ID3DBlob> DemoApp::LoadShader(LPCWSTR Filename, LPCSTR EntryPoint, LPCSTR Target)
{
    /* no lo usaremos en este ejemplo */
}

void DemoApp::CreateRootSignature()
{
    /* no lo usaremos en este ejemplo */
}

void DemoApp::CreatePipeline()
{
    /* no lo usaremos en este ejemplo */
}

void DemoApp::CreateRenderTargets()
{
  /* RenderTargetHeap */
  D3D12_DESCRIPTOR_HEAP_DESC DescriptorHeapDesc{};
  DescriptorHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
  DescriptorHeapDesc.NodeMask = 0;
  DescriptorHeapDesc.NumDescriptors = kFrameCount;
  DescriptorHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;

  Device->CreateDescriptorHeap(&DescriptorHeapDesc, IID_PPV_ARGS(&RenderTargetViewHeap));

  for (UINT FrameIndex = 0; FrameIndex < kFrameCount; ++FrameIndex)
  {
    Swapchain->GetBuffer(FrameIndex, IID_PPV_ARGS(&RenderTargets[FrameIndex]));

    D3D12_RENDER_TARGET_VIEW_DESC RTDesc{};
    RTDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
    RTDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    RTDesc.Texture2D.MipSlice = 0;
    RTDesc.Texture2D.PlaneSlice = 0;

    D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor = RenderTargetViewHeap->GetCPUDescriptorHandleForHeapStart();
    DestDescriptor.ptr += ((SIZE_T)FrameIndex) * Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

    Device->CreateRenderTargetView(RenderTargets[FrameIndex].Get(), &RTDesc, DestDescriptor);
  }
}

void DemoApp::CreateSwapchain(HWND hWnd, UINT Width, UINT Height)
{
  /* Swapchain */
  DXGI_SWAP_CHAIN_DESC1 SwapchainDesc{};

  SwapchainDesc.BufferCount = 2;
  SwapchainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;

  SwapchainDesc.Width = Width;
  SwapchainDesc.Height = Height;
  SwapchainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;

  SwapchainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;

  SwapchainDesc.SampleDesc.Count = 1;

  ComPtr<IDXGISwapChain1> SwapchainTemp;
  Factory->CreateSwapChainForHwnd(
    CommandQueue.Get(), // swap chain forces flush when does flip
    hWnd,
    &SwapchainDesc,
    nullptr,
    nullptr,
    &SwapchainTemp
  );
	
  SwapchainTemp.As(&Swapchain);	
}

void DemoApp::RecordCommandList()
{
  const UINT BackFrameIndex = Swapchain->GetCurrentBackBufferIndex();

  CommandAllocator->Reset();
  CommandList->Reset(CommandAllocator.Get(), nullptr); // para borrar la pantalla no necesitamos un pipeline

  D3D12_CPU_DESCRIPTOR_HANDLE RenderTargetDescriptor = RenderTargetViewHeap->GetCPUDescriptorHandleForHeapStart();
  RenderTargetDescriptor.ptr += ((SIZE_T)BackFrameIndex) * Device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

  GPUMem::ResourceBarrier(CommandList.Get(), RenderTargets[BackFrameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET);

  const FLOAT ClearValue[] = { 1.0f, 0.0f, 0.0f, 1.0f };
  CommandList->ClearRenderTargetView(RenderTargetDescriptor, ClearValue, 0, nullptr);
	
  GPUMem::ResourceBarrier(CommandList.Get(), RenderTargets[BackFrameIndex].Get(), D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT);

  CommandList->Close();
}

void DemoApp::Tick()
{
  RecordCommandList();
  ID3D12CommandList* ppCommandLists[] = { CommandList.Get() };
  CommandQueue->ExecuteCommandLists(1, ppCommandLists);
  Swapchain->Present(1, 0);
  FlushAndWait();
}

Y el código del fichero main.cpp

// main.cpp

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
  // ...
  DemoApp App{hWnd, Width, Height};
  // ...
  while (true)
  {
    // ...
    App.Tick();
  }
}

Y nuestro espectacular resultado:

Pantalla en rojo jaja saludos

comments powered by Disqus