Passes de renderização do Direct3D 12

O recurso de passes de renderização é novo para Windows 10, versão 1809 (10.0; Build 17763) e apresenta o conceito de uma passagem de renderização do Direct3D 12. Um passe de renderização consiste em um subconjunto dos comandos que você registra em uma lista de comandos.

Para declarar onde cada passagem de renderização começa e termina, aninha os comandos que pertencem a essa passagem dentro de chamadas para ID3D12GraphicsCommandList4::BeginRenderPass e EndRenderPass. Consequentemente, qualquer lista de comandos contém zero, um ou mais passes de renderização.

Cenários

As passagens de renderização poderão melhorar o desempenho do renderizador se ele for baseado em Tile-Based TBDR (Renderização Adiada), entre outras técnicas. Mais especificamente, a técnica ajuda o renderizador a melhorar a eficiência da GPU, reduzindo o tráfego de memória de/para a memória fora do chip, permitindo que seu aplicativo identifique melhor os requisitos de ordenação de renderização de recursos e as dependências de dados.

Um driver de exibição escrito expressamente para aproveitar o recurso de passes de renderização fornece os melhores resultados. Mas as APIs de renderização podem ser executadas mesmo em drivers pré-existentes (embora, não necessariamente com melhorias de desempenho).

Esses são os cenários em que as passagens de renderização são projetadas para fornecer valor.

Permitir que seu aplicativo evite carregamentos/repositórios desnecessários de/para main memória em uma arquitetura tbdr (renderização adiada) Tile-Based

Uma das propostas de valor dos passes de renderização é que ele fornece um local central para indicar as dependências de dados do aplicativo para um conjunto de operações de renderização. Essas dependências de dados permitem que o driver de exibição inspecione esses dados em tempo de associação/barreira e emita instruções que minimizem cargas/armazenamentos de recursos de/para main memória.

Permitir que sua arquitetura TBDR retome de forma oportunista recursos no cache no chip em passagens de renderização (mesmo em listas de comandos separadas)

Observação

Especificamente, esse cenário é limitado aos casos em que você está gravando nos mesmos destinos de renderização em várias listas de comandos.

Um padrão de renderização comum é que seu aplicativo renderize para os mesmos destinos de renderização em várias listas de comandos serialmente, mesmo que os comandos de renderização sejam gerados em paralelo. O uso de passes de renderização nesse cenário permite que essas passagens sejam combinadas de tal forma (já que o aplicativo sabe que retomará a renderização na lista de comandos de êxito imediato) que o driver de exibição pode evitar uma liberação para main memória nos limites da lista de comandos.

Responsabilidades do aplicativo

Mesmo com o recurso de passes de renderização, nem o runtime do Direct3D 12 nem o driver de exibição assumem a responsabilidade de deduzer oportunidades para reordificar/evitar cargas e repositórios. Para aproveitar corretamente o recurso de passes de renderização, seu aplicativo tem essas responsabilidades.

  • Identifique corretamente as dependências de dados/ordenação para suas operações.
  • Ordene seus envios de uma maneira que minimize as liberações (portanto, minimize o uso de sinalizadores de _PRESERVE ).
  • Use corretamente as barreiras de recursos e acompanhe o estado do recurso.
  • Evite cópias/limpezas desnecessárias. Para ajudar a identificá-los, você pode usar os avisos de desempenho automatizados da ferramenta PIX no Windows.

Usando o recurso de passagem de renderização

O que é um passe de renderização?

Uma passagem de renderização é definida por esses elementos.

  • Um conjunto de associações de saída que são corrigidas durante a passagem de renderização. Essas associações são para uma ou mais RTVs (exibições de destino de renderização) e/ou para uma exibição de estêncil de profundidade (DSV).
  • Uma lista de operações de GPU direcionadas a esse conjunto de associações de saída.
  • Metadados que descrevem as dependências de carregamento/repositório para todas as associações de saída direcionadas pela passagem de renderização.

Declarar suas associações de saída

