DirectX11, wie verwalte und aktualisiere ich mehrere Shader-Konstantenpuffer?

13

In Ordnung, es fällt mir schwer zu verstehen, wie konstante Puffer an eine Pipeline-Phase gebunden und aktualisiert werden. Ich verstehe, dass DirectX11 bis zu 15 Puffer mit Shader-Konstanten pro Stufe haben kann und jeder Puffer bis zu 4096 Konstanten enthalten kann. Ich verstehe jedoch nicht, ob die ID3D11-Puffer-COM, die zur Interaktion mit den konstanten Puffern verwendet wird, nur ein Mechanismus (oder ein Handle) ist, der zum Füllen dieser Pufferschlitze verwendet wird, oder ob das Objekt tatsächlich auf eine bestimmte Instanz von Pufferdaten verweist, die hin und her verschoben werden zwischen der GPU und der CPU.

Ich denke, meine Verwirrung in Bezug auf das Thema ist die Ursache für ein Problem, bei dem ich zwei verschiedene konstante Puffer verwende.

Hier ist ein Beispiel für Shader-Code.

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

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

Wie mein Code organisiert ist, aktualisiert die Kamera die relevanten Pro-Frame-Daten und GameObjects aktualisiert ihre eigenen Pro-Objekt-Daten. Beide Klassen haben einen eigenen ID3D11-Puffer, der dazu verwendet wird. (Unter Verwendung einer Hub-Architektur übernimmt eine GameObject-Klasse das Rendern aller instanzierten GameObjects in der Welt.)

Das Problem ist, dass ich je nach Steckplatz immer nur eine Aktualisierung durchführen kann, und ich nehme an, dass in der Aktualisierungsreihenfolge ein Puffer gefüllt wird, während der andere Nullen aufweist.

Dies ist im Wesentlichen mein Code. Beide Klassen verwenden eine identische Aktualisierungslogik.

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

Meine Hauptfragen sind -

  • Muss ich den ShaderBuffer setzen oder binden, um ihn mit dem UpdateSubresource-Aufruf zu aktualisieren? (Das heißt, manipulieren Sie es nur, wenn es sich in der Pipeline befindet.) Oder handelt es sich um einen Datenblock, der mit dem VSSetConstantBuffer-Aufruf gesendet wird? (Das heißt, die Reihenfolge des Verbindens und Aktualisierens von Daten spielt keine Rolle, ich kann sie in der Pipeline oder irgendwie auf der CPU aktualisieren.)
  • Muss ich beim Festlegen oder Binden des Puffers auf Steckplatz 0 verweisen, um den PerFrame-Puffer zu aktualisieren, und auf Steckplatz 1, um den PerObject-Puffer zu aktualisieren? Könnte eine Verwechslung mit diesem Aufruf in meinem Code dazu führen, dass alle Puffer überschrieben werden?
  • Woher weiß D3D11, welchen Puffer ich aktualisieren oder zuordnen möchte? Kennt es den vom ID3D11Buffer verwendeten COM?

Bearbeiten -

