Interoperabilidade do Direct3D 12

D3D12 pode ser usado para gravar aplicativos componentes.

Visão geral da interoperabilidade

O D3D12 pode ser muito poderoso e permitir que os aplicativos escrevam código gráfico com eficiência semelhante ao console, mas nem todo aplicativo precisa reinventar a roda e gravar a totalidade do mecanismo de renderização do zero. Em alguns casos, outro componente ou biblioteca já o fez melhor ou, em outros casos, o desempenho de uma parte do código não é tão crítico quanto sua correção e legibilidade.

Esta seção aborda as seguintes técnicas de interoperabilidade:

  • D3D12 e D3D12, no mesmo dispositivo
  • D3D12 e D3D12, em dispositivos diferentes
  • D3D12 e qualquer combinação de D3D11, D3D10 ou D2D, no mesmo dispositivo
  • D3D12 e qualquer combinação de D3D11, D3D10 ou D2D, em dispositivos diferentes
  • D3D12 e GDI, ou D3D12 e D3D11 e GDI

Motivos para usar a interoperabilidade

Há vários motivos pelos quais um aplicativo deseja interoperabilidade D3D12 com outras APIs. Alguns exemplos:

  • Portabilidade incremental: desejando portar um aplicativo inteiro de D3D10 ou D3D11 para D3D12, ao mesmo tempo em que o tem funcional em estágios intermediários do processo de portabilidade (para habilitar o teste e a depuração).
  • Código de caixa preta: desejando deixar uma parte específica de um aplicativo como está ao portar o restante do código. Por exemplo, pode não haver necessidade de portar elementos de interface do usuário de um jogo.
  • Componentes imutáveis: precisa usar componentes que não pertencem ao aplicativo, que não são gravados no D3D12 de destino.
  • Um novo componente: não querer portar todo o aplicativo, mas querer usar um novo componente escrito usando D3D12.

Há quatro técnicas de main para interoperabilidade em D3D12:

  • Um aplicativo pode optar por fornecer uma lista de comandos aberta a um componente, que registra alguns comandos de renderização adicionais em um destino de renderização já associado. Isso equivale a fornecer um contexto de dispositivo preparado para outro componente em D3D11 e é ótimo para coisas como adicionar interface do usuário/texto a um buffer back já associado.
  • Um aplicativo pode optar por fornecer uma fila de comandos a um componente, juntamente com um recurso de destino desejado. Isso é equivalente ao uso de APIs ClearState ou DeviceContextState em D3D11 para fornecer um contexto de dispositivo limpo a outro componente. É assim que os componentes como o D2D operam.
  • Um componente pode optar por um modelo em que produz uma lista de comandos, potencialmente em paralelo, que o aplicativo é responsável pelo envio posteriormente. Pelo menos um recurso deve ser fornecido entre os limites do componente. Essa mesma técnica está disponível em D3D11 usando contextos adiados, embora o desempenho em D3D12 seja mais desejável.
  • Cada componente tem suas próprias filas e/ou dispositivos, e o aplicativo e os componentes precisam compartilhar recursos e informações de sincronização entre os limites do componente. Isso é semelhante ao herdado ISurfaceQueuee ao IDXGIKeyedMutex mais moderno.

As diferenças entre esses cenários são exatamente o que é compartilhado entre os limites do componente. Presume-se que o dispositivo seja compartilhado, mas como ele é basicamente sem estado, ele não é realmente relevante. Os objetos de chave são a lista de comandos, a fila de comandos, os objetos de sincronização e os recursos. Cada um deles tem suas próprias complicações ao compartilhá-las.

Compartilhando uma lista de comandos

O método mais simples de interoperabilidade requer o compartilhamento apenas de uma lista de comandos com uma parte do mecanismo. Depois que as operações de renderização forem concluídas, a propriedade da lista de comandos voltará para o chamador. A propriedade da lista de comandos pode ser rastreada por meio da pilha. Como as listas de comandos são de thread único, não há como um aplicativo fazer algo exclusivo ou inovador usando essa técnica.

Compartilhando uma fila de comandos

Provavelmente a técnica mais comum para vários componentes que compartilham um dispositivo no mesmo processo.

Quando a fila de comandos é a unidade de compartilhamento, precisa haver uma chamada para o componente para que ele saiba que todas as listas de comandos pendentes precisam ser enviadas para a fila de comandos imediatamente (e todas as filas de comando internas precisam ser sincronizadas). Isso é equivalente à API de Liberação D3D11 e é a única maneira de o aplicativo enviar suas próprias listas de comandos ou sincronizar primitivos.

Compartilhando primitivos de sincronização

O padrão esperado para um componente que opera em seus próprios dispositivos e/ou filas de comando será aceitar um identificador ID3D12Fence ou compartilhado e o par UINT64 ao iniciar seu trabalho, que ele aguardará e, em seguida, uma segunda ID3D12Fence ou identificador compartilhado e par UINT64 que ele sinalizará quando todo o trabalho for concluído. Esse padrão corresponde à implementação atual de IDXGIKeyedMutex e do design de sincronização de modelo de inversão DWM/DXGI.

Compartilhando recursos

De longe, a parte mais complicada da escrita de um aplicativo D3D12 que aproveita vários componentes é como lidar com os recursos que são compartilhados entre os limites do componente. Isso ocorre principalmente devido ao conceito de estados de recurso. Embora alguns aspectos do design do estado do recurso se destinem a lidar com a sincronização entre listas de comandos, outros têm impacto entre listas de comandos, afetando o layout do recurso e conjuntos válidos de operações ou características de desempenho do acesso aos dados do recurso.

Há dois padrões de lidar com essa complicação, que envolvem essencialmente um contrato entre componentes.

  • O contrato pode ser definido pelo desenvolvedor do componente e documentado. Isso pode ser tão simples quanto "o recurso deve estar no estado padrão quando o trabalho for iniciado e será colocado novamente no estado padrão quando o trabalho for feito" ou pode ter regras mais complicadas para permitir coisas como compartilhar um buffer de profundidade sem forçar resoluções de profundidade intermediárias.
  • O contrato pode ser definido pelo aplicativo em runtime, no momento em que o recurso é compartilhado entre os limites do componente. Ele consiste nas mesmas duas informações: o estado em que o recurso estará quando o componente começar a usá-lo e o estado em que o componente deverá deixá-lo quando for concluído.

Escolhendo um modelo de interoperabilidade

Para a maioria dos aplicativos D3D12, compartilhar uma fila de comandos provavelmente é o modelo ideal. Ele permite a propriedade completa da criação e envio do trabalho, sem que a sobrecarga de memória adicional tenha filas redundantes e sem o impacto perf de lidar com os primitivos de sincronização de GPU.

O compartilhamento de primitivos de sincronização é necessário quando os componentes precisam lidar com propriedades de fila diferentes, como tipo ou prioridade, ou quando o compartilhamento precisa abranger os limites do processo.

O compartilhamento ou a produção de listas de comandos não são amplamente usados externamente por componentes de terceiros, mas podem ser amplamente usados em componentes internos de um mecanismo de jogo.

APIs de interoperabilidade

O tópico Direct3D 11 on 12 orienta você pelo uso de grande parte da superfície da API relacionada aos tipos de interoperação descritos neste tópico.

Consulte também o método ID3D12Device::CreateSharedHandle , que você pode usar para compartilhar superfícies entre APIs gráficas do Windows.