DirectX11, как мне управлять и обновлять несколько постоянных буферов шейдеров?

13

Хорошо, мне трудно понять, как постоянные буферы связаны со стадией конвейера и обновляются. Я понимаю, что DirectX11 может иметь до 15 буферных констант на каждую стадию, и каждый буфер может содержать до 4096 констант. Однако я не понимаю, является ли ID3D11Buffer COM, используемый для взаимодействия с постоянными буферами, просто механизмом (или дескриптором), используемым для заполнения этих слотов буфера, или если объект на самом деле ссылается на конкретный экземпляр данных буфера, который перемещается взад и вперед между графическим процессором и процессором.

Я думаю, что моя путаница в теме - причина проблемы, которую я использую, используя два разных постоянных буфера.

Вот пример кода шейдера.

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? (То есть порядок связывания и обновления данных не имеет значения, я могу обновить его в конвейере или как-то на процессоре)
  • При установке или привязке буфера нужно ли ссылаться на слот 0 для обновления буфера PerFrame и слот 1 для обновления буфера PerObject? Может ли какая-то путаница с этим вызовом в моем коде вызвать перезапись всех буферов?
  • Как D3D11 узнает, какой буфер я хочу обновить или отобразить? Знает ли он из используемого ID3D11Buffer COM?

Редактировать -

Изменены теги константного буфера в приведенном выше примере. Использование (cb #) вместо (b #) по какой-то причине сказалось на корректном обновлении буферов. Не уверен, где я взял оригинальный синтаксис или он действителен вообще, но, похоже, это была моя главная проблема.

KlashnikovKid
источник

Ответы:

18

ID3D11Buffer ссылается на фактический кусок памяти, который содержит ваши данные, будь то буфер вершин, постоянный буфер или что-то еще.

Постоянные буферы работают так же, как буферы вершин и другие виды буферов. А именно, данные в них не доступны для графического процессора до тех пор, пока он фактически не отобразит кадр, поэтому буфер должен оставаться действительным, пока графический процессор не завершит работу с ним. Вы должны дважды буферизовать каждый постоянный буфер, чтобы у вас была одна копия для обновления для следующего кадра и одна копия для чтения графическим процессором при рендеринге текущего кадра. Это похоже на то, как вы будете делать динамические буферы вершин для системы частиц или тому подобное.

register(cb0), register(cb1)Настройки в HLSL соответствуют с пазами в VSSetConstantBuffers. Когда вы обновляете константы для каждого кадра, вы 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самом деле «отправляет ранее обновленные данные в графический процессор или уведомляет его об обновлении, чтобы он принимал новые значения - что, по-видимому, совершенно неверно.

Действительно, при использовании UpdateSubresource или Map/Unmapдокументация утверждает, что внутреннее множество копий может быть сделано графическим процессором, если ему все еще нужны старые данные, но это не беспокоит пользователя 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);
    }
}

Вот некоторые темы из Интернета (форумы 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 (при изменении размера буферов / окон)
Рокки Енот
источник