© StonePictures/Shutterstock.com
Descriptor-Sets und Pipelineobjekte

Besuch auf dem Vulkan


Nachdem wir uns im letzten Artikel mit der Initialisierung und dem CPU-seitigen Zugriff auf Buffer-, Textur- und Texturarrayobjekte auseinandergesetzt haben, werden wir uns an dieser Stelle mit den notwendigen Vorarbeiten befassen, um auch innerhalb eines Shader-Programms auf die besagten Datensätze zugreifen zu können.

Zugegeben, Wunderdinge kann selbst das neue Vulkan-API [1], [2], [3] nicht vollbringen. Und auch wenn Aussagen wie „High-End-Grafik endlich auch auf leistungsschwächeren Endgeräten“ oder „eierlegende Wollmilchschnittstelle“ maßlos übertrieben sind, steckt zumindest mehr als nur ein Körnchen Wahrheit in diesen Werbebotschaften. Zweifelsohne ist es keine allzu große Überraschung, dass die Performancezuwächse bei GPU-limitierten Grafikanwendungen auch bei einem API-Wechsel hin zu Vulkan bestenfalls moderat ausfallen. Sofern die Recheneinheiten einer GPU voll ausgelastet sind, lassen sich spürbare Performancesteigerungen noch am ehesten durch eine Überarbeitung der einzelnen Shader-Programme erreichen. Sieht man jedoch einmal von besonders aufwendig programmierten High-End-Grafikanwendungen ab, dann erweist sich insbesondere auf leistungsschwächeren Endgeräten in vielen Fällen gar nicht der verbaute Grafikprozessor als limitierender Faktor, sondern die CPU.

Genau an dieser Stelle setzt das neue Vulkan-API an. Durch einen geringen Treiber-Overhead auf der einen und eine effiziente wechselseitige Kommunikation zwischen der CPU und der GPU auf der anderen Seite lässt sich die Prozessorauslastung deutlich vermindern. Im Unterschied zu Vulkan erfolgt im Rahmen einer OpenGL-Anwendung die CPU-GPU-Kommunikation zuweilen auf eine recht ineffiziente Art und Weise. Im nachstehend gezeigten Sourcecode-Ausschnitt werden beispielsweise bei jedem Rendering-Durchgang beständig die gleichen Informationen zwischen Haupt- und Shader-Programm ausgetauscht – was sowohl aufseiten der CPU als auch aufseiten der GPU zu einer Verschwendung von Rechenleistung führt:

  • Auswahl des zu verwendenden Shader-Programms:

VegetationShader->Use_Shader(); [...]
  • Übergabe sämtlicher Parameter (einfache Variablen, Vektoren und Matrizen), die für eine korrekte Durchführung der Shader-Berechnungen unverzichtbar sind:

VegetationShader->Set_ShaderMatrix4X4(pViewProjectionMatrix, "matViewProjection"); VegetationShader->Set_ShaderFloatVector3(pCameraPosition, "CameraPosition"); [...]
  • Festlegen, auf welche Texturen und Texturarrays man innerhalb des besagten Shader-Programms zugreifen kann:

VegetationShader->Set_TextureArray(0, ..., "SurfaceTextureArray"); VegetationShader->Set_TextureArray(1, ..., "NormalTextureArray"); [...]
  • Festlegen, auf welche Bufferobjekte man innerhalb des Shader-Programms zugreifen kann:

VegetationShader->Set_TextureBuffer(5, ..., "LightingDataTBO"); [...]
  • Nachdem schließlich alle erforderlichen Parameter an das Shader-Programm weitergeleitet und die zu verwendenden Buffer-, Textur- sowie Texturarrayobjekte gebunden worden sind, können wir nun endlich im letzten Schritt den eigentlichen Draw Call an die Grafikkarte übermitteln:

VegetationShader->Set_VertexArrayObject(pVegetationLayerMeshVB_IB-> VertexArrayObjectID); pVegetationLayerMeshVB_IB->Render_Mesh(VegetationShader, 0, NumVisibleVegetationLayerObjects);

Im Zuge einer Vulkan-Anwendung müssen wir hingegen vor dem ersten Einsatz eines Shader-Programms zunächst einmal eine Liste (ein sogenanntes Descriptor-Set) anlegen, in der sämtliche Buffer-, Textur- und Texturarrayobjekte (kurz: Datenobjekte) vermerkt werden, auf die der betreffende Shader später einmal zugreifen kann. Dank der Verwendung eines solchen Descriptor-Sets, das sich auf Basis eines zuvor erstellten Descriptor-Set-Layouts aus einem sogenannten Descriptor-Pool heraus anfordern lässt, müssen wir die in diesem Set gespeicherten Informationen nur ein einziges Mal an die GPU übermitteln. Dadurch wird die im vorangegangenen Sourcecode-Beispiel gezeigte permanente Kommunikation zwischen der CPU und der GPU praktisch obsolet. Vor einem Draw Call müssen wir lediglich im Zuge der Command-Buffer-Erstellung festlegen, welches der zuvor übermittelten Descriptor-Sets bei den anstehenden Shader-Berechnungen Verwendung finden soll.

