DirectX11, como gerenciar e atualizar vários buffers constantes do shader?

13

Tudo bem, estou tendo dificuldades para entender como os buffers constantes são vinculados a um estágio de pipeline e atualizados. Entendo que o DirectX11 pode ter até 15 buffers de shader-constante por estágio e cada buffer pode conter até 4096 constantes. No entanto, não entendo se o COM ID3D11Buffer usado para interagir com os buffers constantes é apenas um mecanismo (ou identificador) usado para preencher esses slots de buffer ou se o objeto realmente faz referência a uma instância específica de dados de buffer que é empurrada para frente e para trás entre a GPU e a CPU.

Acho que minha confusão sobre o assunto é a causa de um problema que estou tendo usando dois buffers constantes diferentes.

Aqui está um exemplo de código de sombreador.

cbuffer PerFrame : register(b0) {
    float4x4 view;
};

cbuffer PerObject : register(b1) {
    float4x4 scale;
    float4x4 rotation;
    float4x4 translation;
};

Da maneira como meu código é organizado, a câmera manipula a atualização dos dados relevantes por quadro e o GameObjects atualiza seus próprios dados por objeto. Ambas as classes têm seu próprio ID3D11Buffer usado para fazer isso (usando uma arquitetura de hub, uma classe GameObject manipulará a renderização de todos os GameObjects instanciados no mundo).

O problema é que eu só consigo atualizar uma por vez, dependendo do slot, e presumo que a ordem de atualização de um buffer seja preenchida enquanto o outro será zerado.

Este é essencialmente o meu código. Ambas as classes usam lógica de atualização idêntica.

static PerObjectShaderBuffer _updatedBuffer; // PerFrameShaderBuffer if Camera class
_updatedBuffer.scale       = _rScale;
_updatedBuffer.rotation    = _rRotation;
_updatedBuffer.translation = _rTranslation;
pDeviceContext->UpdateSubresource(pShaderBuffer, 0 , 0, &_updatedBuffer, 0, 0);

pDeviceContext->VSSetShader(pVShader->GetShaderPtr(), 0, 0);
pDeviceContext->PSSetShader(pPShader->GetShaderPtr(), 0, 0);
pDeviceContext->VSSetConstantBuffers(1, 1, &pShaderBuffer);
pDeviceContext->IASetVertexBuffers(0, 1, &pVertexBuffer, &vStride, &_offset );
pDeviceContext->IASetPrimitiveTopology(topologyType);
pDeviceContext->Draw(bufSize, 0);

Minhas principais perguntas são -

  • Preciso definir ou vincular o ShaderBuffer para atualizá-lo com a chamada UpdateSubresource? (O que significa manipulá-lo apenas quando estiver no pipeline) Ou é um blob de dados que serão enviados com a chamada VSSetConstantBuffer? (Isso significa que a ordem dos dados de ligação e atualização não importa, posso atualizá-los no pipeline ou de alguma forma na CPU)
  • Ao definir ou vincular o buffer, preciso fazer referência ao slot 0 para atualizar o buffer PerFrame e ao slot 1 para atualizar o buffer PerObject? Poderia algum tipo de confusão com essa chamada no meu código fazer com que todos os buffers fossem substituídos?
  • Como o D3D11 sabe qual buffer eu quero atualizar ou mapear? Ele conhece o ID3D11Buffer COM usado?

Editar -

