DirectX11, jak zarządzać i aktualizować wiele stałych buforów modułu cieniującego?

13

W porządku, trudno mi zrozumieć, jak stałe bufory są powiązane z etapem potoku i aktualizowane. Rozumiem, że DirectX11 może mieć do 15 buforów stałych shaderów na stopień, a każdy bufor może pomieścić do 4096 stałych. Nie rozumiem jednak, czy ID3D11Buffer COM używany do interakcji ze stałymi buforami jest tylko mechanizmem (lub uchwytem) używanym do wypełniania tych miejsc buforowych, czy też obiekt faktycznie odwołuje się do konkretnej instancji danych bufora, które są wypychane w tę iz powrotem pomiędzy GPU a CPU.

Wydaje mi się, że moje zamieszanie na ten temat jest przyczyną problemu z używaniem dwóch różnych stałych buforów.

Oto przykładowy kod modułu cieniującego.

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

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

Sposób, w jaki mój kod jest zorganizowany, kamera będzie aktualizować odpowiednie dane dla poszczególnych klatek, a GameObjects będzie aktualizować własne dane dla poszczególnych obiektów. Obie klasy mają własny ID3D11Buffer, który służy do tego celu (przy użyciu architektury koncentratora, więc jedna klasa GameObject obsłuży renderowanie wszystkich instancji GameObjects na świecie).

Problem polega na tym, że mogę aktualizować tylko jedną naraz, w zależności od miejsca i zakładam, że kolejność aktualizacji jednego bufora zostanie zapełniona, a drugiego zero.

To jest zasadniczo mój kod. Obie klasy używają identycznej logiki aktualizacji.

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

Moje główne pytania to:

  • Czy muszę ustawić lub powiązać ShaderBuffer, aby zaktualizować go za pomocą wywołania UpdateSubresource? (To znaczy manipuluj nim tylko wtedy, gdy jest w potoku) Czy może jest to kropla danych, która zostanie wysłana za pomocą wywołania VSSetConstantBuffer? (Oznacza to, że kolejność wiązania i aktualizacji danych nie ma znaczenia, mogę zaktualizować je w potoku lub w jakiś sposób na procesorze)
  • Czy podczas ustawiania lub wiązania bufora muszę odwoływać się do gniazda 0, aby zaktualizować bufor PerFrame, i do gniazda 1, aby zaktualizować bufor PerObject? Czy jakieś zamieszanie związane z tym wywołaniem w moim kodzie może spowodować zastąpienie wszystkich buforów?
  • Skąd D3D11 wie, który bufor chcę zaktualizować lub zmapować? Czy to wiadomo z zastosowanego ID3D11Buffer COM?

Edytować -