No início de uma passagem de renderização, você declara associações aos destinos de renderização e/ou ao buffer de profundidade/estêncil. É opcional associar a destinos de renderização e é opcional associar a um buffer de profundidade/estêncil. Mas você deve associar a pelo menos um dos dois e, no exemplo de código abaixo, associamos a ambos.

Você declara essas associações em uma chamada para ID3D12GraphicsCommandList4::BeginRenderPass.

void render_passes(::ID3D12GraphicsCommandList4 * pIGCL4,
    D3D12_CPU_DESCRIPTOR_HANDLE const& rtvCPUDescriptorHandle,
    D3D12_CPU_DESCRIPTOR_HANDLE const& dsvCPUDescriptorHandle)
{
    const float clearColor4[]{ 0.f, 0.f, 0.f, 0.f };
    CD3DX12_CLEAR_VALUE clearValue{ DXGI_FORMAT_R32G32B32_FLOAT, clearColor4 };

    D3D12_RENDER_PASS_BEGINNING_ACCESS renderPassBeginningAccessClear{ D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_CLEAR, { clearValue } };
    D3D12_RENDER_PASS_ENDING_ACCESS renderPassEndingAccessPreserve{ D3D12_RENDER_PASS_ENDING_ACCESS_TYPE_PRESERVE, {} };
    D3D12_RENDER_PASS_RENDER_TARGET_DESC renderPassRenderTargetDesc{ rtvCPUDescriptorHandle, renderPassBeginningAccessClear, renderPassEndingAccessPreserve };

    D3D12_RENDER_PASS_BEGINNING_ACCESS renderPassBeginningAccessNoAccess{ D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE_NO_ACCESS, {} };
    D3D12_RENDER_PASS_ENDING_ACCESS renderPassEndingAccessNoAccess{ D3D12_RENDER_PASS_ENDING_ACCESS_TYPE_NO_ACCESS, {} };
    D3D12_RENDER_PASS_DEPTH_STENCIL_DESC renderPassDepthStencilDesc{ dsvCPUDescriptorHandle, renderPassBeginningAccessNoAccess, renderPassBeginningAccessNoAccess, renderPassEndingAccessNoAccess, renderPassEndingAccessNoAccess };

    pIGCL4->BeginRenderPass(1, &renderPassRenderTargetDesc, &renderPassDepthStencilDesc, D3D12_RENDER_PASS_FLAG_NONE);
    // Record command list.
    pIGCL4->EndRenderPass();
    // Begin/End further render passes and then execute the command list(s).
}

Você define o primeiro campo da estrutura D3D12_RENDER_PASS_RENDER_TARGET_DESC para o identificador do descritor de CPU correspondente a uma ou mais RTVs (exibições de destino de renderização). Da mesma forma, D3D12_RENDER_PASS_DEPTH_STENCIL_DESC contém o identificador do descritor de CPU correspondente a uma DSV (exibição de estêncil de profundidade). Esses identificadores de descritor de CPU são os mesmos que, de outra forma, você passaria para ID3D12GraphicsCommandList::OMSetRenderTargets. E, assim como acontece com OMSetRenderTargets, os descritores de CPU são retirados de seus respectivos heaps (descritor de CPU) no momento da chamada para BeginRenderPass.

Os RTVs e o DSV não são herdados na passagem de renderização. Em vez disso, eles devem ser definidos. Nem os RTVs e DSV são declarados em BeginRenderPass propagados para a lista de comandos. Em vez disso, eles estão em um estado indefinido após a passagem de renderização.

Renderizar passagens e cargas de trabalho

Você não pode aninhar passes de renderização e não pode ter uma passagem de renderização entre mais de uma lista de comandos (elas devem começar e terminar durante a gravação em uma única lista de comandos). As otimizações projetadas para habilitar a geração eficiente de vários threads de passes de renderização são discutidas na seção renderizar sinalizadores, abaixo.

Uma gravação que você faz de dentro de um passe de renderização não é válida para você ler até uma passagem de renderização subsequente. Isso impede alguns tipos de barreiras de dentro da passagem de renderização, por exemplo, a barreira de RENDER_TARGET para SHADER_RESOURCE no destino de renderização associado no momento. Para obter mais informações, consulte a seção Renderizar passagens e barreiras de recursos, abaixo.

