在我們完成管線創建之前,我們需要告訴Vulkan渲染將要用到的幀緩衝附件的信息。我們需要明確有多少顏色和深度緩衝,每個又有多少採樣以及它們的內容應該如何通過渲染操作來進行處理。所有這些信息都包裝在渲染通道(render pass)對象中,我們就創建一個新的方法createRenderPass,在initVulkan中調用它,且它在createGraphicsPipeline之前。
我們這裏僅有一個顏色緩衝附件,就是來自交換鏈的一個圖像。
void createRenderPass() {
VkAttachmentDescription colorAttachment = {};
colorAttachment.format = swapChainImageFormat;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
}
顏色附件的format參數應該和交換鏈圖像的格式匹配,且我們目前沒有多重採樣,所以就保持一個採樣。
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
loadOp和storeOp決定了在渲染前後和附件中的數據如何交互。loadOp有如下可選項:
VK_ATTACHMENT_LOAD_OP_LOAD:保存附件當前存在的上下文;
VK_ATTACHMENT_LOAD_OP_CLEAR:開始的時候清除值使其變成一個常數;
VK_ATTACHMENT_LOAD_OP_DONT_CARE:當前存在的上下文是未定義的,且我們也不關心它們。
我們這裏將會在繪製新的幀之前使用清除操作來清除幀緩衝到黑色。storeOp只有兩個可選項:
VK_ATTACHMENT_STORE_OP_STORE:渲染內容將會被存儲到內存且能後續讀出;
VK_ATTACHMENT_STORE_OP_DONT_CARE:渲染操作之後幀緩衝的內容會是未定義的;
我們想要看到屏幕上渲染的三角形,所以我們選用存儲操作:
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
loadOp和storeOp應用到顏色和深度數據,stencilLoadOp和stencilStoreOp應用到模板數據。我們的應用不會對模板緩衝做什麼處理,所以加載和存儲結果是無關的。
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
Vulkan中的材質和幀緩衝通過有特定像素格式的VkImage對象表示,但是內存中像素的佈局可以改變,改變的依據是你要和圖像做什麼操作。
幾個最常用的佈局如下:
VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:圖像用作顏色附件;
VK_IMAGE_LAYOUT_PRESENT_SRC_KHR:圖像呈現到交換鏈;
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:圖像用作內存複製操作的目的地 。
我們當前要知道的是,圖像需要轉移到特定的佈局,該佈局適合我們將要進行的操作。
initialLayout明確了在渲染通道開始之前圖像將會擁有的佈局。finalLayout明確了當渲染通道完成後要自動轉移到的佈局。用VK_IMAGE_LAYOUT_UNDEFINED作爲initialLayout表示我們不關心之前圖像佈局。需要注意到該特殊值的一點是,圖像內容不保證被保留,但是這沒什麼影響,因爲我們反正還是要清除它的。我們想要圖像爲使用交換鏈渲染後的呈現準備就緒,所以用VK_IMAGE_LAYOUT_PRESENT_SRC_KHR作爲finalLayout。
單渲染通道可以由多個子通道組成。子通道是依賴於之前通道的幀緩衝內容的後續渲染操作,例如一個接一個應用的一系列後期處理效果。如果你把這些渲染操作合併成一個渲染通道,那麼Vulkan能夠重新對這些操作排序,並保存內存帶寬以便更好地提升性能。我們的第一個三角形就還是用單個子通道。
每個子通道引用一個或多個附件,這些引用就是些類似下面的VkAttachmentReference結構體:
VkAttachmentReference colorAttachmentRef = {};
colorAttachmentRef.attachment = 0;
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
attachment參數表明通過附件描述數組的索引來確定引用哪一個附件。我們的數組是由單個VkAttachmentDescription組成,因此索引就是0。佈局表明了我們想要附件在使用該引用的子通道的適合用哪個佈局。當子通道開啓的時候,Vulkan將會自動將附件轉移到該佈局。我們打算使用附件來當作一個顏色緩衝,VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL佈局會給我們最好的性能,就和它的名字的意思一樣。
子通道使用VkSubpassDescription結構體來描述:
VkSubpassDescription subpass = {};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
Vulkan將來可能支持計算子通道,所以我們必須顯式說明這個是圖形子通道。下面我們指明到顏色附件的引用:
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;
附件的索引就在片段着色器中用 layout(location = 0) out vec4 outColor直接引用。接着其他類型的可以被子通道引用的附件如下:
pInputAttachments:從着色器讀取的附件;
pResolveAttachments:用於多重採樣顏色附件的附件;
pDepthStencilAttachment:用於深度和模板數據的附件;
pPreserveAttachments:不是給這個子通道用的附件,但是數據又必須保存。
現在附件和引用它的基礎子通道已經都說過了,我們需要創建渲染通道了。創建一個新的類成員來存儲VkRenderPass對象,就放在pipelineLayout變量上邊:
VkRenderPass renderPass;
渲染通道對象就可以根據VkRenderPassCreateInfo結構體信息創建,VkAttachmentReference對象通過數組索引引用附件:
VkRenderPassCreateInfo renderPassInfo = {};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = 1;
renderPassInfo.pAttachments = &colorAttachment;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) {
throw std::runtime_error("failed to create render pass!");
}
如管線佈局一樣,渲染通道也是整個程序生命週期中都被引用的,所以在最後的cleanup中清理,緊跟管線佈局清理之後調用:
vkDestroyRenderPass(device, renderPass, nullptr);
現在我們能把前面章節所有的結構體和對象都組合起來創建圖形管線了!現在回顧下我們都有哪些對象:
着色器階段:着色器模塊定義了圖形管線可編程階段的功能;
固定管線狀態:所有的結構體定義了管線的固定功能階段,例如輸入組裝,光柵器,視口和顏色混合;
管線佈局:由着色器引用的可以在繪製時更新的統一和可壓入的值;
渲染通道:由管線階段引用的附件以及它們的用法。
所有這些組合完整定義了圖形管線的功能,所以我們現在開始填充VkGraphicsPipelineCreateInfo結構體,就在createGraphicsPipeline的末尾處。
VkGraphicsPipelineCreateInfo pipelineInfo = {};
pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO;
pipelineInfo.stageCount = 2;
pipelineInfo.pStages = shaderStages;
上面通過引用VkPipelineShaderStageCreateInfo進行起步,然後我們引用所有的結構體描述固定管線階段:
pipelineInfo.pVertexInputState = &vertexInputInfo;
pipelineInfo.pInputAssemblyState = &inputAssembly;
pipelineInfo.pViewportState = &viewportState;
pipelineInfo.pRasterizationState = &rasterizer;
pipelineInfo.pMultisampleState = &multisampling;
pipelineInfo.pDepthStencilState = nullptr; // optional
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.pDynamicState = nullptr; // optional
之後是管線佈局,它是一個Vulkan句柄而不是一個結構體指針:
pipelineInfo.layout = pipelineLayout;
設置好到渲染通道的引用以及子通道的索引:
pipelineInfo.renderPass = renderPass;
pipelineInfo.subpass = 0;
本管線也可以使用別的渲染通道而不一定是這個特定的實例,但是要和renderPass兼容。兼容的要求是要在這裏描述的,只是本教程不用而已。
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // optional
pipelineInfo.basePipelineIndex = -1; // optional
實際上還有兩個參數,basePipelineHandle和basePipelineIndex。Vulkan允許你通過派生一個已有的管線來創建新的圖形管線。管線派生的想法是因爲如果二者有很多相同的功能,這樣比建立管線更加節省開銷,而且從同一個父對象切換管線也會更快。你可以通過basePipelineHandle指明一個已存在管線的句柄,或者引用另一個管線,也就是要通過basePipelineIndex加上索引來創建的管線。現在只有一個管線,我們就指定一個空句柄和無效的索引。這些值僅僅在VkGraphicsPipelineCreateInfo的flags字段的VK_PIPELINE_CREATE_DERIVATIVE_BIT標記也指定的情況下才有用。
最後一步,通過創建一個類成員來保存VkPipeline對象:
VkPipeline graphicsPipeline;
最終可以創建圖形管線了:
if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) {
throw std::runtime_error("failed to create graphics pipeline!");
}
vkCreateGraphicsPipelines實際有更多的參數,它設計的時候就是接收多個VkGraphicsPipelineCreateInfo對象然後一次調用就會創建多個VkPipeline對象。
第二個參數,我們已經傳了VK_NULL_HANDLE,引用了一個可選的VkPipelineCache對象。管線緩衝可被用於存儲和重用管線創建有關的數據,橫跨多次vkCreateGraphicsPipelines調用,如果存儲到了文件甚至橫跨程序執行。這讓極大提高管線傳將速度成爲可能,以後管線緩衝章節再深究。
圖形管線是所有常用繪製操作都要的,所以它也應該在程序結束的時候清理掉:
vkDestroyPipeline(device, graphicsPipeline, nullptr);
這個就寫在cleanup方法的第一行。現在運行程序,確認所有這些艱苦的工作能夠最終成功創建管線。我們現在離看到屏幕顯示東西已經很近了(其實快一百三十頁了,居然還沒看到三角形),下面的章節會設置來自交換鏈的真正的的幀緩衝並準備繪製命令。