DirectX11,如何管理和更新多个着色器常量缓冲区?


13

好吧,我很难理解常量缓冲区如何绑定到流水线阶段并进行更新。我知道DirectX11每阶段最多可以有15个着色器常量缓冲区,每个缓冲区最多可以容纳4096个常量。但是,我不明白用于与常量缓冲区交互的ID3D11Buffer COM是否只是用于填充这些缓冲区插槽的机制(或句柄),还是该对象是否实际引用了来回推送的缓冲区数据的特定实例?在GPU和CPU之间。

我认为我对该主题的困惑是导致我使用两个不同的常量缓冲区的问题的原因。

这是一些示例着色器代码。

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

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

我的代码的组织方式,相机将处理更新相关的每帧数据,而GameObjects将更新其自己的每对象数据。这两个类都有自己的ID3D11Buffer来执行此操作(使用集线器体系结构,因此一个GameObject类将处理世界上所有实例GameObject的呈现)。

问题是我一次只能更新一个,具体取决于插槽,并且我假设一个缓冲区被填充,而另一个缓冲区被填充为零的更新顺序。

这本质上是我的代码。这两个类使用相同的更新逻辑。

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);

我的主要问题是-

  • 我是否需要设置或绑定ShaderBuffer以便通过UpdateSubresource调用对其进行更新?(意味着仅当它在管道中时才对其进行操作)还是它是将通过VSSetConstantBuffer调用发送的数据块?(意味着绑定和更新数据的顺序无关紧要,我可以在管道中或以某种方式在cpu上对其进行更新)
  • 在设置或绑定缓冲区时,是否需要引用插槽0来更新PerFrame缓冲区,并引用插槽1来更新PerObject缓冲区?我的代码中对此调用的某种混淆是否会导致所有缓冲区被覆盖?
  • D3D11如何知道我要更新或映射哪个缓冲区?从使用的ID3D11Buffer COM知道吗?

编辑-

在上面的示例中更改了常量缓冲寄存器标签。由于某些原因,使用(cb#)而不是(b#)正在影响缓冲区正确更新。不知道我在哪里选择了原始语法,或者它是否完全有效,但这似乎是我的主要问题。

Answers:


18

ID3D11Buffer引用一个实际的内存块来保存您的数据,无论它是顶点缓冲区,常量缓冲区还是其他。

常量缓冲区的工作方式与顶点缓冲区和其他类型的缓冲区相同。也就是说,GPU中的数据直到它实际渲染帧后才被访问,因此缓冲区必须保持有效,直到GPU完成处理为止。您应该对每个常量缓冲区加倍缓冲,这样您就可以为下一帧更新一个副本,并为GPU在渲染当前帧时读取提供一个副本。这与为粒子系统或类似系统执行动态顶点缓冲的方式类似。

register(cb0)register(cb1)与在VSSetConstantBuffers的插槽HLSL对应设置。更新每帧常量时,您将VSSetConstantBuffers(0, 1, &pBuffer)设置CB0,更新每对象常量时,您VSSetConstantBuffers(1, 1, &pBuffer)将设置CB1。每个调用仅更新由start / count参数引用的缓冲区,而不涉及其他缓冲区。

您无需绑定缓冲区即可使用UpdateSubresource对其进行更新。实际上,更新时应该绑定它,否则可能会迫使驱动程序在内部进行额外的内存复制(请参见MSDN页面中的UpdateSubresource,尤其是有关页面向下争用的说明)。

我不确定“ D3D11如何知道我要更新或映射哪个缓冲区?”的意思。它更新或映射您传入其指针的指针。


3

关于在更新常量缓冲区后需要重新绑定常量缓冲区的话题似乎有很多困惑。当我自己了解这一点时,我看到了很多与此相反的话题和讨论。即是最佳答案,建议XXSetConstantBuffers您通过UpdateSubresource或更新后致电Map/Unmap

此外,某些D3D MSDN样本和文档似乎都使用这种模式,即使每帧或每个绘制对象都进行绑定(调用XXSetConstantBuffers),即使它们仅更新现有缓冲区,而不会使用完全不同的缓冲区更改特定的插槽。

我认为最糟糕的误解是,XXSetConstantBuffers实际上是“将先前更新的数据发送到GPU或将更新通知它,以便它采用新值-这似乎是完全错误的。

确实,当使用UpdateSubresource或时Map/Unmap,文档指出如果GPU仍需要旧数据,则可以在内部创建多个副本,但是对于API用户而言,在更新已绑定的缓冲区时不必担心。因此,显式解除绑定的需求似乎是多余的。

在我的实验过程中,我得出的结论是XXSetConstantBuffers,除非更新了缓冲区,否则没有必要重新绑定缓冲区!只要使用曾经绑定(例如在启动阶段)的相同缓冲区(在着色器,均匀流水线阶段之间共享),就无需重新绑定它们-只需对其进行更新即可。

一些代码可以更好地展示我的实验性质:

// 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);
    }
}

以下是一些来自Internet(gamedev论坛)的主题,它们似乎采用并推荐了这种方法:http ://www.gamedev.net/topic/649410-set-constant-buffers-every-frame/?view=findpost&p=5105032 和 http://www.gamedev.net/topic/647203-updating-constant-buffers/#entry5090000

可以肯定的是,除非完全更改缓冲区,否则似乎不需要绑定,但是只要共享缓冲区,并且在以下情况下在着色器之间共享它们的布局(推荐做法),就应进行绑定:

  • 在启动时-初始绑定-例如在创建缓冲区之后。
  • 如果需要/已设计为使用一个以上绑定到一个或多个阶段的特定插槽的缓冲区。
  • 清除deviceContext的状态后(调整缓冲区/窗口的大小时)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.