Im obigen Beispiel wurden die Konstantenpufferregister-Tags geändert. Die Verwendung von (cb #) anstelle von (b #) hatte aus irgendeinem Grund Auswirkungen auf die Aktualisierung der Puffer. Ich bin mir nicht sicher, wo ich die ursprüngliche Syntax gefunden habe oder ob sie überhaupt gültig ist, aber es scheint mein Hauptproblem zu sein.

KlashnikovKid
quelle

Antworten:

18

Der ID3D11-Puffer verweist auf einen tatsächlichen Speicherbereich, in dem Ihre Daten gespeichert sind, unabhängig davon, ob es sich um einen Scheitelpunktpuffer, einen konstanten Puffer oder etwas anderes handelt.

Konstante Puffer funktionieren genauso wie Scheitelpunktpuffer und andere Arten von Puffern. Die GPU greift nämlich erst dann auf die darin enthaltenen Daten zu, wenn der Frame tatsächlich gerendert wird. Daher muss der Puffer gültig bleiben, bis die GPU damit fertig ist. Sie sollten jeden konstanten Puffer doppelt puffern, damit Sie eine Kopie für das nächste Bild und eine Kopie für die GPU zum Lesen haben, während Sie das aktuelle Bild rendern. Dies ähnelt dem Erstellen dynamischer Vertex-Puffer für ein Partikelsystem oder dergleichen.

Die register(cb0), register(cb1)Einstellungen in der HLSL korrespondieren mit den Schlitzen in VSSetConstantBuffers. Wenn Sie die Pro-Frame-Konstanten VSSetConstantBuffers(0, 1, &pBuffer)aktualisieren, müssen Sie CB0 festlegen, und wenn Sie die Pro-Objekt -Konstanten aktualisieren, müssen Sie VSSetConstantBuffers(1, 1, &pBuffer)CB1 festlegen. Bei jedem Aufruf werden nur die Puffer aktualisiert, auf die durch die Start- / Zählparameter verwiesen wird, und die anderen werden nicht berührt.

Sie müssen den Puffer nicht binden, um ihn mit UpdateSubresource zu aktualisieren. Tatsächlich sollte es nicht gebunden sein, wenn Sie es aktualisieren, oder dies kann den Treiber dazu zwingen, zusätzliche Speicherkopien intern zu erstellen (siehe die MSDN-Seite für UpdateSubresource, insbesondere die Anmerkungen zum Streit um eine Seite nach unten).

Ich bin nicht sicher, was Sie unter "Woher weiß D3D11, welchen Puffer ich aktualisieren oder zuordnen möchte?" Es aktualisiert oder ordnet denjenigen zu, dessen Zeiger Sie übergeben haben.

Nathan Reed
quelle
3

Es scheint viel Verwirrung um das Thema zu geben, konstante Puffer nach dem Aktualisieren neu binden zu müssen. Als ich selbst davon erfahre, habe ich viele Themen und Diskussionen mit gegensätzlichen Meinungen dazu gesehen. Nämlich die beste Antwort hier, empfehle XXSetConstantBuffersnach dem Update über UpdateSubresourceoder anzurufen Map/Unmap.

Einige D3D-MSDN-Beispiele und -Dokumentationen scheinen dieses Muster zu verwenden, das XXSetConstantBuffersfür jeden Frame oder sogar für jedes gezeichnete Objekt bindet (aufruft ), obwohl sie nur einen vorhandenen Puffer aktualisieren und einen bestimmten Steckplatz nicht durch einen vollständig anderen Puffer ändern .

Ich denke, das schlimmste Missverständnis ist, dass XXSetConstantBufferstatsächlich "die Daten, die Sie zuvor aktualisiert haben, an die GPU gesendet oder über die Aktualisierung benachrichtigt werden, so dass die neuen Werte übernommen werden - was völlig falsch zu sein scheint.

Bei Verwendung von UpdateSubresourceoder Map/Unmapwird in der Dokumentation zwar angegeben, dass von der GPU möglicherweise mehrere interne Kopien erstellt werden, wenn die alten Daten weiterhin benötigt werden. Dies ist jedoch für den Benutzer der API kein Problem, wenn ein bereits gebundener Puffer aktualisiert werden soll. Daher erscheint die Notwendigkeit einer expliziten Aufhebung der Bindung überflüssig.

Während meiner Experimente bin ich zu dem Schluss gekommen, dass es nicht notwendig ist, Puffer XXSetConstantBuffersnach dem Aktualisieren erneut zu binden , es sei denn, sie sind noch nicht gebunden! Solange Sie dieselben Puffer verwenden (die von Shadern und Eveny-Pipeline-Phasen gemeinsam genutzt werden), die einmal gebunden wurden (z. B. in der Startphase), müssen Sie sie nicht erneut binden - aktualisieren Sie sie einfach.

Ein paar Codes, um meine Experimente besser zu veranschaulichen:

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

Hier sind einige Themen aus dem Internet (Gamedev-Foren), die diesen Ansatz zu übernehmen und zu empfehlen scheinen: http://www.gamedev.net/topic/649410-set-constant-buffers-every-frame/?view=findpost&p=5105032 und http://www.gamedev.net/topic/647203-updating-constant-buffer/#entry5090000

Zusammenfassend scheint es wirklich so zu sein, dass das Binden nicht erforderlich ist, es sei denn, Sie ändern den Puffer vollständig, aber solange Sie Puffer und deren Layout zwischen Shadern teilen (empfohlene Vorgehensweise), sollte das Binden in den folgenden Fällen erfolgen:

  • Beim Start - beim ersten Binden - zum Beispiel nach dem Anlegen des Puffers.
  • Wenn Sie mehr als einen Puffer verwenden möchten, der an einen bestimmten Steckplatz einer oder mehrerer Stufen gebunden ist.
  • Nach dem Löschen des Gerätekontextstatus (beim Ändern der Größe von Puffern / Fenstern)
Felsiger Waschbär
quelle