Descriptor-Set-Layouts und Descriptor-Pool-Objekte

Als wir uns im letzten Artikel mit der CPU-seitigen Handhabung von Buffer-, Textur- und Texturarrayobjekten auseinandergesetzt haben (Initialisierungsschritte, Aufräumarbeiten, Aktualisierung bzw. Auslesen der Bufferdaten, Handling von einzelnen Texturen und Texturarrayobjekten), konnten Sie sich das erste Mal selbst ein Bild davon machen, mit wie viel Aufwand die Entwicklung einer Vulkan-Anwendung verbunden ist. Während sich mithilfe von OpenGL viele alltägliche Aufgaben, wie beispielsweise die Erstellung und Aktualisierung von Buffer- und Texturobjekten, mit einigen wenigen Funktionsaufrufen bewerkstelligen lassen, sind für die Bewältigung der gleichen Aufgaben in einem Vulkan-Programm hingegen viele Zeilen Sourcecode erforderlich. Die Handhabung der zuvor angesprochenen Objekte aus dem Hauptprogramm heraus stellt jedoch nur eine Seite der Medaille dar. An dieser Stelle werden wir nun die Voraussetzungen dafür schaffen, dass man auch GPU-seitig – also von innerhalb eines Shader-Programms – auf die für die anstehenden Berechnungen benötigten Datenobjekte zugreifen kann.

rudolph_vulkan_1.tif_fmt1.jpgAbb. 1: Vulkan-Demoanwendung: Kugelförmige 3-D-Objekte
rudolph_vulkan_2.tif_fmt1.jpgAbb. 2: Vulkan-Demoanwendung: Würfelförmige 3-D-Objekte

Listing 1: Deklaration von Parametern, Buffern und Texturen innerhalb eines Shaders