Alteradas as tags de registro do buffer constante no exemplo acima. O uso de (cb #) em vez de (b #) estava afetando a atualização correta dos buffers por algum motivo. Não sei onde peguei a sintaxe original ou se é válida, mas parece ter sido o meu principal problema.

KlashnikovKid
fonte

Respostas:

18

O ID3D11Buffer faz referência a um pedaço real de memória que mantém seus dados, seja um buffer de vértice, buffer constante ou qualquer outra coisa.

Buffers constantes funcionam da mesma maneira que buffers de vértice e outros tipos de buffers. Nomeadamente, os dados neles não são acessados ​​pela GPU até que efetivamente renderize o quadro; portanto, o buffer deve permanecer válido até que a GPU termine com ele. Você deve colocar um buffer duplo em cada buffer constante, para ter uma cópia para atualizar para o próximo quadro e uma cópia para a GPU ler enquanto renderiza o quadro atual. Isso é semelhante a como você faria buffers de vértices dinâmicos para um sistema de partículas ou algo semelhante.

O register(cb0), register(cb1)configurações correspondem, no HLSL com as ranhuras em VSSetConstantBuffers. Quando você atualiza as constantes por quadro, você VSSetConstantBuffers(0, 1, &pBuffer)deve definir CB0 e quando atualiza as constantes por objeto, VSSetConstantBuffers(1, 1, &pBuffer)para definir CB1. Cada chamada atualiza apenas os buffers referidos pelos parâmetros de início / contagem e não toca nos outros.

Você não precisa vincular o buffer para atualizá-lo com o UpdateSubresource. De fato, ele não deve ser vinculado quando você o atualiza, ou isso pode forçar o driver a fazer cópias extras de memória internamente (consulte a página do MSDN para UpdateSubresource, principalmente as observações sobre a disputa de uma página para baixo).

Não sei ao certo o que você quer dizer com "Como o D3D11 sabe qual buffer eu quero atualizar ou mapear?" Ele atualiza ou mapeia aquele cujo ponteiro você inseriu.

Nathan Reed
fonte
3

Parece haver muita confusão em torno do tópico sobre a necessidade de reconectar os buffers constantes após atualizá-los. Enquanto estou aprendendo sobre isso, eu já vi muitos tópicos e discussões com opiniões opostas sobre isso. Nomeadamente a melhor resposta aqui, recomendando chamadas XXSetConstantBuffersapós a atualização via UpdateSubresourceou Map/Unmap.

Além disso, algumas amostras e documentação do D3D MSDN parecem usar esse padrão, vinculando (chamando XXSetConstantBuffers) por quadro ou mesmo por objeto desenhado, mesmo que atualizem apenas um buffer existente e não alterem um slot específico com um buffer completamente diferente .

Eu acho que o pior equívoco é que, XXSetConstantBuffersna verdade, "envia os dados que você atualizou anteriormente para a GPU ou notifica-os sobre a atualização, para que eles aceitem os novos valores - o que parece estar completamente errado.

De fato, ao usar UpdateSubresourceou Map/Unmap, a documentação afirma que várias cópias internamente podem ser feitas pela GPU se ainda precisar dos dados antigos, mas isso não é uma preocupação para o usuário da API quando se trata de atualizar um buffer já vinculado. Portanto, a necessidade de explicitamente desvincular parece supérflua.

Durante minha experiência, cheguei à conclusão de que não é necessário reconectar os buffers XXSetConstantBuffersapós a atualização deles, a menos que eles ainda não estejam vinculados! Contanto que você use os mesmos buffers (compartilhados entre sombreadores, estágios uniformes do pipeline) que são vinculados uma vez (na fase de inicialização, por exemplo), você não precisa ligá-los novamente - apenas atualize-os.

Algum código para mostrar melhor minha natureza de experimentos:

// Memory double of the buffer (static)
ConstBuffer* ShaderBase::CBuffer = (ConstBuffer*)_aligned_malloc(sizeof(ConstBuffer), 16);
// Hardware resource pointer (static)
ID3D11Buffer* ShaderBase::m_HwBuffer = nullptr;

void ShaderBase::Buffer_init()
{
     // Prepare buffer description etc.
     // Create one global buffer shared across shaders
     result = device->CreateBuffer(&cBufferDesc, NULL, &m_HwBuffer);
     BindConstBuffer();
}

...

void ShaderBase::BindConstBuffer()
{
     // Bind buffer to both VS and PS stages since it's a big global one
     deviceContext->VSSetConstantBuffers(0, 1, &m_HwBuffer);
     deviceContext->PSSetConstantBuffers(0, 1, &m_HwBuffer);
}

...
bool ShaderBase::UpdateConstBuffers()
{
    ...
    D3D11_MAPPED_SUBRESOURCE mappedResource;

    // Lock the constant buffer so it can be written to.
    deviceContext->Map(m_HwBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);

    // Get a pointer to the data in the constant buffer.
    ConstBuffer* dataPtr = (ConstBuffer*)mappedResource.pData;
    memcpy(dataPtr, CBuffer, sizeof(ConstBuffer));

    // Unlock the constant buffer.
    deviceContext->Unmap(m_HwBuffer, 0);
    return true;
}

// May be called multiple times per frame (multiple render passes)
void DrawObjects()
{
    // Simplified version
    for each Mesh _m to be drawn
    {
        // Some changes are per frame - but since we have only one global buffer to which we 
        // write with write-discard we need to set all of the values again when we update per-object
        ShaderBase::CBuffer->view = view;
        ShaderBase::CBuffer->projection = projection;
        ShaderBase::CBuffer->cameraPosition = m_Camera->GetPosition();

        ... 

        ShaderBase::CBuffer->lightDirection = m_Light->GetDirection();

        ShaderBase::CBuffer->lightView = lightView;
        ShaderBase::CBuffer->lightProjection = lightProjection;
        ShaderBase::CBuffer->world = worldTransform;

        // Only update! No rebind!
        if (ShaderBase::UpdateConstBuffers() == false)
            return false;

        _m->LoadIABuffers(); // Set the vertex and index buffers for the mesh
        deviceContext->DrawIndexed(_m->indexCount, 0, 0);
    }
}

Aqui estão alguns tópicos da Internet (fóruns gamedev) que parecem adotar e recomendar essa abordagem: http://www.gamedev.net/topic/649410-set-constant-buffers-every-frame/?view=findpost&p=5105032 e http://www.gamedev.net/topic/647203-updating-constant-buffers/#entry5090000

Para concluir, realmente parece que a ligação não é necessária, a menos que você altere completamente o buffer, mas desde que você compartilhe buffers e seu layout entre a ligação shaders (prática recomendada), deve ser feita nesses casos:

  • Na inicialização - ligação inicial - depois de criar o buffer, por exemplo.
  • Se você precisar / tiver projetado usar mais de um buffer vinculado a um slot específico de um ou mais estágios.
  • Após limpar o estado do deviceContext (ao redimensionar buffers / janelas)
Guaxinim rochoso
fonte