A única exceção à restrição write-read mencionada envolve as leituras implícitas que ocorrem como parte do teste de profundidade e da combinação de destino de renderização. Portanto, essas APIs não são permitidas em uma passagem de renderização (o runtime principal remove a lista de comandos se alguma delas for chamada durante a gravação).

Renderizar passagens e barreiras de recursos

Você não pode ler ou consumir uma gravação que ocorreu na mesma passagem de renderização. Determinadas barreiras não estão em conformidade com essa restrição, por exemplo, de D3D12_RESOURCE_STATE_RENDER_TARGET a *_SHADER_RESOURCE no destino de renderização associado no momento (e a camada de depuração ocorrerá com esse efeito). No entanto, essa mesma barreira em um destino de renderização que foi gravado fora da passagem de renderização atual está em conformidade, pois as gravações serão concluídas antes da inicialização da passagem de renderização atual. Você pode se beneficiar de saber sobre determinadas otimizações que um driver de exibição pode fazer nesse sentido. Dada uma carga de trabalho em conformidade, um driver de exibição pode mover quaisquer barreiras encontradas em sua passagem de renderização para o início da passagem de renderização. Lá, eles podem ser agrupados (e não interferir em nenhuma operação de tiling/binning). Essa é uma otimização válida desde que todas as suas gravações tenham sido concluídas antes do início da passagem de renderização atual.

Aqui está um exemplo de otimização de driver mais completo, que pressupõe que você tenha um mecanismo de renderização que tenha um design de associação de recursos no estilo pré-Direct3D 12, fazendo barreiras sob demanda com base em como os recursos são associados. Ao gravar em um UAV (modo de exibição de acesso não ordenado) no final de um quadro (a ser consumido no quadro a seguir), o mecanismo pode deixar o recurso no estado D3D12_RESOURCE_STATE_UNORDERED_ACCESS na conclusão do quadro. No quadro a seguir, quando o mecanismo for associar o recurso como uma SRV (exibição de recurso de sombreador), ele descobrirá que o recurso não está no estado correto e emitirá uma barreira de D3D12_RESOURCE_STATE_UNORDERED_ACCESS para D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE. Se essa barreira ocorrer dentro da passagem de renderização, o driver de exibição será justificado ao assumir que todas as gravações já ocorreram fora dessa passagem de renderização atual e, consequentemente (e é aqui que entra a otimização), o driver de exibição pode mover a barreira até o início da passagem de renderização. Novamente, isso é válido, desde que seu código esteja em conformidade com a restrição de leitura de gravação descrita nesta seção e a última.

Estes são exemplos de barreiras compatíveis.

  • D3D12_RESOURCE_STATE_UNORDERED_ACCESSD3D12_RESOURCE_STATE_INDIRECT_ARGUMENT.
  • D3D12_RESOURCE_STATE_COPY_DEST para *_SHADER_RESOURCE.

E estes são exemplos de barreiras não compatíveis.

  • D3D12_RESOURCE_STATE_RENDER_TARGET a qualquer estado de leitura em RTVs/DSVs associados no momento.
  • D3D12_RESOURCE_STATE_DEPTH_WRITE a qualquer estado de leitura em RTVs/DSVs associados no momento.
  • Qualquer barreira de alias.
  • Barreiras de UAV (exibição de acesso não ordenada). 

Declaração de acesso a recursos

No horário de BeginRenderPass , além de declarar todos os recursos que estão servindo como RTVs e/ou DSV dentro dessa passagem, você também deve especificar suas características de acesso inicial e final. Como você pode ver no exemplo de código na seção Declarar suas associações de saída acima, faça isso com as estruturas D3D12_RENDER_PASS_RENDER_TARGET_DESC e D3D12_RENDER_PASS_DEPTH_STENCIL_DESC .

Para obter mais detalhes, consulte as estruturas D3D12_RENDER_PASS_BEGINNING_ACCESS e D3D12_RENDER_PASS_ENDING_ACCESS e as enumerações D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE e D3D12_RENDER_PASS_ENDING_ACCESS_TYPE .

Renderizar sinalizadores de passagem

O último parâmetro passado para BeginRenderPass é um sinalizador de passagem de renderização (um valor da enumeração D3D12_RENDER_PASS_FLAGS ).

enum D3D12_RENDER_PASS_FLAGS
{
    D3D12_RENDER_PASS_FLAG_NONE = 0,
    D3D12_RENDER_PASS_FLAG_ALLOW_UAV_WRITES = 0x1,
    D3D12_RENDER_PASS_FLAG_SUSPENDING_PASS = 0x2,
    D3D12_RENDER_PASS_FLAG_RESUMING_PASS = 0x4
};

Gravações de UAV em um passe de renderização

Gravações de UAV (exibição de acesso não ordenada) são permitidas em uma passagem de renderização, mas você deve indicar especificamente que emitirá gravações UAV dentro da passagem de renderização especificando D3D12_RENDER_PASS_FLAG_ALLOW_UAV_WRITES, para que o driver de exibição possa recusar o bloco, se necessário.

Os acessos UAV devem seguir a restrição de leitura de gravação descrita acima (as gravações em um passe de renderização não são válidas para leitura até uma passagem de renderização subsequente). As barreiras UAV não são permitidas em uma passagem de renderização.

As associações UAV (por meio de tabelas raiz ou descritores raiz) são herdadas em passes de renderização e propagadas para fora dos passes de renderização.

Suspending-passes, and resuming-passes

Você pode indicar um passe de renderização inteiro como sendo um suspending-pass e/ou um resumindo-pass. Um par suspending-followed-by-a-resuming deve ter exibições/sinalizadores de acesso idênticos entre as passagens e pode não ter nenhuma operação de GPU intervindo (por exemplo, sorteios, expedições, descartes, limpezas, cópias, mapeamentos de bloco de atualização, write-buffer-immediates, consultas, resoluções de consulta) entre o passe de renderização de suspensão e a passagem de renderização de retomada.

O caso de uso pretendido é a renderização de vários threads, em que, digamos, quatro listas de comandos (cada uma com suas próprias passagens de renderização) podem atingir os mesmos destinos de renderização. Quando as passagens de renderização são suspensas/retomadas entre listas de comandos, as listas de comandos devem ser executadas na mesma chamada para ID3D12CommandQueue::ExecuteCommandLists.

Uma passagem de renderização pode ser retomada e suspensa. No exemplo multi-threaded fornecido, as listas de comandos 2 e 3 seriam retomadas de 1 e 2, respectivamente. E, ao mesmo tempo, 2 e 3 estariam suspendendo para 3 e 4, respectivamente.

Consulta para suporte a recursos de renderização passa

Você pode chamar ID3D12Device::CheckFeatureSupport para consultar até que ponto um driver de dispositivo e/ou o hardware dão suporte eficiente à renderização.

D3D12_RENDER_PASS_TIER get_render_passes_tier(::ID3D12Device * pIDevice)
{
    D3D12_FEATURE_DATA_D3D12_OPTIONS5 featureSupport{};
    winrt::check_hresult(
        pIDevice->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS5, &featureSupport, sizeof(featureSupport))
    );
    return featureSupport.RenderPassesTier;
}
...
    D3D12_RENDER_PASS_TIER renderPassesTier{ get_render_passes_tier(pIDevice) };

Devido à lógica de mapeamento do runtime, a renderização passa sempre a função. Mas, dependendo do suporte a recursos, eles nem sempre fornecerão um benefício. Você pode usar código semelhante ao exemplo de código acima para determinar se/quando vale a pena emitir comandos conforme a renderização passa e quando definitivamente não é um benefício (ou seja, quando o runtime está apenas mapeando para a superfície de API existente). Executar esse marcar é particularmente importante se você estiver usando D3D11On12).

Para obter uma descrição das três camadas de suporte, consulte a enumeração D3D12_RENDER_PASS_TIER .