Quantcast
Channel: Intel Developer Zone Articles
Viewing all articles
Browse latest Browse all 1201

Einführung in die Ressourcenverknüpfung in Microsoft DirectX* 12

$
0
0

Download Introduction-to-Resource-Binding-in-Microsoft-DirectX12.pdf

Von Wolfgang Engel, Vorstandsvorsitzender von Confetti

Am 20. März 2014 kündigte Microsoft DirectX* 12 auf der Game Developers Conference an. Durch die Reduzierung des Ressourcen-Overhead wird DirectX 12 Anwendungen helfen, effizienter ausgeführt zu werden, den Energieverbrauch senken und Spielern ermöglichen, länger auf mobilen Geräten zu spielen.

Auf der SIGGRAPH 2014 hat Intel den CPU-Stromverbrauch beim Betrieb einer einfachen Asteroiden-Demo auf einem Microsoft Surface* Pro 3 Tablet gemessen. Die Demo-Anwendung kann von der DirectX 11 API auf die DirectX 12 API durch Antippen einer Schaltfläche umgeschaltet werden. Diese Demo zieht eine große Anzahl von Asteroiden im Raum mit einer gesperrten Framerate (https://software.intel.com/de-de/blogs/2014/08/11/siggraph-2014-directx-12-on-intel). Sie verbraucht weniger als die Hälfte der CPU-Leistung, wenn sie von der DirectX 12 API im Vergleich zu DirectX 11** angetrieben wird, was zu einem kühleren Gerät mit längerer Batterielebensdauer führt. In einem typischen Spiele-Szenario können alle Gewinne an CPU-Leistung in bessere Physik, AI, Pathfinding, oder andere CPU intensive Aufgaben, die machen das Spiel reicher an Funktionen oder energieeffizienter machen, investiert werden.

Handwerkszeug

Um Spiele mit DirectX 12 zu entwickeln, benötigen Sie die folgenden Werkzeuge:

  • Windows* 10 Technical Preview
  • DirectX 12 SDK
  • Visual Studio* 2013
  • DirectX 12-fähige GPU-Treiber

Wenn Sie ein Spiele-Entwickler sind, sehen Sie sich Microsofts DirectX Early Access Program unter https://onedrive.live.com/survey?resid=A4B88088C01D9E9A!107&authkey=!AFgbVA2sYbeoepQ an.

Installationsanleitungen für die Installation des SDK und die GPU-Treiber werden nach Ihrer Aufnahme in das DirectX Early Access Programm zur Verfügung gestellt.

Überblick

Von einem hohen Standpunkt aus gesehen und im Vergleich zu DirectX 10 und 11, unterscheidet sich die Architektur von DirectX 12 in den Bereichen der Zustandsverwaltung und der Art, wie Ressource verfolgt und im Speicher verwaltet werden.

DirectX 10 führte Zustandsobjekte ein, um eine Gruppe von Zuständen während der Laufzeit festzulegen. DirectX 12 führt Pipeline-Zustandsobjekte (PSOs) ein, um eine noch größere Gruppe von Zuständen zusammen mit Shadern festzulegen. Dieser Artikel konzentriert sich auf die Veränderungen im Umgang mit Ressourcen und überlässt die Beschreibung, wie Zustände in PSOs gruppiert sind, zukünftigen Artikeln.

In DirectX 11, war das System für die Vorhersage oder das Tracking von Ressourcennutzungsmuster zuständig, was das Anwendungsdesign bei der Verwendung von DirectX 11 auf breiter Ebene einschränkte. Grundsätzlich ist bei DirectX 12 der Programmierer, nicht das System oder der Treiber, für die Behandlung der folgenden drei Verwendungsmuster verantwortlich:

  1. Die Ressourcenbindung
    DirectX 10 und 11 verfolgten die Bindung von Ressourcen zur Grafik-Pipeline, um Ressourcen am Leben zu erhalten, die bereits von der Anwendung freigegeben wurden, weil sie immer noch durch ausstehende GPU-Arbeit referenziert wurde. DirectX 12 verfolgt die Ressourcenbindung nicht. Die Anwendung, oder in anderen Worten der Programmierer muss das Objektlebensdauer-Management behandeln.
  2. Inspektion der Ressourcenbindungen
    DirectX 12 inspiziert die Ressourcenbindungen nicht, um zu wissen, ob oder wann ein Ressourcenübergangs eingetreten sein könnte. Zum Beispiel könnte eine Anwendung in eine Renderziel über eine Renderziel-Ansicht (RTV) schreiben und dann dieses Renderziel als Textur über eine Shader-Ressourcenansicht (SRV) lesen. Mit der DirectX 11 API wurde vom GPU-Treiber erwartet, zu wissen, wann ein solcher Ressourcenübergang geschah um Gefahren beim Speicher lesen-ändern-schreiben zu vermeiden. In DirectX 12 muss man alle Ressourcenübergänge über spezielle API-Aufrufe identifizieren und verfolgen.
  3. Synchronisation von zugeordnetem Speicher
    In DirectX 11, übernimmt der Treiber die Synchronisierung von zugeordnetem Speicher zwischen der CPU und de GPU. Das System überprüfte die Ressourcenbindung, um zu verstehen, ob Rendering verzögert werden musste, weil eine Ressource, die für die CPU-Zugriff zugeordnet wurde noch nicht zugeordnet war. In DirectX 12 muss die Anwendung die Synchronisation des Zugangs von CPU und GPU zu Ressourcen behandeln. Ein Mechanismus, um den Speicherzugriff zu synchronisieren erfordert, dass ein Ereignis einen Tread aufweckt, wenn die GPU die Verarbeitung abgeschlossen hat.

Verschieben dieser Ressourcennutzungsmuster in das Reich der Anwendung erforderte eine neue Reihe von Programmierschnittstellen, die mit einer Vielzahl von unterschiedlichen GPU-Architekturen umgehen können.

Der Rest dieses Dokument beschreibt die neuen Ressourcenbindungsmechanismen, der erste Block sind die Deskriptoren.

Deskriptoren

Deskriptoren beschreiben im Speicher gespeicherte Ressourcen. Ein Deskriptor ist ein Datenblock, welcher der GPU ein Objekt in einem GPU spezifischen opaken Format beschreibt. Eine einfache Art des Denkens über Deskriptoren ist als Ersatz des alten „Ansicht“-Systems in DirectX 11. Zusätzlich zu den verschiedenen Arten von Deskriptoren wie Shader-Ressourcenansicht (SRV) und Ungeordnete-Zugriffsansicht (UAV) in DirectX 11, hat DirectX 12 andere Arten von Deskriptoren wie Sampler und Constant Buffer Views (CBVs).

Zum Beispiel wählt ein SRV welche zugrunde liegende Ressource zu verwenden ist, welche Gruppe von mipmaps/array slices zu verwenden ist, und das Format, um den Speicher zu interpretieren. Ein SRV-Descriptor muss die virtuelle GPU-Adresse der Direct3D* Ressource enthalten, die eine Textur sein könnte. Die Anwendung muss sicherstellen, dass die zugrunde liegende Ressource nicht bereits zerstört oder unzugänglich ist, weil sie nichtansässig ist.

Abbildung 1 zeigt einen Deskriptor, der eine „Ansicht“ in eine Textur darstellt:


Abbildung 1. Eine in einem Deskriptor beschriebene Shader-Ressourcenansicht [mit Genehmigung verwendet, Copyright © Microsoft]

Um einen Shader-Ressourcenansicht in DirectX 12 zu erstellen, verwenden Sie die folgende Struktur und Direct3D-Geräte-Methode:

typedef struct D3D12_SHADER_RESOURCE_VIEW_DESC
{
    DXGI_FORMAT Format;
    D3D12_SRV_DIMENSION ViewDimension;

    union
    {
        D3D12_BUFFER_SRV Buffer;
        D3D12_TEX1D_SRV Texture1D;
        D3D12_TEX1D_ARRAY_SRV Texture1DArray;
        D3D12_TEX2D_SRV Texture2D;
        D3D12_TEX2D_ARRAY_SRV Texture2DArray;
        D3D12_TEX2DMS_SRV Texture2DMS;
        D3D12_TEX2DMS_ARRAY_SRV Texture2DMSArray;
        D3D12_TEX3D_SRV Texture3D;
        D3D12_TEXCUBE_SRV TextureCube;
        D3D12_TEXCUBE_ARRAY_SRV TextureCubeArray;
        D3D12_BUFFEREX_SRV BufferEx;
    };
} D3D12_SHADER_RESOURCE_VIEW_DESC;

interface ID3D12Device
{
...
    void CreateShaderResourceView (
        _In_opt_ ID3D12Resource* pResource,
        _In_opt_ const D3D12_SHADER_RESOURCE_VIEW_DESC* pDesc,
        _In_ D3D12_CPU_DESCRIPTOR_HANDLE DestDescriptor);
};

Beispiel-Code für eine SRV könnte wie folgt aussehen:

// create SRV
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc;
ZeroMemory(&srvDesc, sizeof(D3D12_SHADER_RESOURCE_VIEW_DESC));
srvDesc.Format = mTexture->Format;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = 1;

mDevice->CreateShaderResourceView(mTexture.Get(), &srvDesc, mCbvSrvDescriptorHeap->GetCPUDescriptorHandleForHeapStart());

Dieser Code erstellt ein SRV für eine 2D-Textur und gibt sein Format und die virtuelle GPU-Adresse an. Das letzte Argument an CreateShaderResourceView ist ein Handle, an einen sogenannten Deskriptor-Heap, der vor dem Aufrufen dieser Methode zugeordnet wurde. Deskriptoren werden im Allgemeinen in Deskriptor-Heaps gespeichert, wie im nächsten Abschnitt beschrieben.

Hinweis: Es ist auch möglich, einige Arten von Deskriptoren über die GPU durch Treiber-versionierte Speicher genannt root-Parameter. Mehr dazu später.

Deskriptor-Heaps

Ein Deskriptor-Heap kann als eine Speicherzuordnung für eine Anzahl von Deskriptoren betrachtet werden. Verschiedene Arten von Deskriptor-Heaps können eine mehrere Arten von Deskriptoren enthalten. Hier sind die derzeit unterstützten Arten:

Typedef enum D3D12_DESCRIPTOR_HEAP_TYPE
{
 D3D12_CBV_SRV_UAV_DESCRIPTOR_HEAP	= 0,
 D3D12_SAMPLER_DESCRIPTOR_HEAP = (D3D12_CBV_SRV_UAV_DESCRIPTOR_HEAP + 1) ,
 D3D12_RTV_DESCRIPTOR_HEAP	= ( D3D12_SAMPLER_DESCRIPTOR_HEAP + 1 ) ,
 D3D12_DSV_DESCRIPTOR_HEAP	= ( D3D12_RTV_DESCRIPTOR_HEAP + 1 ) ,
 D3D12_NUM_DESCRIPTOR_HEAP_TYPES = ( D3D12_DSV_DESCRIPTOR_HEAP + 1 )
} 	D3D12_DESCRIPTOR_HEAP_TYPE;

Es gibt eine Deskriptor-Heap-Art für CBVs, SRVs und UAVs. Es gibt auch Arten, die mit render target view (RTV) und depth stencil view (DSV) umgehen.

Der folgende Code erstellt einen Deskriptor-Heap für neun Deskriptoren – jeder kann ein CBV, SRV, oder UAV sein:

// create shader resource view and constant buffer view descriptor heap
D3D12_DESCRIPTOR_HEAP_DESC descHeapCbvSrv = {};
descHeapCbvSrv.NumDescriptors = 9;
descHeapCbvSrv.Type = D3D12_CBV_SRV_UAV_DESCRIPTOR_HEAP;
descHeapCbvSrv.Flags = D3D12_DESCRIPTOR_HEAP_SHADER_VISIBLE;
ThrowIfFailed(mDevice->CreateDescriptorHeap(&descHeapCbvSrv, __uuidof(ID3D12DescriptorHeap), (void**)&mCbvSrvDescriptorHeap));

Die ersten beiden Einträge in der Deskriptor-Heap Beschreibung sind die Anzahl von Deskriptoren und die Art der Deskriptoren, die in diesen Deskriptor-Heap erlaubt sind. Der dritte Parameter D3D12_DESCRIPTOR_HEAP_SHADER_VISIBLE beschreibt diesen Deskriptor-Heap als für den Shader sichtbar. Deskriptor-Heaps, die nicht für einen Shader sichtbar sind, können beispielsweise für Staging-Deskriptoren auf der CPU oder für RTV, die nicht von innerhalb des Shaders wählbar sind, verwenden werden.

Obwohl dieser Code die Markierung setzt, die den Deskriptor-Heap einem Shader sichtbar macht, gibt es eine weitere Ebene. Ein Shader kann einen Deskriptor-Heap durch eine Deskriptor-Tabelle „sehen“ (es gibt auch Root-Deskriptoren, die keine Tabellen verwenden, mehr dazu später).

Deskriptor-Tabellen

Das primäre Ziel mit einem Deskriptor-Heap ist es, so viel Speicherplatz wie erforderlich zuzuweisen, um alle Deskriptoren für so viel Rendering wie möglich zu speichern, vielleicht ein Frame oder mehr

Hinweis: Umschaltende Deskriptor-Heaps könnten – abhängig von der zugrunde liegenden Hardware – im Leeren der GPU-Pipeline resultieren. Daher sollten umschaltende Deskriptor-Heaps minimiert oder mit anderen Operationen gepaart werde, die ohnehin die Grafik-Pipeline leeren würden.

Eine Deskriptor-Tabelle verschiebt in den Deskriptor-Heap. Statt die Grafik-Pipeline zu zwingen, immer den gesamten Heap zu sehen, sind umschaltende Deskriptoren-Tabellen eine kostengünstige Möglichkeit, um eine Reihe von Ressourcen zu ändern, die ein bestimmter Shader. Auf diese Weise muss der Shader nicht verstehen, wo die Ressourcen in Heap-Speicher zu finden. sind.

Mit anderen Worten, kann eine Anwendung mehrere Deskriptor-Tabellen verwenden, die den gleichen Deskriptor-Heap für verschiedene Shader, wie in Abbildung 2 dargestellt, indexieren:


Abbildung 2. Verschiedene Shader indexieren in den Deskriptor-Heap mit verschiedenen Deskriptor-Tabellen

Descriptor-Tabellen für eine SRV und einen Sampler werden im folgenden Codeausschnitt mit Sichtbarkeit für eine Pixel-Shader erstellt.

// define descriptor tables for a SRV and a sampler for pixel shaders
D3D12_DESCRIPTOR_RANGE descRange[2];
descRange[0].Init(D3D12_DESCRIPTOR_RANGE_SRV, 1, 0);
descRange[1].Init(D3D12_DESCRIPTOR_RANGE_SAMPLER, 1, 0);

D3D12_ROOT_PARAMETER rootParameters[2];
rootParameters[0].InitAsDescriptorTable(1, &descRange[0], D3D12_SHADER_VISIBILITY_PIXEL);
rootParameters[1].InitAsDescriptorTable(1, &descRange[1], D3D12_SHADER_VISIBILITY_PIXEL);

Die Sichtbarkeit der Deskriptoren-Tabelle ist auf den Pixel-Shader durch Bereitstellung der D3D12_SHADER_VISIBILITY_PIXEL Markierung. Die folgende Aufzählung definiert verschiedene Ebenen der Sichtbarkeit einer Deskriptor-Tabelle:

typedef enum D3D12_SHADER_VISIBILITY
{
 D3D12_SHADER_VISIBILITY_ALL	= 0,
 D3D12_SHADER_VISIBILITY_VERTEX	= 1,
 D3D12_SHADER_VISIBILITY_HULL	= 2,
 D3D12_SHADER_VISIBILITY_DOMAIN	= 3,
 D3D12_SHADER_VISIBILITY_GEOMETRY	= 4,
 D3D12_SHADER_VISIBILITY_PIXEL	= 5
} D3D12_SHADER_VISIBILITY;

Die Bereitstellung einer Markierung, die die Sichtbarkeit festlegt, wird das Argument an alle Shader-Ebenen senden, auch wenn sie nur einmal festgelegt ist.

Ein Shader kann eine Ressource über Deskriptor-Tabellen finden, aber die Deskriptor-Tabellen müssen diesem Shader zunächst als root-Parameter in einer Root-Signatur bekannt gemacht werden.

Root-Signatur und Parameter

Ein Root-Signatur speichert Root-Parameter, die von Shadern verwendet werden, um die Ressourcen, zu denen sie Zugang benötigen, zu finden. Diese Parameter existieren als verbindliche Platz auf einer Befehlsliste für die Sammlung von Ressourcen, die die Anwendung Shadern zur Vefügung stellen muss.

Die Root-Argumente können sein:

  • Descriptor-Tabellen: Wie oben beschrieben, enthalten sie einen Offset plus die Anzahl von Deskriptoren in dem Deskriptor-Heap.
  • Root-Deskriptoren: Nur eine geringe Anzahl von Deskriptoren können direkt in einem Root-Parameter gespeichert werden. Dies erspart der Anwendung die Mühe, diese Deskriptoren in einen Deskriptor-Heap abzulegen und entfernt einen Umweg.
  • Root-Konstanten: Die sind Konstanten, die den Shadern direkt zur Verfügung gestellt werden, ohne durch Root-Deskriptoren oder Deskriptor-Tabellen gehen zu müssen.

Um eine optimale Leistung zu erzielen, sollten Anwendungen in der Regel das Layout der Root-Parameter in abnehmender Reihenfolge der Änderungsfrequenz sortieren.

Alle Root-Parameter wie Deskriptor-Tabellen, Root-Deskriptoren, und Root-Konstanten werden in einem Befehlsliste gebacken und der Treiber wird diese im Namen der Anwendung versionieren. Mit anderen Worten, wann immer einer der Root-Parameter sich zwischen Draw- oder Dispatch-Aufrufen ändert, wird die Hardware die Versionsnummer der Root-Signatur aktualisieren. Jeder Draw-/Dispatch-Anruf erhält eine eindeutige ganze Reihe von Root-Parameter-Zuständen, wenn sic eines der Argumente ändert.

Root-Deskriptoren und Root-Konstanten verringern die Höhe der GPU-Umleitung, wenn darauf zugegriffen wird, während Deskriptor-Tabellen den Zugriff auf eine größere Datenmenge ermöglichen, aber auf Kosten einer erhöhten Dereferenzierungsebene. Wegen der höheren Dereferenzierungsebene mit Deskriptor-Tabellen kann die Anwendung bis zum Absenden der Befehlsliste für die Ausführung Inhalt initialisieren. Zusätzlich bietet das Shader Model 5.1, das von jeder DirectX 12 Hardware unterstützt wird, Shader in eine bestimmte Deskriptor-Tabelle dynamisch zu indexieren. So kann ein Shader auswählen, welchen Deskriptor er zur Shader-Ausführungszeit aus einer Deskriptor-Tabelle entfernen möchte. Eine Anwendung kann nur eine große Deskriptor-Tabelle erstellen und immer die Indizierung verwenden (über so etwas wie eine Material-ID), um den gewünschten Deskriptor zu erhalten.

Verschiedene Hardware-Architekturen werden verschiedene Kompromisse zwischen Leistung unter Verwendung großer Mengen von Root-Konstanten und Root-Deskriptoren im Vergleich mit Deskriptor-Tabellen zeigen. Daher ist es notwendig, das Verhältnis zwischen Root-Parameter und Deskriptor-Tabellen je nach Hardwarezielplattformen fein einzustellen.
Ein durchaus vernünftiges Ergebnis für eine Anwendung für eine Anwendung kann eine Kombination aus allen Arten von Bindungen sein: Root-Konstanten, Root-Deskriptoren, Deskriptor-Tabellen für Deskriptoren, die on-the-fly gesammelt werden, wenn Draw-Aufrufe ausgegeben werden, und dynamische Indizierung großer Deskriptor-Tabellen.

Der folgende Code speichert die beiden oben genannten Deskriptor-Tabellen als Root-Parameter in einer Root-Signatur. 

// define descriptor tables for a SRV and a sampler for pixel shaders
D3D12_DESCRIPTOR_RANGE descRange[2];
descRange[0].Init(D3D12_DESCRIPTOR_RANGE_SRV, 1, 0);
descRange[1].Init(D3D12_DESCRIPTOR_RANGE_SAMPLER, 1, 0);

D3D12_ROOT_PARAMETER rootParameters[2];
rootParameters[0].InitAsDescriptorTable(1, &descRange[0], D3D12_SHADER_VISIBILITY_PIXEL);
rootParameters[1].InitAsDescriptorTable(1, &descRange[1], D3D12_SHADER_VISIBILITY_PIXEL);

// store the descriptor tables int the root signature
D3D12_ROOT_SIGNATURE descRootSignature;
descRootSignature.Init(2, rootParameters, 0);

ComPtr<ID3DBlob> pOutBlob;
ComPtr<ID3DBlob> pErrorBlob;
ThrowIfFailed(D3D12SerializeRootSignature(&descRootSignature,
              D3D_ROOT_SIGNATURE_V1, pOutBlob.GetAddressOf(),
              pErrorBlob.GetAddressOf()));

ThrowIfFailed(mDevice->CreateRootSignature(pOutBlob->GetBufferPointer(),
              pOutBlob->GetBufferSize(), __uuidof(ID3D12RootSignature),
             (void**)&mRootSignature));

Alle Shader in einem PSO müssen mit der in diesem PSO angegebenen Root-Signatur kompatibel sein; andernfalls wird der PSO nicht erstellt werden.
 

Eine Root-Signatur muss in einer Befehlsliste oder als Bundle eingestellt werden. Dies geschieht durch den Aufruf: 

commandList->SetGraphicsRootSignature(mRootSignature);

Nach dem Einstellen der Root-Signatur, muss das Set der Bindungen definiert werden. In dem obigen Beispiel würde dies mit dem folgenden Code durchgeführt werden:

// set the two descriptor tables to index into the descriptor heap
// for the SRV and the sampler
commandList->SetGraphicsRootDescriptorTable(0,
               mCbvSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart());
commandList->SetGraphicsRootDescriptorTable(1,
               mSamplerDescriptorHeap->GetGPUDescriptorHandleForHeapStart());

Die Anwendung muss die entsprechenden Parameter in jedem der beiden Slots in der Root-Signatur vor der Ausgabe eines Draw-Aufrufs oder eines Dispatch-Aufruf einstellen. In diesem Beispiel enthält der erste Slot nun einen Deskriptor-Handle, dass in den Deskriptor-Heap auf einen SRV-Deskriptor indexiert und der zweite Slot enthält nun eine Deskriptor-Tabelle, die in den Deskriptor-Heap auf einen Sample-Deskriptor indexiert.

Eine Anwendung kann beispielsweise zwischen Draw-Aufrufen die Bindung auf den zweiten Slot ändern. Das heißt, sie muss nur den zweiten Slot für den zweiten Draw-Aufruf binden. 

Alles zusammensetzen

Das große Quellcode-Snippet unten zeigt alle Mechanismen, die verwendet werden, um Ressourcen zu binden. Diese Anwendung verwendet nur eine Textur, und dieser Code bietet einen Sampler und einen SRV für diese Textur:

// define descriptor tables for a SRV and a sampler for pixel shaders
D3D12_DESCRIPTOR_RANGE descRange[2];
descRange[0].Init(D3D12_DESCRIPTOR_RANGE_SRV, 1, 0);
descRange[1].Init(D3D12_DESCRIPTOR_RANGE_SAMPLER, 1, 0);

D3D12_ROOT_PARAMETER rootParameters[2];
rootParameters[0].InitAsDescriptorTable(1, &descRange[0], D3D12_SHADER_VISIBILITY_PIXEL);
rootParameters[1].InitAsDescriptorTable(1, &descRange[1], D3D12_SHADER_VISIBILITY_PIXEL);

// store the descriptor tables in the root signature
D3D12_ROOT_SIGNATURE descRootSignature;
descRootSignature.Init(2, rootParameters, 0);

ComPtr<ID3DBlob> pOutBlob;
ComPtr<ID3DBlob> pErrorBlob;
ThrowIfFailed(D3D12SerializeRootSignature(&descRootSignature,
              D3D_ROOT_SIGNATURE_V1, pOutBlob.GetAddressOf(),
              pErrorBlob.GetAddressOf()));

ThrowIfFailed(mDevice->CreateRootSignature(pOutBlob->GetBufferPointer(),
              pOutBlob->GetBufferSize(), __uuidof(ID3D12RootSignature),
             (void**)&mRootSignature));



// create descriptor heap for shader resource view
D3D12_DESCRIPTOR_HEAP_DESC descHeapCbvSrv = {};
descHeapCbvSrv.NumDescriptors = 1; // for SRV
descHeapCbvSrv.Type = D3D12_CBV_SRV_UAV_DESCRIPTOR_HEAP;
descHeapCbvSrv.Flags = D3D12_DESCRIPTOR_HEAP_SHADER_VISIBLE;
ThrowIfFailed(mDevice->CreateDescriptorHeap(&descHeapCbvSrv, __uuidof(ID3D12DescriptorHeap), (void**)&mCbvSrvDescriptorHeap));

// create sampler descriptor heap
D3D12_DESCRIPTOR_HEAP_DESC descHeapSampler = {};
descHeapSampler.NumDescriptors = 1;
descHeapSampler.Type = D3D12_SAMPLER_DESCRIPTOR_HEAP;
descHeapSampler.Flags = D3D12_DESCRIPTOR_HEAP_SHADER_VISIBLE;
ThrowIfFailed(mDevice->CreateDescriptorHeap(&descHeapSampler, __uuidof(ID3D12DescriptorHeap), (void**)&mSamplerDescriptorHeap));

// skip the code that uploads the texture data into heap

// create sampler descriptor in the sample descriptor heap
D3D12_SAMPLER_DESC samplerDesc;
ZeroMemory(&samplerDesc, sizeof(D3D12_SAMPLER_DESC));
samplerDesc.Filter = D3D12_FILTER_MIN_MAG_MIP_LINEAR;
samplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_WRAP;
samplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_WRAP;
samplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_WRAP;
samplerDesc.MinLOD = 0;
samplerDesc.MaxLOD = D3D11_FLOAT32_MAX;
samplerDesc.MipLODBias = 0.0f;
samplerDesc.MaxAnisotropy = 1;
samplerDesc.ComparisonFunc = D3D12_COMPARISON_ALWAYS;
mDevice->CreateSampler(&samplerDesc,
           mSamplerDescriptorHeap->GetCPUDescriptorHandleForHeapStart());

// create SRV descriptor in the SRV descriptor heap
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc;
ZeroMemory(&srvDesc, sizeof(D3D12_SHADER_RESOURCE_VIEW_DESC));
srvDesc.Format = SampleAssets::Textures->Format;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = 1;
mDevice->CreateShaderResourceView(mTexture.Get(), &srvDesc,
            mCbvSrvDescriptorHeap->GetCPUDescriptorHandleForHeapStart());


// writing into the command list
// set the root signature
commandList->SetGraphicsRootSignature(mRootSignature);

// other commands here ...

// set the two descriptor tables to index into the descriptor heap
// for the SRV and the sampler
commandList->SetGraphicsRootDescriptorTable(0,
               mCbvSrvDescriptorHeap->GetGPUDescriptorHandleForHeapStart());
commandList->SetGraphicsRootDescriptorTable(1,
               mSamplerDescriptorHeap->GetGPUDescriptorHandleForHeapStart());

Statische Sampler

Nachdem Sie nun gesehen haben, wie man einen Sampler mit einem Deskriptor-Heap und einer Deskriptor-Tabelle erstellt, gibt es eine weitere Möglichkeit, Sampler in Anwendungen zu verwenden. Da viele Anwendungen nur einen festen Satz von Samplern brauchen, ist es möglich, statische Sampler als Root-Argument zu verwenden.

Derzeit sieht die Root-Signatur wie folgt aus:

 

typedef struct D3D12_ROOT_SIGNATURE
{
    UINT NumParameters;
    const D3D12_ROOT_PARAMETER* pParameters;
    UINT NumStaticSamplers;
    const D3D12_STATIC_SAMPLER* pStaticSamplers;
    D3D12_ROOT_SIGNATURE_FLAGS Flags;

    // Initialize struct
    void Init(
        UINT numParameters,
        const D3D12_ROOT_PARAMETER* _pParameters,
        UINT numStaticSamplers = 0,
        const D3D12_STATIC_SAMPLER* _pStaticSamplers = NULL,
        D3D12_ROOT_SIGNATURE_FLAGS flags = D3D12_ROOT_SIGNATURE_NONE)
    {
        NumParameters = numParameters;
        pParameters = _pParameters;
        NumStaticSamplers = numStaticSamplers;
        pStaticSamplers = _pStaticSamplers;
        Flags = flags;
    }

    D3D12_ROOT_SIGNATURE() { Init(0,NULL,0,NULL,D3D12_ROOT_SIGNATURE_NONE);}

    D3D12_ROOT_SIGNATURE(
        UINT numParameters,
        const D3D12_ROOT_PARAMETER* _pParameters,
        UINT numStaticSamplers = 0,
        const D3D12_STATIC_SAMPLER* _pStaticSamplers = NULL,
        D3D12_ROOT_SIGNATURE_FLAGS flags = D3D12_ROOT_SIGNATURE_NONE)
    {
        Init(numParameters, _pParameters, numStaticSamplers, _pStaticSamplers, flags);
    }
} D3D12_ROOT_SIGNATURE;

Ein Satz von statischen Samplern kann unabhängig von den Root-Parametern in einer Root-Signatur definiert werden. Wie oben erwähnt, definieren Root-Parameter einen Bindungsraum, in dem Argumente zur Laufzeit zur Verfügung gestellt werden können, während statische Sampler per Definition unveränderlich sind.

Da Root-Signaturen in HLSL verfasst werden können, können statische Sampler damit ebenso verfasst werden. Fürs Erste kann eine Anwendung nur maximal 2032 eindeutige statische Sampler haben. Dies ist etwas weniger als die nächste Potenz von zwei und ermöglicht es dem Treiber, einen Teil der Slots für interne Verwendung zu nutzen.

Die statischen Sampler, die in einer Root-Signatur definiert werden, sind unabhängig von Samplern, die eine Anwendung wählt, um sie in einen Deskriptor-Heap zu setzen, somit können beiden Mechanismen zur gleichen Zeit verwendet werden.

Falls die Auswahl der Sampler nicht wirklich dynamisch und dem Shader bei der Kompilierung unbekannt ist, sollte eine Anwendung Sampler in einem Deskriptor-Heap verwalten.

Fazit

DirectX 12 bietet die volle Kontrolle über Ressourcennutzungsmuster. Der Anwendungsentwickler ist verantwortlich für die Zuteilung von Speicher in Deskriptor-Heaps, die Beschreibung der Ressourcen in Deskriptoren, und das Zulassen der „Indexierung“ durch den Shader in Deskriptor-Heaps über Deskriptor-Tabellen, die dem Shader über Root-Signaturen „bekannt“ gemacht werden.

Darüber hinaus können Root-Signaturen verwendet werden, um einen benutzerdefinierte Parameterraum für Shader mit einer beliebigen Kombination der vier Optionen zu definieren:

  • Root-Konstanten
  • Statische Sampler
  • Root-Deskriptoren
  • Deskriptor-Tabellen

Am Ende ist die Herausforderung, die wünschenswerteste Form der Bindung für die Arten von Ressourcen und deren Häufigkeit der Aktualisierung auswählen.

Über den Autor

Wolfgang is der Vorstandsvorsitzend von Confetti. Confetti ist eine Denkfabrik für fortgeschrittene Echtzeit-Grafik Forschung und Dienstleister für die Videospiel-und Filmindustrie. Vor der Mitgründung von Confetti arbeitete Wolfgang für mehr als vier Jahre als Lead-Programmer Grafiken in Rockstars Core-Technologie-Gruppe RAGE. Er ist der Gründer und Herausgeber der ShaderX und GPU Pro Bücher-Serie, ein Microsoft MVP, Autor mehrerer Bücher und Artikel über Echtzeit-Rendering und schreibt regelmäßig für Websites und den GDC. Eines der Bücher, das er geschrieben -ShaderX4- hat den Spiel-Entwickler Front line award 2006 gewonnen. Wolfgang ist in vielen Beiräten in der Branche; einer von ihnen ist der Microsoft Graphics Beirat für DirectX 12. Er ist ein aktiver Beiträger für mehrere zukünftige Standards, die die Spiele-Industrie antreiben. Sie können ihn auf Twitter finden unter: wolfangengel.  Die Webseite von Confetti ist www.conffx.com.

Anerkennung

Ich möchte Chas Boyd, Amar Patel, und David Reinig für ihre Korrektur und Rückmeldung danken.

Verweise und Links zum Thema

 

** Software und Workloads, die in Performance-Tests verwendet werden, können für die Leistungseigenschaften von Intel Mikroprozessoren optimiert worden sein. Leistungstests wie SYSmark* und MobileMark* werden mit spezifischen Computersystemen, Komponenten, Software, Operationen und Funktionen gemessen. Eine Änderung einer dieser Faktoren kann dazu führen, dass die Ergebnisse variieren. Sie sollten andere Informationen und Leistungstests heranziehen, um Ihnen bei einer umfassenden Bewertung Ihrer vorgesehenen Anschaffung, einschließlich der Leistung des betreffenden Produkts in Verbindung mit anderen Produkten, zu helfen.


Viewing all articles
Browse latest Browse all 1201

Trending Articles