Zmieniono tagi rejestru stałego bufora w powyższym przykładzie. Użycie (cb #) zamiast (b #) wpłynęło na poprawność aktualizacji buforów z jakiegoś powodu. Nie jestem pewien, gdzie wybrałem oryginalną składnię lub czy w ogóle jest ona poprawna, ale wydaje się, że był to mój główny problem.

KlashnikovKid
źródło

Odpowiedzi:

18

ID3D11Buffer odwołuje się do rzeczywistego fragmentu pamięci, który przechowuje dane, niezależnie od tego, czy jest to bufor wierzchołków, bufor stały lub cokolwiek innego.

Bufory stałe działają tak samo, jak bufory wierzchołków i inne rodzaje buforów. Mianowicie, dane w nich nie są dostępne dla GPU, dopóki faktycznie nie renderuje ramki, więc bufor musi pozostać ważny, dopóki GPU z nim nie skończy. Powinieneś podwójnie buforować każdy stały bufor, abyś miał jedną kopię do aktualizacji dla następnej ramki i jedną kopię do GPU do odczytania podczas renderowania bieżącej ramki. Jest to podobne do sposobu wykonywania dynamicznych buforów wierzchołków dla układu cząstek lub tym podobnych.

register(cb0), register(cb1)Ustawienia w odpowiadają HLSL ze szczelinami w VSSetConstantBuffers. Gdy aktualizujesz stałe dla ramki, robisz, VSSetConstantBuffers(0, 1, &pBuffer)aby ustawić CB0, a kiedy aktualizujesz stałe dla obiektu, robisz, VSSetConstantBuffers(1, 1, &pBuffer)aby ustawić CB1. Każde połączenie aktualizuje tylko bufory, o których mowa w parametrach start / count, i nie dotyka pozostałych.

Nie trzeba wiązać bufora, aby zaktualizować go za pomocą UpdateSubresource. W rzeczywistości nie należy go wiązać podczas aktualizacji, ponieważ może to zmusić sterownik do wykonania dodatkowych kopii pamięci wewnętrznie (patrz strona MSDN dla UpdateSubresource, zwłaszcza uwagi na temat sporów o stronę w dół).

Nie jestem pewien, co masz na myśli przez „Skąd D3D11 wie, który bufor chcę zaktualizować lub zmapować?” Aktualizuje lub mapuje ten, którego wskaźnik został przekazany.

Nathan Reed
źródło
3

Wydaje się, że istnieje wiele zamieszania na temat konieczności ponownego wiązania stałych buforów po ich aktualizacji. Ucząc się o tym sam, widziałem wiele tematów i dyskusji z przeciwnymi opiniami na ten temat. Mianowicie najlepsza odpowiedź tutaj, zalecająca dzwonienie XXSetConstantBufferspo aktualizacji przez UpdateSubresourcelub Map/Unmap.

Ponadto niektóre próbki D3D MSDN i dokumentacja wydają się używać tego wzorca, wiązania (wywoływania XXSetConstantBuffers) na ramkę lub nawet na podstawie obiektu rysowanego, nawet jeśli aktualizują tylko istniejący bufor, a nie zmieniają konkretnego gniazda z zupełnie innym buforem .

Myślę, że najgorsze nieporozumienie polega na tym, że XXSetConstantBufferstak naprawdę „wysyła dane, które wcześniej zaktualizowałeś, do GPU lub powiadamia o aktualizacji, aby przyjmować nowe wartości - co wydaje się całkowicie błędne.

Rzeczywiście, podczas używania UpdateSubresourcelub Map/Unmap, dokumentacja stwierdza, że ​​GPU może wykonać wewnętrznie wiele kopii, jeśli nadal potrzebuje starych danych, ale nie jest to problemem dla użytkownika interfejsu API, jeśli chodzi o aktualizację już powiązanego bufora. Dlatego potrzeba jawnego niezwiązania wydaje się zbędna.

Podczas moich eksperymentów doszedłem do wniosku, że ponowne XXSetConstantBuffersaktualizowanie buforów po aktualizacji nie jest konieczne , chyba że nie są one już powiązane! Tak długo, jak korzystasz z tych samych buforów (dzielonych między modułami cieniującymi, równymi etapami potoku), które są kiedyś powiązane (na przykład w fazie rozruchu), nie musisz ich ponownie wiązać - po prostu je zaktualizuj.

Niektóre kody, które lepiej prezentują naturę moich eksperymentów:

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

Oto kilka tematów z Internetu (fora gamedev), które wydają się przyjmować i polecać to podejście: http://www.gamedev.net/topic/649410-set-constant-buffers-every-frame/?view=findpost&p=5105032 i http://www.gamedev.net/topic/647203-updating-constant-buffers/#entry5090000

Podsumowując, naprawdę wydaje się, że wiązanie nie jest potrzebne, chyba że zmienisz bufor całkowicie, ale dopóki udostępnisz bufory, a ich układ między modułami cieniującymi (zalecana praktyka) należy wykonać w następujących przypadkach:

  • Przy uruchomieniu - początkowe wiązanie - na przykład po utworzeniu bufora.
  • Jeśli potrzebujesz / zaprojektowałeś użycie więcej niż jednego bufora związanego z określonym gniazdem jednego lub więcej etapów.
  • Po wyczyszczeniu stanu urządzeniaContext (podczas zmiany rozmiaru buforów / okien)
Rocky Raccoon
źródło