Kapseln des VulkanCommand Buffers in eigener Klasse



  • Hallo alle zusammen,

    ich hab vor für meine Grafik Engine die Vulkan Command Buffer Funktionalität zu kapseln.
    Dabei ist es mir jedoch wichtig, dass diese flexibel nutzbar ist.

    Aktuell habe ich eine Methode, welches mir einen Commandbuffer erstellt.
    Diese Methode will ich jedoch aufteilen in viele diverse Methoden wie zum Beispiel: BeginCommandBuffer(), BeginRenderPass(), BindPipeline(), Draw(), DrawIndexed() etc.
    Nun was mich bei der Umsetzung stört ist jedoch die interne Schleife, welche jeweils einen CommandBuffer pro Framebuffer erstellt. Meine Überlegung war dies zu trennen mit einer Klasse CommandBuffer, welche diese Methoden bietet und einer Funktion buildCommandBuffer(class CommandBuffer buffer), welche dann jeweils einen CommandBuffer pro Framebuffer erstellt. Aber ich bin mir nicht sicher, ob das eine gute Lösung ist.

    Zur besseren Vorstellung hier das Beispiel meiner aktuellen Methode:

    void VulkanCommandBuffer::CreateCommandBuffers(SVulkan &vulkanInstance, VulkanFramebuffer &vulkanFramebuffer, VulkanPipeline &vulkanPipeline, VulkanRenderPass &renderPass, VkViewport viewportParam, VkRect2D scissorParam)
    	{
    		mCommandBuffers.resize(vulkanFramebuffer.GetFramebuffers().size());
    
    		VkCommandBufferAllocateInfo allocInfo = {};
    		allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    		allocInfo.commandPool = mCommandPool;
    		allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    		allocInfo.commandBufferCount = (uint32)mCommandBuffers.size();
    
    		if (vkAllocateCommandBuffers(vulkanInstance.logicalDevice, &allocInfo, mCommandBuffers.data()) != VK_SUCCESS)
    		{
    			throw VulkanException("failed to create command buffers!");
    		}
    
    		for (unsigned int i = 0; i < mCommandBuffers.size(); i++)
    		{
    			VkCommandBufferBeginInfo beginInfo = {};
    			beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    			beginInfo.flags = 0;
    			beginInfo.pInheritanceInfo = nullptr;
    
    			if (vkBeginCommandBuffer(mCommandBuffers[i], &beginInfo) != VK_SUCCESS)
    			{
    				throw VulkanException("failed to begin recording command buffer!");
    			}
    		
    
    			if (viewportParam.height == 0 && viewportParam.width == 0)
    			{
    				// there is no viewport used
    				VkViewport viewport = {};
    				viewport.x = 0.0f; // default: 0.0f;
    				viewport.y = 0.0f; // default: 0.0f;
    				viewport.width = (float)vulkanInstance.swapChainExtent.width; // default: (float)vulkanInstance.swapchainExtend.Width
    				viewport.height = (float)vulkanInstance.swapChainExtent.height;// default: (float)vulkanInstance.swapchainExtend.Height
    				viewport.minDepth = 0.0; // default: 0.0
    				viewport.maxDepth = 1.0; // default: 1.0
    				vkCmdSetViewport(mCommandBuffers[i], 0, 1, &viewport);
    			}
    			else
    			{
    				vkCmdSetViewport(mCommandBuffers[i], 0, 1, &viewportParam);
    			}
    
    			if (scissorParam.extent.height == 0 && scissorParam.extent.width == 0)
    			{
    				VkRect2D scissor = {};
    				scissor.offset = { 0, 0 }; // default: { 0, 0 };
    				scissor.extent.width = vulkanInstance.swapChainExtent.width;
    				scissor.extent.height = vulkanInstance.swapChainExtent.height;
    				vkCmdSetScissor(mCommandBuffers[i], 0, 1, &scissor);
    			}
    			else
    			{
    				vkCmdSetScissor(mCommandBuffers[i], 0, 1, &scissorParam);
    			}
    
    
    
    			VkRenderPassBeginInfo renderPassInfo = {};
    			renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
    			renderPassInfo.renderPass = renderPass.GetRenderPass();
    			renderPassInfo.framebuffer = vulkanFramebuffer.GetFramebuffers()[i];
    			renderPassInfo.renderArea.offset = { 0, 0 };
    			renderPassInfo.renderArea.extent = vulkanInstance.swapChainExtent;
    
    			VkClearValue clearColor = {0.0f, 0.0f, 0.0f, 1.0f};
    			renderPassInfo.clearValueCount = 1;
    			renderPassInfo.pClearValues = &clearColor;
    
    			vkCmdBeginRenderPass(mCommandBuffers[i], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);
    
    			vkCmdBindPipeline(mCommandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, vulkanPipeline.GetPipeline());
    			vkCmdDraw(mCommandBuffers[i], 3, 1, 0, 0);
    
    			vkCmdEndRenderPass(mCommandBuffers[i]);
    
    			if (vkEndCommandBuffer(mCommandBuffers[i]) != VK_SUCCESS)
    			{
    				throw VulkanException("failed to record command buffer!");
    			}
    		}
    	}
    
    

    Mein Ziel ist es auf einfache und flexible Weise diverse CommandBuffer erstellen zu können für MainRendering, offscreen Rendering usw. Ich zerbreche mir an diese Designentscheidung jetzt schon seit längeren den Kopf, vielen Dank im Vorraus für eure Tipps und Ratschläge 🙂

    Gruß

    Dennis



  • Bis jetzt benutzt du ja mehrfach den Indexzugriff mCommandBuffers[i] in deinem Code.
    Besser wäre es, nur einmalig die Referenz darauf zu holen und diese dann an die Unterfunktionen weiterzugeben:

    // ...
    for (unsigned int i = 0; i < mCommandBuffers.size(); i++)
    {
      auto& commandBuffer = mCommandBuffers[i];
    
      BeginCommandBuffer(commandBuffer /*, ...*/);
      // ...
      EndCommandBuffer(commandBuffer /*, ...*/);
    }
    

    (noch besser natürlich, wenn du mind. C++11 verwendest, ranged-based for verwenden)

    Und dann kannst du daraus noch eine eigene Klasse CommandBuffer, so wie ich dich wohl verstanden habe, erzeugen, welcher du dann im Konstruktor den commandBuffer übergibst und die Methoden arbeiten dann mit dieser Referenz (bzw. Zeiger).

    for (unsigned int i = 0; i < mCommandBuffers.size(); i++)
    {
      CommandBuffer commandBuffer(mCommandBuffers[i]); // bzw. &mCommandBuffers[i]
    
      commandBuffer.Begin(/*...*/);
      // ...
      commandBuffer.End(/*...*/);
    }
    

    Meintest du es so?



  • Hallo und vielen Dank für deine Antwort Th69,

    vielen Dank für deine Tipps.
    Am liebsten hätte ich es, dass ich es in der Renderschleife ausführen kann.
    Dabei sollte die Schleife am besten nicht in der RenderSchleife sein, sondern intern, sodass man sich als Programmierer nicht mit der Schleife beschäftigen muss.

    z.B:

    void Render()
    {
         Commandbuffer commandBuffer();
         commandBuffer.Begin(/*...*/)
         commandBuffer.SetViewport(/*...*/)
         commandBuffer.SetScissor(/*...*/)
    
         commandBuffer.BeginRenderPass(/*...*/)
         commandBuffer.BindPipeline(/*...*/);
         commandBuffer.Draw(/*...*/)
         commandBuffer.EndRenderPass(/*...*/);
         commandBuffer.End(/*...*/)
    
         buildCommandBuffer(commandBuffer);
    }
    

    So wie in der Render() Funktion gezeigt stelle ich mir das ungefähr vor. Ich erstelle meine CommandBuffer() und kann damit recht flexibel arbeiten ohne die Schleife die benötigt wird. Da dachte ich, dass ich evtl. das ganze dann in der Funktion buildCommandBuffer(commandBuffer); zusammenbaue. Aber ich weiß nicht ob diese Lösung so gut wäre, weil ich in der Klasse Commandbuffer alle Bindings, Set und Begins irgendwie in einer Struktur zwischenspeichern muss für die Methode buildCommandBuffer welche Strukturen dann in der Schleife ausführt.

    Gruß Dennis



  • Du möchtest also in diesen Methoden nur bestimmte Daten setzen und erst in der buildCommandBuffer-Funktion dann den CommandBuffer daraus erzeugen?
    Sollen die CommandBuffer-Methoden denn völlig frei kombinierbar sein (und ist die Reihenfolge wichtig)?

    Und sollen denn alle mCommandBuffers[i] dieselben Daten erhalten (denn in deinem bisherigen Code sind ja - bis auf vulkanFramebuffer.GetFramebuffers()[i] - keine von i abhängigen Daten enthalten)?
    Weil dann könntest du auch einfach ein Objekt erzeugen und dieses dann X-mal kopieren (und nur den framebuffer unterschiedlich setzen).

    Evtl. könntest du auch die Entwurfsmuster (Design patterns) durchschauen, ob dich etwas design-technisch anspricht (bes. die Erzeugungsmuster und Verhaltensmuster).
    Das Kopieren aus einer Vorlage entspräche dann dem Prototyp (Entwurfsmuster).

    Ich würde aber erst mal nach und nach die Änderungen machen (entsprechend meines ersten Beitrags) und dann mal aus Anwendersicht (deines Codes) schauen, wie flexibel die Klasse CommandBuffer sein muß (nicht, daß du zuviel Overdesigning betreibst und keine Anwendung daraus zum Laufen kriegst ;- )

    PS: Ich habe schon selber einige Framework entwickelt und weiß also, wieviel Zeit man damit aufbringen kann. Und daß man niemals wirklich 100%ig damit designtechnisch zufrieden ist (manchmal muß man, z.B. aus Performancegründen, bestimmte Funktionalitäten anders umsetzen). Aber so etwas treibt einen Software-Entwickler eben immer an. 😉



  • ich hab es nun anders gemacht. Mir ist nach einer Weile aufgefallen, dass ich die Befehle in der Schleife einfach stückeln kann.
    Wundert mich wieso mir das nicht vorher schon aufgefallen ist.
    Nun habe ich eine Klasse CommandBuffer, welche mir die oben genannten Methoden wie z.B. BindPipeline(), Draw() etc. anbietet.
    So sieht meine BindPipeline Methode nun aus.

    void VulkanCommandBuffer::BindPipeline(VulkanPipeline pipeline, VkPipelineBindPoint bindPoint)
    	{
    		if (mCommandBufferIsInitialized && mIsCommandBufferRecording)
    		{
    			for (unsigned int i = 0; i < mCommandBuffers.size(); i++)
    			{
    				vkCmdBindPipeline(mCommandBuffers[i], bindPoint, pipeline.GetPipeline());
    			}
    		}
    		else
    		{
    			throw VulkanException("Bind Pipeline cannot be executed!");
    		}
    	}
    

    Ich hab nun um den Befehl vkCmdBindPipeline(...) die Schleife gepackt, wodurch der Befehl für jeden CommandBuffer einmal ausgeführt wird.
    Und nach meinen ersten Tests funktioniert, das nun auch 🙂
    Dadurch bin ich finde ich ziemlich flexibel. So brauche ich nichtmal die Funktion buildCommandBuffer(...).
    Meine RenderFunktion sieht nun so aus:

    commandBuffer.CreateCommandBuffer(vulkInstance, commandPool, framebuffer.GetFramebuffers().size());
    	commandBuffer.BeginCommandBuffer();
    	commandBuffer.SetViewport(...);
    	commandBuffer.SetScissor(...);
    	commandBuffer.BeginRenderPass(vulkInstance, renderPass, framebuffer);
    	commandBuffer.BindPipeline(pipeline, VK_PIPELINE_BIND_POINT_GRAPHICS);
    	commandBuffer.Draw(3, 1, 0, 0);
    	commandBuffer.EndRenderPass();
    	commandBuffer.EndCommandBuffer();
    

    Da hast du recht, über Design zerbreche ich mir manchmal echt den Kopf. 🙂



  • Ja, so (ähnlich) meinte ich es ja auch mit den einzelnen Methoden.

    Für die CommandBuffer-Methoden könntest du auch ein Fluent Interface anbieten, um die Methoden nicht immer einzeln per Objekt aufrufen zu müssen (also statt void gibst du CommandBuffer& zurück):

    commandBuffer.CreateCommandBuffer(vulkInstance, commandPool, framebuffer.GetFramebuffers().size())
    	.BeginCommandBuffer()
    	.SetViewport(...)
    	.SetScissor(...)
    	.BeginRenderPass(vulkInstance, renderPass, framebuffer)
    	.BindPipeline(pipeline, VK_PIPELINE_BIND_POINT_GRAPHICS)
    	.Draw(3, 1, 0, 0)
    	.EndRenderPass()
    	.EndCommandBuffer();
    


  • Vielen Dank für deinen Hinweis.
    Ich hab es nun implementiert. Nun kann ich die einzelnen Methoden auch direkt hintereinander aufrufen 🙂
    Die Methode BindPipeline zum Beispiel sieht nun so aus.

    VulkanCommandBuffer& VulkanCommandBuffer::BindPipeline(VulkanPipeline pipeline, VkPipelineBindPoint bindPoint)
    	{
    		if (mCommandBufferIsInitialized && mIsCommandBufferRecording)
    		{
    			for (unsigned int i = 0; i < mCommandBuffers.size(); i++)
    			{
    				vkCmdBindPipeline(mCommandBuffers[i], bindPoint, pipeline.GetPipeline());
    			}
    		}
    		else
    		{
    			throw VulkanException("Bind Pipeline cannot be executed!");
    		}
    
    		return *this;
    	}
    


  • 👍

    Und frohe Feiertage!



  • danke, wünsche ich dir auch 🙂



  • Wünsche ebenfalls frohe und besinnliche Feiertage 🎄