// Vertex-Shader-Version festlegen: #version 450 // Genauigkeit für Fließkommaberechnungen // festlegen: precision highp float; // Zu verwendende Erweiterungen festlegen: #extension GL_ARB_separate_shader_objects : enable #extension GL_ARB_shading_language_420pack : enable #extension GL_ARB_enhanced_layouts: enable /* Variablen für den Zugriff auf die im Vertexbuffer gespeicherten Daten: */ layout (location = 0) in vec3 inPos; layout (location = 1) in vec3 inNormal; layout (location = 2) in vec3 inPerpendicular1; layout (location = 3) in vec3 inPerpendicular2; layout (location = 4) in vec3 inTexCoord; // Push-Constant-List mit zusätzlichen Shader-// Parametern: layout(push_constant) uniform _PushConstantsListPerVertex { vec3 Scale; float unusedParam; } PushConstantsListPerVertex; /* Der nachfolgende Uniform-Block ermöglicht den Zugriff auf die in einem Uniform-Buffer gespeicherten Transformationsmatrizen: */ layout (set = 0, binding = 0) uniform UniformDataPerVertex { mat4 matProjection; mat4 matView; mat4 matRotationArray[2]; mat4 matCameraSpaceTransformArray[2]; }; struct SimulationDataStruct { vec4 InstancePositionOffset; vec4 InstanceVelocity; }; /* Der nachfolgende Shader-Storage-Block ermöglicht den Zugriff auf die in einem Shader-Storage-Buffer gespeicherten Positions- und Geschwindigkeitsdaten: */ layout (set = 0, binding = 1) readonly buffer SimulationDataStorageBuffer { SimulationDataStruct SimulationData[]; }; // Vom Shader berechnete Vertexposition im // Bildraum: out gl_PerVertex {vec4 gl_Position;}; /* Weitere Daten zur Weiterleitung an den Fragment Shader (Vertex-Shader-Output-Variablen): */ layout (location = 0) out vec3 outTransformedNormal; layout (location = 1) out vec3 outTransformedPerpendicular1; layout (location = 2) out vec3 outTransformedPerpendicular2; layout (location = 3) out vec4 outCameraSpacePosAndDepth; layout (location = 4) out vec3 outTexCoord; layout (location = 5) flat out int outVertexIndex; void main() { [Vertex Shader Code] } //------------------------------------------------------- // Fragment-Shader-Version festlegen: #version 450 precision highp float; #extension GL_ARB_separate_shader_objects : enable #extension GL_ARB_shading_language_420pack : enable #extension GL_ARB_enhanced_layouts: enable // Push Constant List mit zusätzlichen Shader-// Parametern: #define PushConstantsListVertexShaderSize layout(offset = 16) layout(push_constant) uniform _PushConstantsListPerFragment { PushConstantsListVertexShaderSize vec4 AdditionalEmissiveColor; } PushConstantsListPerFragment; /* Uniform-Block für den Zugriff auf die in einem weiteren Uniform-Buffer gespeicherten Beleuchtungs- und Texturierungsdaten: */ layout (set = 0, binding = 2) uniform UniformDataPerFragment { float LightTextureIntensity; float InvSpecularIntensity; float SurfaceHeightScale; float TextureID; }; /* 4 sampler2DArray-Objekte für den Zugriff auf die in diversen Texturarrays gespeicherten Texturen: */ layout (set = 0, binding = 3) uniform sampler2DArray SurfaceTextureArray; layout (set = 0, binding = 4) uniform sampler2DArray NormalTextureArray; layout (set = 0, binding = 5) uniform sampler2DArray SpecularTextureArray; layout (set = 0, binding = 6) uniform sampler2DArray LightTextureArray; /* samplerBuffer-Objekt für den Zugriff auf die in einem Uniform-Texel-Buffer gespeicherten Farbwerte: */ layout (set = 0, binding = 7) uniform samplerBuffer RandomColorTBO; // Fragment-Shader-Inputvariablen: layout (location = 0) in vec3 inTransformedNormal; layout (location = 1) in vec3 inTransformedPerpendicular1; layout (location = 2) in vec3 inTransformedPerpendicular2; layout (location = 3) in vec4 inCameraSpacePosAndDepth; layout (location = 4) in vec3 inTexCoord; layout (location = 5) flat in int inVertexIndex; // Fragment-Shader-Outputvariablen: layout(location = 0) out vec4 outBaseColor; layout(location = 1) out vec4 outPositionAndDepth; layout(location = 2) out vec4 outNormal; layout(location = 3) out vec4 outSpecular; layout(location = 4) out vec4 outEmissive; const float g_SpecularBrightnessFactor = 1.0f; void main() { [Fragment Shader Code] }

In diesem Zusammenhang richten wir unser Augenmerk zunächst einmal auf die beiden in Listing 1 skizzierten Shader-Programme, die im Rahmen unserer ersten beiden Demoanwendungen beim Rendering der in den Abbildungen 1 und 2 gezeigten kugel- und würfelförmigen 3-D-Objekte zum Einsatz kommen. Wie Sie nun anhand der nachfolgend gezeigten Aufstellung erkennen können, ist für eine korrekte Darstellung der besagten 3-D-Modelle der Zugriff auf insgesamt zwei Push-Constant-Blöcke, einen Shader-Storage-Buffer, zwei Uniform-Buffer, vier Texturarrayobjekte sowie einen Uniform-Texel-Buffer erforderlich:

  • Wir benötigen zwei Push-Constant-Blöcke: einen für den Vertex und einen für den Fragment Shader;

  • einen Uniform-Block im Vertex-Shader für den Zugriff auf die in einem Uniform-Buffer gespeicherten Transformationsmatrizen;

  • einen Shader-Storage-Block im Vertex-Shader für den Zugriff auf die in einem Shader-Storage-Buffer gespeicherten Positions- und Geschwindigkeitsdaten;

  • einen zweiten Uniform-Block im Fragment Shader für den Zugriff auf die in einem weiteren Uniform-Buffer gespeicherten Beleuchtungs- und Texturierungsdaten;

  • vier sampler2DArray-Objekte für den Zugriff auf die in vier Texturarrays gespeicherten Texturen (Surface Maps, Normal Maps, Specular Maps sowie Light Maps)

  • sowie ein samplerBuffer-Objekt für den Zugriff auf die in einem Uniform-Texel-Buffer gespeicherten Farbwerte.

Bevor wir jetzt im Rahmen der anstehenden Shader-Berechnungen auf die besagten Datenobjekte zugreifen können, müssen wir diese zunächst einmal an unsere Shader-Programme anbinden. In diesem Zusammenhang greifen wir auf eine Instanz der bereits in einem vorigen Vulkan-Artikel [4] angesprochenen CVulkanDescSetLayout_PipelineLayout_DescPool-Klasse zurück, um festzulegen, über welche Shade...

Neugierig geworden?

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang