DirectX11, ¿cómo administro y actualizo múltiples buffers constantes de sombreado?

13

Muy bien, me está costando comprender cómo los buffers constantes están vinculados a una etapa de canalización y se actualizan. Entiendo que DirectX11 puede tener hasta 15 búferes de constante de sombreador por etapa y cada búfer puede contener hasta 4096 constantes. Sin embargo, no entiendo si el ID3D11Buffer COM utilizado para interactuar con los búferes constantes es solo un mecanismo (o identificador) utilizado para llenar estas ranuras de búfer o si el objeto realmente hace referencia a una instancia particular de datos de búfer que se empuja hacia adelante y hacia atrás entre la GPU y la CPU.

Creo que mi confusión sobre el tema es la causa de un problema que estoy teniendo usando dos buffers constantes diferentes.

Aquí hay un ejemplo de código de sombreador.

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

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

La forma en que está organizado mi código, la cámara se encargará de actualizar los datos relevantes por fotograma y GameObjects actualizará sus propios datos por objeto. Ambas clases tienen su propio ID3D11Buffer que se usa para hacer esto (usando una arquitectura de concentrador, por lo que una clase de GameObject manejará la representación de todos los GameObjects instanciados en el mundo).

El problema es que solo puedo actualizar uno a la vez, dependiendo de la ranura y supongo que el orden de actualización se llena un búfer mientras que el otro se pone a cero.

Este es esencialmente mi código. Ambas clases usan una lógica de actualización 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);

Mis preguntas principales son:

  • ¿Debo configurar o vincular el ShaderBuffer para actualizarlo con la llamada UpdateSubresource? (Lo que significa manipularlo solo cuando está en la tubería) ¿O es un conjunto de datos que se enviarán con la llamada VSSetConstantBuffer? (Es decir, el orden de enlace y actualización de datos no importa, puedo actualizarlo en la tubería o de alguna manera en la CPU)
  • Al configurar o vincular el búfer, ¿necesito hacer referencia a la ranura 0 para actualizar el búfer PerFrame y la ranura 1 para actualizar el búfer PerObject? ¿Podría algún tipo de confusión con esta llamada en mi código hacer que se sobrescriban todos los búferes?
  • ¿Cómo sabe D3D11 qué búfer quiero actualizar o asignar? ¿Sabe por el ID3D11Buffer COM utilizado?

Editar -

Cambió las etiquetas de registro de búfer constante en el ejemplo anterior. El uso de (cb #) en lugar de (b #) estaba afectando a los buffers de actualizarse correctamente por alguna razón. No estoy seguro de dónde recogí la sintaxis original o si es válida, pero parece que fue mi principal problema.

KlashnikovKid
fuente

Respuestas:

18

El ID3D11Buffer hace referencia a un fragmento de memoria real que contiene sus datos, ya sea un búfer de vértices, un búfer constante o lo que sea.

Los búferes constantes funcionan de la misma manera que los búferes de vértices y otros tipos de búferes. Es decir, la GPU no accede a los datos que contiene hasta que realmente representa el marco, por lo que el búfer debe permanecer válido hasta que la GPU termine con él. Debería hacer doble búfer en cada búfer constante, de modo que tenga una copia para actualizar para el siguiente fotograma y una copia para que la GPU lea mientras renderiza el fotograma actual. Esto es similar a cómo haría amortiguadores de vértices dinámicos para un sistema de partículas o similar.

El register(cb0), register(cb1)ajustes en el HLSL corresponden con las ranuras de VSSetConstantBuffers. Cuando actualiza las constantes por cuadro que haría VSSetConstantBuffers(0, 1, &pBuffer)para establecer CB0 y cuando actualiza las por objeto que haría VSSetConstantBuffers(1, 1, &pBuffer)para establecer CB1. Cada llamada actualiza solo las memorias intermedias a las que se refieren los parámetros de inicio / conteo, y no toca las otras.

No necesita vincular el búfer para actualizarlo con UpdateSubresource. De hecho, no debe vincularse cuando lo actualiza, o esto puede obligar al controlador a realizar copias de memoria adicionales internamente (consulte la página de MSDN para UpdateSubresource, en particular los comentarios sobre la contención sobre una página abajo).

No estoy seguro de lo que quiere decir con "¿Cómo sabe D3D11 qué búfer quiero actualizar o asignar?" Actualiza o asigna el cuyo puntero pasó.

Nathan Reed
fuente
3

Parece haber mucha confusión sobre el tema de la necesidad de volver a vincular buffers constantes después de actualizarlos. Mientras estoy aprendiendo sobre esto, he visto muchos temas y discusiones con opiniones opuestas sobre esto. Es decir, la mejor respuesta aquí, recomendando llamar XXSetConstantBuffersdespués de actualizar a través de UpdateSubresourceo Map/Unmap.

Además, algunas muestras y documentación de D3D MSDN parecen usar este patrón, vinculando (llamando XXSetConstantBuffers) por cuadro o incluso por objeto dibujado, a pesar de que solo actualizan un búfer existente y no cambian una ranura específica con un búfer completamente diferente .

Creo que la peor idea errónea es que en XXSetConstantBuffersrealidad "envía los datos que actualizó previamente a la GPU o lo notifica de la actualización, para que tome los nuevos valores, lo que parece estar completamente equivocado.

De hecho, cuando se usa UpdateSubresourceoMap/Unmap , la documentación establece que internamente la GPU puede realizar múltiples copias si aún necesita los datos antiguos, pero esto no es una preocupación para el usuario de la API cuando se trata de actualizar un búfer ya vinculado. Por lo tanto, la necesidad de desligarse explícitamente parece superflua.

Durante mi experimentación, llegué a la conclusión de que no es necesario volver a vincular los buffers a través de XXSetConstantBuffers después de actualizarlos, ¡a menos que ya no estén vinculados! Siempre que use los mismos búferes (compartidos entre sombreadores, etapas de canalización uniforme) que alguna vez están vinculados (en la fase de inicio, por ejemplo), no necesita volver a vincularlos, solo actualícelos.

Algún código para mostrar mejor la naturaleza de mis 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);
    }
}

Estos son algunos temas de Internet (foros de gamedev) que parecen adoptar y recomendar este enfoque: http://www.gamedev.net/topic/649410-set-constant-buffers-every-frame/?view=findpost&p=5105032 y http://www.gamedev.net/topic/647203-updating-constant-buffers/#entry5090000

Para concluir, realmente parece que el enlace no es necesario a menos que cambie el búfer por completo, pero siempre que comparta búferes y su diseño entre sombreadores (práctica recomendada), el enlace debe hacerse en estos casos:

  • Al inicio, enlace inicial, después de crear el búfer, por ejemplo.
  • Si necesita / ha diseñado utilizar más de un búfer vinculado a una ranura específica de una o más etapas.
  • Después de borrar el estado del dispositivo Contexto (al cambiar el tamaño de las memorias intermedias / ventanas)
Mapache rocoso
fuente