第 9 章. Drawing Objects 繪製對象
第 9 章. Drawing Objects 繪製對象
在上兩章中,我們實現了渲染通道實例並顯示了具有指定背景色的交換鏈圖像。 在前一章中,我們將各種管線狀態與 Render Pass 一起放入圖形管線中。 在本章中,我們將把所有以前的實現結合在一起來構建 Vulkan 中的第一個 Hello World!程序並在顯示輸出中顯示我們的第一個渲染對象。
本章全面介紹了在 Vulkan 中繪製對象的過程;包括記錄和執行繪圖對象的命令緩衝區。 記錄將 Render Pass,framebuffer 和管線與視口和幾何數據相關聯。 命令緩衝區執行涉及在設備隊列中提交命令緩衝區,並將繪製的交換鏈圖像呈現在展示引擎上。 在本章的最後,我們將討論 Vulkan API 中可用的各種同步原語。
在本章中,我們將介紹以下主題:
- Vulkan 中繪圖過程概述
- 準備繪圖對象
- 渲染繪圖對象
- 渲染索引化的幾何圖形
- 了解 Vulkan 中的同步原語
- 調整顯示窗口的大小
Vulkan 中繪圖過程概述
在 Vulkan 中實現繪圖對象很簡單;它由兩個階段組成:準備或構建繪圖對象並對其進行渲染。 前一階段產生命令緩衝區並記錄繪圖命令。 在後一階段,命令緩衝區會執行這些繪圖命令來渲染對象。 我們來仔細看看這些階段:
- 準備繪圖對象:此階段生成繪製對象所需的命令緩衝區:
- 命令緩衝區創建:Command buffer creation,對於每個交換鏈彩色圖像,需要創建一個相應的命令緩衝區對象。 例如,如果交換鏈是雙緩衝區,那麼它將包含兩個彩色圖像。 因此,我們應該創建兩個命令緩衝區對應於每個圖像。
- 命令緩衝區記錄:Command buffer recording,對於創建的命令緩衝區,一次記錄 Render Pass 實例命令的一個子通道,請參閱以下步驟:
- 將創建的 Render Pass 對象和 framebuffer 與展示渲染區域所需的尺寸關聯。
- 指定清除背景色和深度圖的標量值。 我們在第 7 章「緩衝區資源」,「渲染通道」,「幀緩衝區」和「使用 SPIR-V 的著色器」中實現了此步驟。 有關更多信息,請參閱「清除背景色」部分。
- 綁定圖形管線對象 graphics pipeline object。
- 綁定管線使用的資源,包括頂點緩衝區 vertex buffers 和描述符集 descriptor sets。
- 定義視口 viewport 和剪裁區域 scissoring region。
- 繪製對象。
- 渲染繪圖對象:Rendering the drawing object,在這裡,準備階段中創建的命令緩衝區會被多次重用和執行,以便渲染繪圖對象:
- 獲取(對它來說)可用於執行渲染的交換鏈圖像 swapchain image。
- 提交命令緩衝區 command buffer 到渲染對象 render the object。
- 在展示引擎 presentation engine 上顯示渲染的圖形圖像 rendered drawing image。
可繪製對象的準備和渲染是通過 VulkanDrawable 類的 prepare()和 render()函數實現的。
頭文件聲明
在本節中,我們先來看看 VulkanDrawable 的頭文件聲明。 請按照內聯注釋了解每個函數和變數的功能和用途。 在我們繼續閱讀本章時,我們會實現這些函數並對其詳細討論。 以下是新增的成員變數和函數:
class VulkanDrawable
{
public:
// Prepares the drawing object before rendering,
// allocate, create, record command buffer
void prepare();
// Renders the drawing object
void render();
// Initialize the viewport parameters here
void initViewports(VkCommandBuffer* cmd);
// Initialize the scissor parameters here
void initScissors(VkCommandBuffer* cmd);
// Destroy the drawing command buffer object
void destroyCommandBuffer(); private:
// Command buffer for drawing
std::vector<VkCommandBuffer> vecCmdDraw;
// Prepares render pass instance
void recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw);
// Viewport and Scissor variables
VkViewport viewport; VkRect2D scissor;
VkSemaphore presentCompleteSemaphore; VkSemaphore drawingCompleteSemaphore;
};
prepare()函數創建命令緩衝區對象,這些對象會在 render()函數中用於繪製對象。 prepare()函數從命令池(VulkanRenderer :: cmdPool)為命令緩衝區(vecCmdDraw)分配內存並創建命令緩衝區對象。 命令緩衝區的命令記錄在 recordCommandBuffer()函數內部;這會創建 Render Pass 實例和其他一些重要的作業,例如將圖形管線與繪圖對象相關聯,並通過 initViewport()和 initScissor()指定視口和裁剪管理。
render()函數使用準備好的記錄的命令緩衝區,並在可用的交換鏈彩色圖像上渲染繪圖圖形。 一旦在交換鏈的彩色圖像上完成繪圖,就會將其交給展示引擎用於顯示目的。
準備繪圖對象 drawing object
繪圖對象的準備工作在 prepare()函數中實現;第 7 章中已經介紹了該函數 ------「緩衝區資源,渲染通道,幀緩衝區以及使用 SPIR-V 的著色器」。 有關更多信息,請參閱「清除背景色」中的「在 Render Pass 實例中設置背景色」小節。
記錄 Render Pass 命令
Render Pass 實例一次記錄命令的一個子通道。 一個渲染通道可能包含一個或多個子通道。 對於每個子通道,都使用 vkCmdBeginRenderPass()和 vkCmdEndRenderPass()API 記錄這些命令。 這兩個 API 共同定義了一個範圍,在這個範圍內會迭代通過不同的子通道,它們會記錄特定於該子通道的命令。
開始 Render Pass 記錄
vkCmdBeginRenderPass()API 會為給定的子通道開始 Render Pass 實例命令的記錄。 以下是規範:
void vkCmdBeginRenderPass(
VkCommandBuffer commandBuffer,
const VkRenderPassBeginInfo* pRenderPassBegin,
VkSubpassContents contents);
vkCmdBeginRenderPass API 接受三個參數。 VkCommandBuffer 類型的第一個參數 commandBuffer 指示把命令記錄到其中的命令緩衝區。 第二個參數 pRenderPassBegin 是傳遞 Render Pass 元數據到其中的 VkRenderPassBeginInfo 類型的控制結構(更多信息在下一節中介紹)。 最後一個參數是 VkSubpassContents 類型,並指明將內容記錄在子通道執行過程中的什麼位置以及以何種方式存儲。
以下是 VkSubpassContents 的兩種類型:
- K_SUBPASS_CONTENTS_INLINE:在這種類型中,主命令緩衝區直接記錄子通道內容,並且不允許在該子通道內執行輔助命令緩衝區。
- VK_SUBPASS_CONTENTS_SECONDARY_COMMAND:在這裡,輔助命令緩衝區通過主命令緩衝區被調用並負責記錄輔助通道的內容。
提示
主命令緩衝區和輔助命令緩衝區是什麼??primary and secondary command buffers
主命令緩衝區沒有任何父命令緩衝區。 而輔助命令緩衝區總是從主命令緩衝區執行,相當於它的父親。 輔助命令緩衝區不直接被提交到設備隊列中;這些會被記錄到主命令緩衝區中並使用 vkCmdExecuteCommands()API 執行。 void vkCmdExecuteCommands(VkCommandBuffer commandBuffer,uint32_t commandBufferCount,const VkCommandBuffer * pCommandBuffers); 這個 API 有三個參數。 第一個參數 -commandBuffer(類型為 Vk CommandBuffer) - 是主命令緩衝區對象的句柄。 第二個參數 commandBufferCount(類型為 uint32_t) - 表示需要在主命令緩衝區下調用的輔助命令緩衝區的總數。 最後一個參數 -pCommandBuffers(類型為 VkCommandBuffer *) - 指定要傳入的輔助命令緩衝區對象的完整列表。
輔助命令緩衝區如何有用?
輔助命令緩衝區在將常用的操作記錄到模塊單元中時很有用。 這些模塊化的組件可以根據需要附加到任何您想要的主緩衝區。 在沒有輔助命令緩衝區的情況下,這些常用操作會被記錄為主緩衝區的一部分,使它們體積龐大並導致冗餘,造成污染。 Lets take a look at the VkRenderPassBeginInfo structure syntax and its parameters 我們來看看 VkRenderPassBeginInfo 結構的語法及其參數:
typedef struct VkRenderPassBeginInfo {
VkStructureType sType;
const void* pNext;
VkRenderPass renderPass;
VkFramebuffer framebuffer;
VkRect2D renderArea;
uint32_t clearValueCount; const VkClearValue* pClearValues;
} VkRenderPassBeginInfo;
以下是 VkRenderPassBeginInfo 結構的各種欄位:
欄位 | 描述
---|---
sType | 這是這個控制結構的類型信息。 必須將其指定為 VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO。
pNext | 這可能是一個指向擴展特定結構的有效指針,也可能是 NULL。
renderPass | 渲染通道實例使用渲染通道對象來開始記錄。
framebuffer | 渲染通道還與包含顏色和深度附件的 VkFramebuffer 對象關聯。 renderArea | 該欄位表示由於渲染通道執行而受影響的矩形區域。
clearValueCount | 這表示顏色或深度的清除值的數量。
pClearValues | 該欄位包含與幀緩衝區 framebuffer 中指定的附件關聯的清除值。
注意
在 Render Pass 實例中定義 renderArea 時,如果渲染區域小於 framebuffer,則可能會導致性能損失。 在這種情況下,建議保持渲染區域等於幀緩衝區域,或者限定渲染通道 Render Pass 的粒度。 渲染區域的粒度可以使用 vkGetRenderAreaGranularity()API 進行檢查。
void vkGetRenderAreaGranularity( VkDevice device,
VkRenderPass renderPass,
VkExtent2D* pGranularity);
該 API 接受三個參數:第一個參數 device 是與 renderPass 關聯的 VkDevice; 第二個參數是 renderPass 對象;最後一個參數用 pGranularity 檢索粒度大小。
過渡到下一個子通道 Transitioning to the next subpass
在給定的渲染通道實例中,當子通道記錄完成時,應用程序可以使用 vkCmdNextSubpass()API 切換或過渡到下一個子通道。
void vkCmdNextSubpass( VkCommandBuffer commandBuffer, VkSubpassContents contents);
該 API 接受兩個參數,如下表所示:
參數 | 描述
---|---
commandBuffer | 這表示將命令記錄到其中的命令緩衝區。
contents | 這表明內容將在何處以及如何在下一個子通道執行過程中提供。 有關 VkSubpassContents 的更多信息,請參閱上一小節 - 開始 Render Pass 實例。
完成渲染通道 Render Pass 實例的記錄
vkCmdEndRenderPass()API 結束當前正在被執行的子通道的 Render Pass 實例命令緩衝區的記錄。 該 API 使用一個參數來指定必須停止記錄的命令緩衝區的句柄。
void vkCmdEndRenderPass( VkCommandBuffer commandBuffer);
實現
當前的子通道是用純黑色指定的,會用這個值繪製交換鏈圖像,使背景顯示為黑色。 另外,還指定了其他參數,例如渲染通道對象,幀緩衝區和尺寸。 在 Render Pass 實例中有很多其他命令被執行;這將在下一節討論,下面的實現顯示了使用 vkCmdBeginRenderPass()和 vkCmdEndRenderPass()API 的 Render Pass 實例的記錄過程:
void VulkanDrawable::recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw)
{
VulkanDevice* deviceObj= rendererObj->getDevice();
// Specify the clear color value VkClearValue clearValues[2]; clearValues[0].color.float32[0]= 0.0f; clearValues[0].color.float32[1]= 0.0f; clearValues[0].color.float32[2]= 0.0f; clearValues[0].color.float32[3]= 0.0f;
// Specify the depth/stencil clear value clearValues[1].depthStencil.depth = 1.0f; clearValues[1].depthStencil.stencil = 0;
// Define the VkRenderPassBeginInfo control structure VkRenderPassBeginInfo renderPassBegin; renderPassBegin.sType =
VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassBegin.pNext = NULL;
renderPassBegin.renderPass = rendererObj->renderPass;
renderPassBegin.framebuffer = rendererObj-> framebuffers[currentImage];
renderPassBegin.renderArea.offset.x = 0;
renderPassBegin.renderArea.offset.y = 0; renderPassBegin.renderArea.extent.width = rendererObj->width; renderPassBegin.renderArea.extent.height= rendererObj->height; renderPassBegin.clearValueCount = 2;
renderPassBegin.pClearValues = clearValues;
// Start recording the render pass instance
vkCmdBeginRenderPass(*cmdDraw, &renderPassBegin, VK_SUBPASS_CONTENTS_INLINE);
// Execute the commands as per requirement
. . . .
// pipeline bind, geometry, viewport, scissoring
// End of render pass instance recording
vkCmdEndRenderPass(*cmdDraw);
. . . .
}
Binding pipeline object 綁定管線對象
在 Render Pass 實例中,我們需要做的第一件事是使用 vkCmdBindPipeline()API 綁定管線。 此 API 將一個特定的管線(圖形或計算)與使用此命令的當前命令緩衝區進行綁定。
void vkCmdBindPipeline(
VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint, VkPipeline pipeline);
參數 | 描述
---|---
commandBuffer | 這表示將被綁定到管線 pipeline 對象的命令緩衝區對象(用於 Render Pass 記錄操作)。
pipelineBindPoint | 該參數表示管線對象將被綁定到的管線綁定點的類型。 該參數的類型為 VkPipelineBindPoint,可以採用以下兩個給定值之一:
typedef enum VkPipelineBindPoint { VK_PIPELINE_BIND_POINT_GRAPHICS = 0, VK_PIPELINE_BIND_POINT_COMPUTE = 1, } VkPipelineBindPoint;第一個值表示用於圖形管線的綁定點,第二個表示用於計算管線的綁定點。pipeline | 這個參數表示命令緩衝區將被綁定到的管線對象。
每個管線 --- 圖形或計算 --- 對於那些一旦綁定就會受到其影響的命令來說是非常特定的:
- 計算管線:Compute pipeline,當管線綁定到 VK_PIPELINE_BIND_POINT_COMPUTE 時,只能控制 vkCmdDispatch 和 vkCmdDispatchIndirect 的命令行為。 此管線狀態下的其他任何命令都不會受影響。 有關這些命令的更多信息,請參閱官方的 Vulkan 規範文檔 https://www.khronos.org/registry/vulkan/specs/1.0/xhtml/vkspec.html。
- 圖形管線:Graphics pipeline,當管線綁定到 VK_PIPELINE_BIND_POINT_GRAPHICS 時,可以控制 vkCmdDraw,vkCmdDrawIndexed,vkCmdDrawIndirect 和 vkCmdDrawIndexedIndirect 命令。 此管線狀態下的其他任何命令都不會受到影響。
Implementation 實現
以下代碼顯示了圖形管線與現有命令緩衝區對象 cmdDraw 的綁定過程,管線對象 pipeline 與圖形綁定點(VK_PIPELINE_BIND_POINT_GRAPHICS)進行連接:
void VulkanDrawable::recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw){
. . . .
// Bound the command buffer with the graphics pipeline
vkCmdBindPipeline(*cmdDraw, VK_PIPELINE_BIND_POINT_GRAPHICS,
*pipeline);
. . . .
}
注意
有關管線創建和實現中指定的管線對象的更多信息,請參閱第 8 章「管線和管線狀態管理」。
指定繪圖對象的幾何信息 drawing object geometry information
繪圖對象的幾何信息可以使用頂點緩衝區來指定。 請記住,我們在第 7 章「緩衝區資源,渲染通道,幀緩衝區和使用 SPIR-V 的著色器」中構建了緩衝區資源,並將頂點信息存儲在 VulkanDrawble 類的 VertexBuffer.buf 中。
注意
有關頂點緩衝區構建的更多信息和概述,請參閱第 7 章「緩衝區資源,渲染通道,幀緩衝區和使用 SPIR-V 的著色器」中的「了解緩衝區資源和使用緩衝區資源創建幾何圖形」部分。
頂點數據包含以交錯形式存儲的頂點位置和顏色信息。 使用 vkCmdBindVertexBuffers()API 將此頂點數據綁定到圖形管線狀態下的命令緩衝區。 該命令會對每個基本的繪圖操作,將特定的頂點緩衝區綁定到命令緩衝區。
void vkCmdBindVertexBuffers(
VkCommandBuffer commandBuffer,
uint32_t firstBinding,
uint32_t bindingCount,
const VkBuffer* pBuffers,
const VkDeviceSize* pOffsets);
下表列出了各個欄位並指出了每個參數的說明:
參數 | 描述
---|---
commandBuffer | 這是要將 vkCmdBindVertexBuffers 命令記錄到其中的的命令緩衝區對象。
firstBinding | 該參數表示將由命令進行更新的頂點輸入綁定的索引。 bindingCount | 這表示其狀態將由命令進行更新的頂點輸入綁定的數量。 pBuffers | 該參數是傳入的 VkBuffer 頂點緩衝區句柄的數組。
pOffsets | 該參數是頂點緩衝區偏移量的數組。
Implementation 實現
vkCmdBindVertexBuffer()API 會使用必要的信息綁定命令緩衝區對象,從而選取幾何數據。此 API 會獲取我們感興趣的繪圖對象的頂點緩衝區信息,以便在顯示器上進行繪製:
void VulkanDrawable::recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw){
. . . .
// Bound the vertex buffer with the command buffer vkCmdBindVertexBuffers(*cmdDraw, 0, 1, &VertexBuffer.buf, offsets);
. . . .
}
定義動態視口 dynamic viewport
視口確定了繪圖表面區域的部分範圍,繪圖對象圖元會在其上進行渲染。 在第 8 章「管線和管線狀態管理」中,我們在「了解管理狀態對象(PSO)」下的「視口管理」小節中學習了在圖形管線下管理視口的狀態,並創建了管線狀態對象。 視口參數可以進行靜態或動態控制:
- 靜態控制:Statical control,如果禁用了動態狀態 VK_DYNAMIC_STATE_VIEWPORT,則一旦使用 VkPipelineViewportStateCreateInfo 類的成員變數 pViewport 創建視口管線狀態對象,則無法支持更改和指定視口參數。
- 動態控制:Dynamical control,另一方面,當創建管線狀態對象的時候啟用了動態狀態 VK_DYNAMIC_STATE_VIEWPORT 的情況下,就允許在運行時更改視口轉換參數。 這些參數可以在運行時使用 vkCmdSetViewport()API 進行動態控制。 以下是此 API 的語法和描述:
void vkCmdSetViewport( VkCommandBuffer commandBuffer, uint32_t firstViewport,
uint32_t viewportCount, const VkViewport* pViewports);
其中的每個參數的說明如下:
參數 | 描述
---|---
commandBuffer | 此參數指定要用來記錄此命令的命令緩衝區對象。
firstViewport | firstViewport 是一個內部視口數組的索引,指示需要進行更新的第一個視口。
viewportCount | 這些是 pViewport 中視口的總數,pViewport 的參數將由命令進行更新。 pViewports | 這是 VkViewport 結構的指針數組,這個結構指定視口區域的尺寸。
typedef struct VkViewport { float x;
float y;
float width;
float height;
float minDepth;
float maxDepth;
} VkViewport;
其中的欄位 說明如下:
欄位 | 描述
---|---
x, y | 這是視口的左上角(x,y)。
width, height | 這表示視口的寬度和高度。 minDepth,
maxDepth | 這是視口的深度範圍。 minDepth 可能大於或等於 maxDepth。
Implementation 實現
使用 initViewport()函數初始化視口,通過傳遞當前命令緩衝區對象(會把 vkCmdSetViewport()命令記錄到其中)。 vkCmdSetViewport()API 設置視口參數,視口參數是在視口區域中動態設置的,指定可視區域的左上角位置和深度信息:
void VulkanDrawable::recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw){
. . . .
// Define the dynamic viewport here.
initViewports(cmdDraw);
. . . .
}
void VulkanDrawable::initViewports(VkCommandBuffer* cmd)
{
viewport.height = (float)rendererObj->height; viewport.width = (float)rendererObj->width; viewport.minDepth = (float) 0.0f; viewport.maxDepth = (float) 1.0f;
viewport.x = 0;
viewport.y = 0;
vkCmdSetViewport(*cmd, 0, NUMBER_OF_VIEWPORTS, &viewport);
}
Scissoring 裁剪
裁剪器定義了一個矩形區域;位置(x,y)落在該矩形區域之外的所有幀緩衝區的片段都會丟棄。
如果不是在啟用動態狀態 VK_DYNAMIC_STATE_VIEWPORT 的情況下創建這個管線狀態對象,則使用 VkPipelineViewportStateCreateInfo 類的成員變數 pScissors 控制裁剪矩形。 相反,如果在啟用動態狀態 VK_DYNAMIC_STATE_VIEWPORT 的情況下創建這個管線狀態對象,則可以使用 vkCmdSetScissor()API 動態指定和控制裁剪矩形的參數。
與視口參數類似,裁剪器參數也可以進行靜態或動態的控制:
- 靜態控制:Static control,當不打算改變視口的參數時,則表示觀看尺寸是固定的。 這可以通過禁用 VK_DYNAMIC_STATE_VIEWPORT 的方式,指示底層管線按照這樣的形式使用動態狀態。 這種情況下會通知管線視口相關的靜態特性,這可能有利於做出決策並避免視口動態形式所需的多種操作。 對於靜態的視口配置,會從視口管線狀態對象 VkPipelineViewportStateCreateInfo 類的成員變數 pScissors 讀取裁剪參數。
- 動態控制:Dynamical control,另一方面,如果啟用了動態狀態 VK_DYNAMIC_STATE_VIEWPORT,則可以通過稱為 vkCmdSetViewport()的特殊 API 動態更改和指定裁剪參數。 以下是此 API 的語法和描述:
void vkCmdSetScissor( VkCommandBuffer commandBuffer, uint32_t firstScissor,
uint32_t scissorCount, const VkRect2D* pScissors);
其中每個參數的說明如下:
參數 | 描述
---|---
commandBuffer | 此參數指定要用於記錄此命令的命令緩衝區對象。
firstScissor | firstScissor 是一個內部裁剪器數組的索引,表示要更新的第一個裁剪器。
scissorCount | 這是 pScissors 數組中裁剪器的總數,其參數會由命令進行更新。
pScissors | 這是一個指向 VkRect2D 結構的數組的指針,這個結構指定裁剪區域的 2D 尺寸。
實現
通過使用指定非剪輯區域的 initScissors()函數初始化裁剪器。 這個矩形區域外的任何東西都會被丟棄。 vkCmdSetScissor()API 設置了一些裁剪參數,它們是可以動態設置的,指示用於單裁剪或多裁剪的矩形尺寸:
void VulkanDrawable::recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw){
. . . .
// Define the scissor here. initScissors(cmdDraw);
. . . .
}
void VulkanDrawable::initScissors(VkCommandBuffer* cmd)
{
scissor.extent.width = rendererObj->width; scissor.extent.height = rendererObj->height; scissor.offset.x = 0;
scissor.offset.y = 0;
vkCmdSetScissor(*cmd, 0, NUMBER_OF_SCISSORS, &scissor);
}
繪圖命令 Draw command
繪圖命令有助於組裝圖元。 Vulkan 支持基於索引和非索引的繪製命令。 繪圖命令可以通過片段排列的順序影響幀緩衝區。 在使用一個繪圖命令的多個實例的情況下,API 的排序次序用來處理繪圖命令。 對於基於非索引的命令,規則是將較低數量實例的圖元排在靠前的位置,以此類推。 對於基於索引的命令,具有較少數量的頂點索引值的圖元會排在 API 排序中靠前的位置,以此類推。
Vulkan 通過在命令緩衝區中記錄繪圖命令的形式渲染繪圖對象。 在 Vulkan 中有四種不同的繪圖命令,它們大致分為以下兩類:
- 第一種類型(vkCmdDraw 和 vkCmdDrawIndexed)指定命令緩衝區對象本身的繪圖參數。
- 相反,第二種類型(vkCmdDrawIndirect 和 vkCmdDrawIndexedIndirect)使用緩衝區內存從繪圖 API 讀取參數,該參數後綴為 Indirect 關鍵字,並且屬於後者;否則,它就是前者。
vkCmdDraw 命令
vkCmdDraw()API 從命令緩衝區讀取繪圖參數;按照數組的形式,以特定的順序訪問頂點信息,從第一個頂點(firstVertex)開始,直到 vertexCount 指定的頂點總數。 vkCmdDraw API 使用頂點數組的數據信息渲染由管線狀態對象中的輸入裝配狀態指定的圖元。
這個 API 支持實例化,允許多次高效渲染對象而無需調用多個繪製命令。 這些繪圖功能在人群的模擬,樹的渲染,克隆模式等情況下非常有用。 使用 instanceCount 指定繪圖實例的總數,從 firstInstance 指示的第一個實例索引開始計算。
void vkCmdDraw(
VkCommandBuffer commandBuffer, uint32_t vertexCount,
uint32_t instanceCount,
uint32_t firstVertex,
uint32_t firstInstance);
其中每個參數的說明如下:
參數 | 描述
---|---
commandBuffer | 該參數指定執行記錄的命令緩衝區對象(VkCommandBuffer)。 vertexCount | 這是要繪製的頂點的總數。
instanceCount | 該參數表示使用此命令要繪製的實例總數。
firstVertex | 該參數指定為了繪製所有頂點而從中讀取的第一個頂點的索引。
firstInstance | 該參數指定要繪製實例的第一個實例 ID。
本章中的示例使用以下命令:
vkCmdDraw(*cmdDraw, 3, 1, 0, 0);
該命令用到了下面的三角形數據,以交錯的形式表示三個頂點,因此第二個參數指定為 3; 由於只有一個實例,因此第三個參數為 1。繪圖應從第一個頂點開始,第一個頂點由索引 0 指示,作為第四個參數。 最後一個參數是 0,指向第一個實例 ID:
struct VertexWithColor
{
float x, y, z, w; // Vertex Position
float r, g, b, a; // Color format Red, Green, Blue, Alpha
};
static const VertexWithColor triangleData[] =
{
};
注意
{ 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0 },/*Vertex0*/
{ 1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0 },/*Vertex1*/
{-1.0f, -1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0 },/*Vertex2*/
本章中使用的另一種繪圖命令是 vkCmdDrawIndexed()API。 此 API 用來渲染索引化的幾何圖形。 有關詳細信息,請參閱本章末尾處的「渲染索引化的幾何圖形」小節。 但在跳到這節之前,您必須看完所有剩餘的部分,以了解 Vulkan 中繪圖對象的渲染。
實現繪圖對象 drawing object 的準備工作
繪圖對象的準備過程在 VulkanDrawable 類的 recordCommandBuffer 函數中執行。 此函數將每個交換鏈緩衝區的繪圖對象的相關命令記錄在單獨的一個命令緩衝區對象(cmdDraw)中。 該過程包括以下步驟。
使用 vkCmdBeginRenderPass()API 記錄 Render Pass 實例;該調用接受 VkRenderPassBeginInfo 類的對象(renderPassBegin)作為輸入參數,其中包含 Render Pass 和 framebuffer 對象,用於指示與此 Render Pass 實例關聯的附件、子通道以及圖像視圖。 有關渲染通道和幀緩衝區對象的更多信息,請參閱第 7 章,「緩衝區資源,渲染通道,幀緩衝區和使用 SPIR-V 的著色器」。
renderPassBegin 承載的其他信息就是幀緩衝區上繪圖區域的範圍。 此外,顏色,深度或模板圖像的清除值也在裡面設置。 此信息將會使用指定的清除值清除關聯的緩衝區附件。 例如,使用顏色緩衝區附件關聯的清除值清除後就像背景色一樣。
注意
傳遞給 vkCmdBeginRenderPass 的最後一個參數是 VK_SUBPASS_CONTENTS_INLINE; 它表明只有主命令緩衝區應該記錄子通道內容,並且在這個子通道內,不會執行輔助命令。
使用 vkCmdBindPipeline(),並將命令緩衝區與我們在第 8 章「管線和管線狀態管理」中創建的圖形管線對象綁定。
需要動態設置視口和裁剪區域,請使用 initViewports()和 initScissors(),並使用所需的矩形區域大小設置 vkCmdSetViewport()和 vkCmdSetScissor()API。
注意
在運行時使用 vkCmdSetViewport()和 vkCmdSetScissor()API 指定視口和裁剪尺寸時,必須使用 VK_DYNAMIC_STATE_VIEWPORT 啟用動態狀態(VkDynamicState)。
使用非索引化的繪圖 API vkCmdDraw()指定繪圖命令。 第一個參數指定將要在其中記錄繪圖命令的命令緩衝區對象(VkCommandBuffer)。 第二個參數 3 指定了打算繪製的幾何圖形的頂點數量。 第三個參數 1 指定需要一次繪製一個實例。 第四個參數指定要繪製的第一個頂點索引(0); 最後一個參數 0 指定要基於實例進行繪圖的第一個索引,負責控制數據從實例化數組步進的速率。
使用 vkCmdEndRenderPass()完成 Render Pass 實例的記錄操作並將當前命令緩衝區對象傳遞給它。
記錄命令緩衝區的實現如下所示:
void VulkanDrawable::recordCommandBuffer(int currentImage, VkCommandBuffer* cmdDraw)
{
VulkanDevice* deviceObj = rendererObj->getDevice(); VkClearValue clearValues[2];
clearValues[0].color.float32[0] = 0.0f; clearValues[0].color.float32[1] = 0.0f; clearValues[0].color.float32[2] = 0.0f; clearValues[0].color.float32[3] = 0.0f; clearValues[1].depthStencil.depth = 1.0f; clearValues[1].depthStencil.stencil = 0;
VkRenderPassBeginInfo renderPassBegin;
renderPassBegin.sType = VK_STRUCTURE_TYPE_ RENDER_PASS-_BEGIN_INFO;
renderPassBegin.pNext = NULL;
renderPassBegin.renderPass = rendererObj->renderPass;
renderPassBegin.framebuffer = rendererObj-> framebuffers[currentImage];
renderPassBegin.renderArea.offset.x = 0;
renderPassBegin.renderArea.offset.y = 0; renderPassBegin.renderArea.extent.width = rendererObj->width; renderPassBegin.renderArea.extent.height = rendererObj->height; renderPassBegin.clearValueCount = 2;
renderPassBegin.pClearValues = clearValues;
// Start recording the render pass instance vkCmdBeginRenderPass(*cmdDraw, &renderPassBegin, VK_SUBPASS_CONTENTS_INLINE);
// Bound the command buffer with the graphics pipeline
vkCmdBindPipeline(*cmdDraw, VK_PIPELINE_BIND_POINT_GRAPHICS,
*pipeline);
// Bound the command buffer with the graphics pipeline const VkDeviceSize offsets[1] = { 0 }; vkCmdBindVertexBuffers(*cmdDraw, 0, 1, &VertexBuffer.buf, offsets);
// Define the dynamic viewport here
initViewports(cmdDraw);
// Define the scissoring
initScissors(cmdDraw);
// Issue the draw command with 3 vertex, 1 instance starting
// from first vertex
vkCmdDraw(*cmdDraw, 3, 1, 0, 0);
// End of render pass instance recording
vkCmdEndRenderPass(*cmdDraw);
}
渲染繪圖對象 drawing object
一旦 Render Pass 實例準備好並成功記錄在繪圖對象的命令緩衝區中,我們就可以每次重用它來繪製對象了。
繪製一個對象包括三個步驟:首先,我們需要獲取下一個可用交換鏈圖像的索引,圖元將會在其上(交換鏈圖像)進行繪製或者光柵化。 其次,我們需要將命令緩衝區提交給圖形隊列,以在 GPU 上執行記錄的命令;GPU 會執行這些命令並使用這些繪圖命令繪製可用的交換鏈繪圖圖像。 最後,繪製圖像會被移交給展示引擎,它會把輸出渲染到附加的顯示窗口上。 以下小節會詳細介紹這三個步驟。
獲取交換鏈圖像 swapchain image
在執行 Render Pass 實例記錄的命令之前,我們需要獲取一個在其上執行繪圖操作的交換鏈圖像。 為此,我們需要使用 WSI 擴展 vkAcquireNextImageKHR()從系統中查詢可用的交換鏈圖像的索引;這會返回應用程序要渲染的交換鏈圖像的索引。 這個擴展是以 PFN_vkAcquireNextImageKHR 類型的函數指針的形式提供的。
在 API 調用時,會獲取可展示的圖像 ----- 會使用當前的命令緩衝區在其上進行繪製操作,並通知應用程序可展示的目標圖像已經發生了改變。
注意
調用這個 API 時,有多種因素會影響可展示圖像的可用性。 這其中包括展示引擎的實現,如何使用 VkPresentModeKHR,交換鏈中的圖像總數,應用程序在任意給定的時刻擁有的圖像數量,當然還有應用程序的性能。
VkResult vkAcquireNextImageKHR( VkDevice device, VkSwapchainKHR swapchain, uint64_t timeout,
VkSemaphore semaphore,
VkFence fence,
uint32_t* pImageIndex);
其中每個參數的說明如下:
參數 | 描述
---|---
device | 這表示邏輯的 VkDevice,它與交換鏈對象進行了關聯。
swapchain | 這表示要從中獲取繪圖圖像的交換鏈對象(VkSwapchainKHR)。
timeout | 此參數指示 API 是阻塞的還是非阻塞的。 當指定超時(以納秒為單位)時,它指示函數等待或阻塞多長時間(如果沒有圖像可用)。 如果 timeout 為 0,則此 API 不會阻塞並返回成功或失敗錯誤。 此欄位保證 vkAcquireNextImageKHR()API 在請求失敗時不會阻塞系統並在有限的時間後返回所有權。
semaphore | 這是 VkSemaphore 對象,必須是無信號的,並且不應該有任何未完成的信號或等待的掛起操作。 當展示引擎釋放圖像的所有權並且設備可以修改其內容時,它將會是有信號的。 如果不使用這個 API,則可以設置為 VK_NULL_HANDLE。
fence | 這是一個 VkFence 對象,並且必須是無信號的,並且沒有任何待處理的、未完成的信號操作。 當展示引擎釋放圖像的所有權時,它將變成有信號的。 如果不使用這個 API,則可以等於 VK_NULL_HANDLE。 這可以用來測量與顯示速率相匹配的幀的生成工作。
pImageIndex | 這會檢索下一個可呈現圖像的索引。 此索引屬於由 vkGetSwapchainImagesKHR()API 返回的圖像句柄數組中的索引。 如果 API 不返回 VK_SUCCESS 實例,則意味著指向的索引不會被修改。
注意
vkAcquireNextImageKHR()不能同時指定信號量 semaphore 和欄柵 fence VK_NULL_HANDLE; 其中之一必須是一個有效的輸入。
下表指定了 vkAcquireNextImageKHR()API 的返回值,該值取決於超時 timeout 參數:
返回值 | 描述
---|---
VK_SUCCESS | 這意味著它已經成功獲得了可呈現的圖像。 VK_ERROR_SURFACE_LOST_KHR | 這種錯誤在表面不再可用時會出現。 VK_NOT_READY | 這表示沒有圖像可用,並且超時時間為 0。
VK_TIMEOUT | 當超時值是非零平均值(> 0 和<UINT64_MAX)時,表示在允許的持續時間內沒有可顯示的圖像可用。
VK_SUBOPTIMAL_KHR | 在這種情況下,返回的可呈現圖像不再與交換鏈表面屬性相匹配,但仍可以使用。
VK_ERROR_OUT_OF_DATE_KHR | 在這裡,返回的可呈現圖像不再與交換鏈兼容,因此不能再與交換鏈一起使用,用於顯示。 在這種情況下,應用程序必須查詢兼容的表面屬性,並使用所需的表面屬性重新創建交換鏈,以便繼續使用展示功能的服務。
執行繪圖命令緩衝區 drawing command buffer object
執行命令緩衝區對象是通過使用 CommandBufferMgr :: submitCommandBuffer()函數將其提交到圖形隊列中實現的,該函數在內部調用了 vkQueueSubmit()API 提交命令緩衝區。
注意
有關 submitCommandBuffer()API 及其用法的更多信息,請參閱第 5 章,「Vulkan 中的命令緩衝區以及內存管理」。 在本章中,您可以參考「實現命令緩衝區的包裝類」下的「提交命令到隊列」小節。
使用展示引擎 presentation engine 顯示輸出
當繪圖對象的命令緩衝區被執行時,就會使用記錄的命令繪製目標展示圖像。 然後使用 vkQueuePresentKHR()API 將該圖像排入展示引擎,該 API 會將輸出展示圖像渲染在顯示輸出上。
VkResult vkQueuePresentKHR( VkQueue queue,
const VkPresentInfoKHR* pPresentInfo);
vkQueuePresentKHR()API 使用的參數如下所示:
數 | 描述
---|---
queue | 這是一個 VkQueue 對象,能夠展示並具有圖形隊列的功能。 此外,此隊列屬於和圖像交換鏈相同的設備。
pPresentInfo | 這是一個指向 VkPresentInfoKHR 結構的指針,指定展示元數據。
以下是 VkPresentInfoKHR()的語法和描述:
typedef struct VkPresentInfoKHR {
VkStructureType sType;
const void* pNext;
uint32_t waitSemaphoreCount; const VkSemaphore* pWaitSemaphores;
uint32_t swapchainCount; const VkSwapchainKHR* pSwapchains;
const uint32_t* pImageIndices;
VkResult* pResults;
} VkPresentInfoKHR;
其中的欄位說明如下:
欄位 | 描述
---|---
sType | 這指定了這個結構的類型;這必須是 VK_STRUCTURE_TYPE_PRESENT_INFO_KHR。
pNext | 這可能是一個指向擴展特定結構的有效指針,也可能是 NULL。 waitSemaphoreCount | 該欄位指示顯示引擎在顯示圖像之前應等待的信號量計數。
pWaitSemaphores | 這指定了在發出顯示請求之前要等待的信號量。 這是一個大小等於 waitSemaphoreCount 的非空 VkSemaphore 對象數組。
swapchainCount | 可以有多個用於展示的交換鏈;該欄位表示要使用此 API 命令,被呈現的交換鏈數量。
pSwapchains | 這表示大小等於 swapchainCount 項的 VkSwapchainKHR 對象數組。
pImageIndices | 這是每個交換鏈的可呈現圖像的索引數組,其中總計條目由 swapchainCount 指定。 索引數組中的每個條目都指示在展示中要使用的可呈現圖像。
pResults | 該欄位(如果非 NULL)返回每個可呈現圖像的狀態(VkResult 類型); 條目數等於 swapchainCount。 如果應用程序不需要每個交換鏈的返回結果,則可以將 pResults 設置為 NULL。
注意
vkQueuePresentKHR 能夠呈現來自對應交換鏈的多個可呈現圖像。 它將圖像的所有權(由 pImageIndices 指示;請參考以下 VkPresentInfoKHR)釋放給展示引擎。 在應用程序使用 vkAcquireNextImageKHR()API 重新獲得對它們的控制權之前(並且必須等到返回的信號量獲得信號通知,即有信號,或者 fence 完成以後),這些可呈現的圖像還不能被再次使用。
發送到隊列的所有展示圖像總是按順序進行處理;將展示圖像的所有權轉移給展示引擎會發生在隊列中的提交操作過程中。 這些可呈現的圖像僅在被提交的信號量獲得信號(有信號)時才執行,表示先前的渲染操作都已經完成。 展示的時間是非常特定於具體實現的, 很可能會受到呈現引擎以及正在使用的本地平台的語義的影響。
下表指定了 vkQueuePresentKHR()API 的返回值:
參數 | 描述
---|---
VK_SUCCESS | 這意味著它已經成功獲得了可展示的圖像。
VK_ERROR_SURFACE_LOST_KHR | 這會在表面不再可用時出現這個返回值。 VK_SUBOPTIMAL_KHR | 在這種情況下,返回的可呈現圖像不再與交換鏈表面屬性匹配,但仍可成功用於繪圖目的。
VK_ERROR_OUT_OF_DATE_KHR | 這裡,返回的可呈現圖像不再與交換鏈兼容,因此不能再與交換鏈一起使用,進行展示操作。 在這種情況下,應用程序必須查詢兼容的表面屬性並使用所需的表面屬性重新創建交換鏈,以便繼續使用展示服務。
當可展示的圖像被指定給展示引擎時,展示操作不會更改該圖像的內容。 如果使用 vkAcquireNextImageKHR()再次重新獲取該圖像,並且將轉換從 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR 布局中移除,則其內容與轉換前的內容保持一致,不會發生變化。 相比之下,如果 Vulkan 以外的某些其他機制修改了平台窗口,則交換鏈中所有可呈現圖像的內容將變成未定義的,具有不確定性。
實現繪圖對象渲染 drawing object rendering
讓我們來了解一下實現的渲染代碼。 首先,使用 vkAcquireNextImageKHR()API 擴展獲取可展示的圖像索引;該索引是在 currentColorImage 變數中返回的。 交換鏈擴展存儲為 VulkanRender 類中的一個函數指針(fpAcquireNextImageKHR)。
注意
有關查詢交換鏈擴展的更多信息,請參閱第 6 章「分配映像資源並使用 WSI 構建交換鏈」中的「查詢交換鏈擴展」小節。
您必須至少提供一個同步對象(信號量或欄柵); 否則,您會不知道何時才可以使用該圖像。 例如,當您嘗試獲取圖片時,展示引擎仍然會讀取您的圖片信息。 vkAcquireNextImageKHR()一旦識別出必須提供給你下一個圖像的時候,就允許返回這個 API,而不是該圖像實際可用時才返回。 出於這個原因,同步在這一步是非常重要的。 Vulkan 提供了兩種同步交換鏈圖像的方法,即使用信號量和欄柵。 當使用信號量和欄柵時,它們能夠確保在獲取圖像時,這個圖像沒有以前的待處理操作(例如展示引擎還在讀取這個圖像)。
在這個例子中,我們使用一個信號量對象(presentCompleteSemaphore)進行同步;該對象被傳遞到 vkAcquireNextImageKHR(),以便與圖像相關聯,並且在能夠渲染圖像時該信號量就會獲得信號通知,處於有信號的狀態。
使用檢索到的圖像索引(currentColorImage),並從 vector vecCmdDraw 中獲取相應的命令緩衝區。 創建 VkSubmitInfo 控制結構並指定創建信號量對象(presentCompleteSemaphore)以提交命令緩衝區。 提交後,命令只會在信號量獲得信號(有信號狀態)時才會開始執行;換句話說,圖像已準備好用繪圖命令進行繪製了。
作為最後一種方法,使用 fpQueuePresentKHR API(vkQueuePresentKHR)把繪製的圖像在展示引擎中排隊,用於顯示目的,將所有權轉移給展示引擎。 確保顯示引擎使用圖像時,自上次命令緩衝區提交以來,該圖像未被正在繪製或有任何掛起的操作,這一點非常重要。 這可以使用名為 drawingCompleteSemaphore 的另一個信號量對象簡單地進行檢查;將命令緩衝區提交到隊列之前,這個對象會被傳入 VkSubmitInfo 類 pSignalSemaphores 成員中。 在成功處理好了命令緩衝區時,這個信號量也就有信號了(有信號狀態),消除了與展示引擎所有權重疊的機會,消除了衝突。 一旦顯示了可展示的圖像,展示引擎就會放棄所有權。 vkAcquireNextImageKHR()可以再次查詢相同的圖像並獲得所有權。
以下是演示 Vulkan 中對象渲染的實現代碼:
void VulkanDrawable::render()
{
VulkanDevice* deviceObj = rendererObj->getDevice(); VulkanSwapChain* swapChainObj= rendererObj->getSwapChain(); uint32_t¤tColorImage = swapChainObj->
scPublicVars.currentColorBuffer; VkSwapchainKHR& swapChain = swapChainObj->
scPublicVars.swapChain;
VkFence nullFence = VK_NULL_HANDLE;
// Get the index of the next available swapchain image: VkResult result = swapChainObj->fpAcquireNextImageKHR( deviceObj->device, swapChain,UINT64_MAX, presentCompleteSemaphore, VK_NULL_HANDLE, ¤tColorImage);
VkPipelineStageFlags submitPipelineStages = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
// Prepare the submit into control structure
VkSubmitInfo submitInfo = {};
submitInfo.sType= VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.pNext= NULL; submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores= & presentCompleteSemaphore; submitInfo.pWaitDstStageMask= &submitPipelineStages; submitInfo.commandBufferCount= (uint32_t)sizeof(&vecCmdDraw [currentColorImage]) / sizeof(VkCommandBuffer); submitInfo.pCommandBuffers = &vecCmdDraw[currentColorImage]; submitInfo.signalSemaphoreCount= 1;
submitInfo.pSignalSemaphores = & drawingCompleteSemaphore;
// Queue the command buffer for execution CommandBufferMgr::submitCommandBuffer(deviceObj->queue, &cmdDraw[currentColorImage],&submitInfo);
// Present the image in the window
VkPresentInfoKHR present;
present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; present.pNext = NULL;
present.swapchainCount= 1; present.pSwapchains= &swapChain; present.pImageIndices= ¤tColorImage;
present.pWaitSemaphores= & drawingCompleteSemaphore; present.waitSemaphoreCount= 1;
present.pResults= NULL;
// Queue the image for presentation, result = swapChainObj->fpQueuePresentKHR (deviceObj->queue, & present); assert(result == VK_SUCCESS);
}
信號量對象是在 drawable 類的構造函數中創建的,並在整個應用程序中重用,如下所示。
VulkanDrawable::VulkanDrawable(VulkanRenderer* parent) { memset(&VertexBuffer, 0, sizeof(VertexBuffer)); rendererObj = parent;
// Prepare the semaphore create info data structure VkSemaphoreCreateInfo presentCompleteSemaphoreCreateInfo;
presentCompleteSemaphoreCreateInfo.sType =
VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
presentCompleteSemaphoreCreateInfo.pNext = NULL; presentCompleteSemaphoreCreateInfo.flags = 0;
VkSemaphoreCreateInfo drawingCompleteSemaphoreCreateInfo; drawingCompleteSemaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
drawingCompleteSemaphoreCreateInfo.pNext = NULL; drawingCompleteSemaphoreCreateInfo.flags = 0;
VulkanDevice* deviceObj = VulkanApplication::GetInstance()-> deviceObj;
vkCreateSemaphore(deviceObj->device, & presentCompleteSemaphoreCreateInfo, NULL, & presentCompleteSemaphore);
vkCreateSemaphore(deviceObj->device, & drawingCompleteSemaphoreCreateInfo, NULL,
& drawingCompleteSemaphore);
}
在析構過程中這些信號量對象可以使用用戶定義的 destroySynchronizationObjects()函數進行銷毀:
void VulkanDrawable::destroySynchronizationObjects()
{
VulkanApplication* appObj = VulkanApplication::GetInstance(); VulkanDevice* deviceObj = appObj->deviceObj;
vkDestroySemaphore(deviceObj->device, presentCompleteSemaphore, NULL); vkDestroySemaphore(deviceObj->device, drawingCompleteSemaphore, NULL);
}
以下是該程序的輸出:
渲染索引化的幾何圖形 ndexed geometry
在本節中,您將學習使用 vkCmdDrawIndexed()繪圖命令, 該命令用於繪製索引化的幾何圖形。vkCmdDrawIndexed()API 是索引緩衝區的繪圖命令。 在索引緩衝區中,每個頂點都使用索引號表示。 這種代表網格 mesh 數據的方式需要較少的內存和存儲空間來表示網格 mesh 擁有共享頂點時(比如閉合的形狀)互相連接的 mesh 。
例如,使用兩個三角形渲染的正方形幾何圖形共享兩個公共頂點,如以下示例中所示;如你所見,第一個和第三個頂點重複:
static const VertexWithColor squareData[] =
{
{ -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0 },
{ 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0 },
{ 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0 },
{ -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0 },
};
uint16_t squareIndices[] = { 0,3,1, 3,2,1 }; // 6 indices
在本節中,我們將使用這個幾何圖形的數據和索引來演示如何使用 vkCmdDrawIndexed()API。 以下是它的語法:
void vkCmdDrawIndexed(
VkCommandBuffer commandBuffer, uint32_t indexCount,
uint32_t instanceCount,
uint32_t firstIndex,
int32_t vertexOffset,
uint32_t firstInstance);
我們來看看這個 API 中使用的不同參數及其各自的描述:
參數 | 描述
---|---
commandBuffer | 該參數指定執行記錄的命令緩衝區對象(VkCommandBuffer)。 indexCount | 這是要繪製的緩衝區資源的索引列表中索引元素的總數。
instanceCount | 該參數表示使用此命令要繪製的實例總數。
firstIndex | 該參數指定讀取索引中的第一個索引(索引序列 indices 中的)。
vertexOffset | 此參數指定要被添加到頂點索引以產生結果索引的偏移值。 這個計算出來的索引會被用來從頂點緩衝區讀取頂點信息。
firstInstance | 該參數指定要繪製實例中的第一個實例 ID。
以下是渲染索引化的繪圖的步驟:
- 使用 squareData 和 squareIndices,創建一個緩衝區資源(VkBuffer)。 將 VkBuffer 的句柄存儲在 VertexBuffer :: buf 和 VertexIndex :: idx 中。 有關更多信息,請參閱本章提供的「索引化的繪圖」示例。
- 使用 vkCmdBindVertexBuffers()綁定頂點緩衝區並將 VertexBuffer.buf 傳遞給它。
- 類似地,使用 vkCmdBindIndexBuffer()API 命令綁定索引緩衝區(VertexIndex.idx)。
- 使用 vkCmdDrawIndexed()繪製對象。
vkCmdDrawIndexed()與 vkCmdBindIndexBuffer()API 結合使用。 與綁定頂點緩衝區的 vkCmdBindVertexBuffers()類似,該命令會綁定索引緩衝區。 以下是此 API 的語法;有關 vkCmdBindVertexBuffers()API 的更多信息,請參閱「指定繪圖對象幾何信息」部分。
void vkCmdBindIndexBuffer(
VkCommandBuffer commandBuffer,
VkBuffer buffer,
VkDeviceSize offset,
VkIndexType indexType);
我們來看看這個 API 中使用的不同參數及其各自的描述:
參數 | 描述
---|---
commandBuffer | 這指定要記錄此命令 -vkCmdBindIndexBuffer()的命令緩衝區對象。 buffer | 這表示要綁定到此 API 的索引緩衝區(VkBuffer)的句柄。
offset | 這是索引緩衝區中以位元組為單位指定的起始偏移量,會用於索引緩衝區的地址計算。 indexType | 該參數指示索引是 16 位還是 32 位寬。 這必須是 VkIndexType 類型之一:
Typedef enum VkIndexType {VK_INDEX_TYPE_UINT16 = 0,
VK_INDEX_TYPE_UINT32 = 1,
} VkIndexType;
與 createVertexBuffer()函數類似,我們創建了一個名為 createIndexBuffer()的新函數;它創建索引緩衝區並將索引緩衝區句柄存儲在 VertexIndex.idx 中。 有關此函數的詳細實現,請參閱隨附的源代碼。 createIndexBuffer()的實現與 createVertexBuffer()非常相似;有關該函數實現的詳細信息,請參閱第 7 章「緩衝區資源,渲染通道,幀緩衝區以及使用 SPIR-V 的著色器」中的「實現緩衝區資源 - 為幾何圖形創建緩衝區資源 」。
以下代碼演示了索引化的幾何對象的渲染過程:
// Local data structure in VulkanDrawable class
// for storing vertex buffer and index buffer metadata
struct {
VkBuffer buf; VkDeviceMemory mem;
VkDescriptorBufferInfo bufferInfo;
} VertexBuffer;
struct {
VkBuffer idx; VkDeviceMemory mem;
VkDescriptorBufferInfo bufferInfo;
} VertexIndex;
// Create the VkBuffer and store the handle in
// VertexBuffer.buf and VertexIndex.idx
. . . .
// Bind the vertex buffer
const VkDeviceSize offsets[1] = { 0 }; vkCmdBindVertexBuffers(*cmdDraw, 0, 1,&VertexBuffer.buf,
offsets);
// Bind the Index buffer
vkCmdBindIndexBuffer(*cmdDraw, VertexIndex.idx,
0, VK_INDEX_TYPE_UINT16);
// Draw the object
vkCmdDrawIndexed(*cmdDraw, 6, 1, 0, 0, 0);
上述幾何數據的輸出效果顯示如下: 有關詳細代碼,請參閱本章中的「索引化的繪圖」示例:

vkCmdDrawIndirect 和 vkCmdDrawIndexedIndirect 繪圖命令與 vkCmdDraw 和 vkCmdDrawIndexed 非常相似,只是這裡的參數是從緩衝區內存中讀取的。 有關這些 API 的更多信息,請參閱官方的 Vulkan 規範。
理解 Vulkan 中的同步原語 synchronization primitives
同步是將順序和約束引入非同步系統的關鍵。 它不僅提高了資源利用率,而且還通過減少 CPU 和 GPU 空閑時間從並行性中受益。
Vulkan 為並發執行提供以下四種類型的同步原語:
- 柵欄:Fences,提供主機和設備之間的同步
- 信號量:Semaphores,在隊列之間以及隊列內部進行同步
- 事件:Events,對隊列提交操作之間提供同步
- 障礙:Barriers,在一個命令緩衝區內的命令之間進行同步
在本節中,我們將學習同步原語並理解他們的 API 規範。 我們在本章中實現的繪圖對象示例使用信號量來同步交換鏈圖像的寫入操作。 在下一章中,我們將學習繪製紋理並實現欄柵,以便同步主機和設備。
Fences 欄柵
當主機在隊列中提交命令時,會對主機進行調度,以便進行設備的處理。 有時主機可能需要知道 GPU 上命令執行的狀態,以便控制下一個批處理的執行,以此來確保它不會與前一批命令重疊,這可能會產生未定義的結果或導致資源訪問非法的情況 。
欄柵提供主機和 GPU 之間的同步;使用欄柵,應用程序指示主機等待某個提交的操作完成。 這樣,就可以防止 GPU 將更多操作堆積到命令隊列中:
創建欄柵 fence 對象:可以使用 vkCreateFence()API 創建欄柵對象。
VkResult vkCreateFence(
VkDevice device,
const VkFenceCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkFence* pFence);
我們來看看這個 API 中使用的不同參數及其各自的描述:
參數 | 描述 -
--|---
device | 這是邏輯設備對象,用於創建 fence 對象。
pCreateInfo | 這是一個指向 VkFenceCreateInfo 控制結構數組的指針。
pAllocator | 這個參數控制了主機內存的分配。 有關更多信息,請參閱第 5 章「Vulkan 中的命令緩衝區以及內存管理」中的「主機內存」。
pFence | 這是創建的欄柵對象的句柄。
vkCreateFence()API 採用包含了元數據的 VkFenceCreateInfo 結構作為參數,該元數據用於控制 fence 對象的創建。 以下是這個控制結構的語法:
typedef struct VkFenceCreateInfo {
VkStructureType sType;
const void* pNext;
VkFenceCreateFlags flags;
} VkFenceCreateInfo;
它有三個欄位:第一個欄位 sType 表示結構的類型信息,必須是 VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; 第二個欄位 pNext 未被使用且必須為 NULL; 最後一個參數是 VkFenceCreateFlagBits(請參閱下面的代碼片段),表示創建的欄柵對象是處於有信號還是無信號狀態。 信號狀態使用 VK_FENCE_CREATE_SIGNALED_BIT 指定。
typedef enum VkFenceCreateFlagBits {
VK_FENCE_CREATE_SIGNALED_BIT = 0x00000001,
} VkFenceCreateFlagBits;
等待柵欄 fence 對象:一旦創建了一個有效的 fence 對象,主機就可以將其注入到命令中並使用 vkWaitForFences()API 等待 fence 對象,直到設備處理不對其進行處理。 設備在處理完相關命令後立即給 fence 對象發送信號,這樣就會允許主機解鎖等待狀態。 以下是 API 的語法:
VkResult vkWaitForFences(
VkDevice device,
uint32_t fenceCount,
const VkFence* pFences,
VkBool32 waitAll,
uint64_t timeout);
vkWaitForFences API 採用以下參數:
參數 | 描述
---|---
device | 這是要用於銷毀欄柵對象的邏輯設備對象(VkDevice)。
fenceCount | 這是需要銷毀的欄柵對象的數量。
PFences | 這是一個需要銷毀的欄柵對象句柄數組。 數組大小必須等於 fenceCount。 waitAll | 阻塞狀態可以通過使用此布爾標誌解除阻塞。 當這個標誌值是:
VK_TRUE:表示所有的 pFences 都必須是有信號的,以便成功解除等待。VK_FALSE:pFences 數組中的至少一個 fence 對象有信號,才能成功解除等待狀態。 timeOut | 這是指定的超時期限(以納秒為單位),如果欄柵對象從未獲得信號,則超時期限會用於解鎖等待狀態。 該參數保證系統不會陷入無限的阻塞狀態,從而導致應用程序停止。銷毀欄柵 fence 對象:欄柵一旦被使用且不再需要的時候,就可以使用 vkDestroyFence()API 銷毀欄柵對象;這個 API 有三個參數 - 第一個參數 device 是用來銷毀 fence 對象的邏輯設備,它由第二個名為 fence 的參數表示;最後一個參數(pAllocator)管理主機內存的釋放:
void vkDestroyFence(
VkDevice device,
VkFence fence,
const VkAllocationCallbacks* pAllocator);
重置柵欄 fence 對象:應用程序還可以保留創建的 fence 對象,並通過使用 vkResetFences()重置它們來對其進行重用。 該 API 有三個輸入參數:第一個參數表示要用來重置給定欄柵對象的邏輯設備。 被稱為 fenceCount 的第二個參數指示需要重置的 fence 對象的數量。 最後一個參數 pFences 是要由該 API 進行重置的欄柵對象數組的指針。 以下是此 API 的語法:
VkResult vkResetFences(
VkDevice device,
uint32_t fenceCount,
const VkFence* pFences);
讓我們轉到下一個同步原語:信號量 semaphores。
Semaphores 信號量
信號量提供了在隊列級別實現同步的靈活性;它們用於同步一個或多個隊列。 信號量有兩種狀態:有信號和無信號。 有信號的信號量在隊列提交命令 vkQueueSubmit()中指定;它會阻塞批處理的其餘部分,直到設備獲得信號量的信號。 創建的信號量在多個隊列中可見。 如果兩個或多個隊列提交命令在同一信號量上等待,則只有一個會收到有信號狀態;其他隊列可能會繼續等待,以便確保原子性。
創建信號量 semaphore 對象:使用 vkCreateSemaphore()API 創建信號量;以下是此 API 的語法:
VkResult vkCreateSemaphore(
VkDevice device,
const VkSemaphoreCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkSemaphore* pSemaphore);
vkCreateSemaphore API 採用以下參數:
參數 | 描述
---|---
device | 這是邏輯設備對象,用於創建信號量對象。
pCreateInfo | 這是一個指向 VkSemaphoreCreateInfo 控制結構的數組的指針。
pAllocator | 這個參數控制主機內存的分配。 有關更多信息,請參閱第 5 章「Vulkan 中的命令緩衝區以及內存管理」中的「主機內存」部分。
pSemaphore | 這是創建的信號量對象的句柄。
VkSemaphoreCreateInfo 類型結構有三個參數: 第一個參數 sType 表示該控制結構的類型信息;第二個參數是 pNext,它可能是一個指向擴展特定結構的有效指針,或者可能為 NULL。 最後一個參數是一個標誌值(flags),目前未被使用,並為將來的目的而保留。 以下是這個結構的語法:
typedef struct VkSemaphoreCreateInfo {
VkStructureType sType;
const void* pNext;
VkSemaphoreCreateFlags flags;
} VkSemaphoreCreateInfo;
銷毀信號量 semaphore:使用以下語法中聲明的 vkDestroySemaphore()銷毀創建的信號量。 這個 API 有三個參數: 第一個參數 device 是要銷毀信號量對象的邏輯設備,第二個參數(信號量 semaphore)指定的是要銷毀的信號量對象。 最後一個參數(pAllocator)管理主機內存的釋放:
void vkDestroySemaphore(
VkDevice device,
VkSemaphore semaphore,
const VkAllocationCallbacks* pAllocator);
注意
在本章中,我們使用信號量來確保給定的交換鏈圖像只在展示引擎沒有讀取它時才可以使用,換句話說,當展示引擎讀完交換鏈圖像並準備好渲染它時才能夠使用指定的交換鏈圖像。 為此,我們創建了一個信號量對象並將其傳遞給 vkAcquireNextImageKHR(); 此 API 在圖像準備好渲染時會向信號量發送信號。 這個有信號的信號量接下來通過 VkSubmitInfo 控制結構傳遞給 vkQueueSubmit(); 這確保了繪圖命令只能在未使用時才會被繪製到展示圖像上。 vkQueueSubmit()會取消信號量的信號,對下一個要執行的命令(vkQueuePresentKHR)解除阻塞;該命令會將圖像渲染到輸出顯示。
事件 Events
事件控制著細粒度的同步,並且可以以有信號和無信號兩種狀態存在。 它允許在單個命令緩衝區或提交給隊列的多個命令緩衝區序列內進行同步操作。 主機和設備都可以發信號或重置事件。 同樣,兩者都可以等待事件對象;然而,設備只允許在管線內的某個特定管線階段等待。 隨著我們不斷介紹 API 規範,您將會學到更多內容:
創建事件 event 對象:可以使用 vkCreateEvent()API 創建事件。 該 API 接受三個參數;語法如下:
VkResult vkCreateEvent(
VkDevice device,
const VkEventCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkEvent* pEvent);
vkCreateEvent API 接受四個參數,如下表所述:
參數 | 描述
---|---
device | 這是邏輯設備對象,用於創建事件對象。
pCreateInfo | 這是一個指向 VkEventCreateInfo 控制結構數組的指針。
pAllocator | 這個參數控制主機內存的分配。 有關更多信息,請參閱第 5 章「Vulkan 中的命令緩衝區以及內存管理」中的「主機內存」部分。
pSemaphore | 這是創建的事件對象的句柄。
VkEventCreateInfo 結構有三個參數:第一個參數(sType)描述創建的這個信息數據結構的類型信息;它必須是 VK_STRUCTURE_TYPE_EVENT_CREATE_INFO。 第二個參數是 pNext; 這可能是一個指向擴展特定結構的有效指針,也可能是 NULL。 最後一個參數是標誌值(flags),該標誌值當前未被使用並保留用於將來目的。 以下是這個結構的語法:
typedef struct VkEventCreateInfo {
VkStructureType sType;
const void* pNext;
VkEventCreateFlags flags;
} VkEventCreateInfo;
銷毀事件 event 對象:當事件不再使用時,使用 vkDestroyEvent()銷毀創建的事件。 此 API 有三個參數 :第一個參數 device 是要銷毀事件對象的邏輯設備,第二個參數(信號量 semaphore)指定了要銷毀的事件對象。 最後一個參數(pAllocator)管理主機內存的釋放。
void vkDestroyEvent(
VkDevice device,
VkEvent event,
const VkAllocationCallbacks* pAllocator);
查詢事件 event 狀態:可以查詢事件,以檢查它是處於有信號狀態還是無信號狀態。 這是通過使用 vkGetEventStatus()API 完成的;該 API 的第一個參數(device)是擁有事件對象的邏輯設備(VkDevice) ,第二個參數(event)是查詢其狀態的事件句柄。 以下是此 API 的語法:
VkResult vkGetEventStatus(
VkDevice device,
VkEvent event);
該 API 會返回 VK_EVENT_SET,表示事件是有信號的;對於無信號的事件,會返回 VK_EVENT_RESET。
設置和重置事件 events:可以使用 vkSetEvent()和 vkResetEvent()設置事件。 這兩個 API 都接受與上述 vkGetEventStatus()API 相同的輸入參數;有關更多信息,請參閱 vkSetEvent 和 vkResetEvent 語法:
vkSetEvent API | vkResetEvent API
---|---
VkResult vkCmdSetEvent( VkDevice device, VkEvent event);| VkResult vkSetEvent( VkDevice device, VkEvent event);
從設備發出有信號和無信號事件 觸發事件 重置事件:Signaling and unsignaling an event from a device,可以使用命令緩衝區更新事件,用於對設備進行設置或重置。 vkCmdSetEvent()和 vkCmdResetEvent()API 分別用於設置事件的信號和取消事件的信號;以下是這些 API 的語法:
vkCmdSetEvent API
VkResult vkCmdSetEvent(
VkCommandBuffer
commandBuffer,
VkEvent
event,
VkPipelineStageFlags
stageMask);
vkCmdResetEvent API
VkResult vkCmdResetEvent(
VkCommandBuffer
commandBuffer,
VkEvent
event,
VkPipelineStageFlags
stageMask);
兩個 API 都接受以下三個參數:第一個參數(commandBuffer)指定要在其中記錄命令的命令緩衝區。 第二個參數(事件)指示事件對象的句柄,該事件對象需要被設置了信號或者取消了信號。 最後一個參數(stageMask)是 VkPipelineStageFlags 管線階段,指示更新事件狀態的時間點。
等待事件對象 event objects:可以使用 vkCmdWaitEvents()API 等待一個或多個事件對象,直到它們是有信號的。 以下是此 API 的語法:
void vkCmdWaitEvents(
VkCommandBuffer commandBuffer,
uint32_t eventCount,
const VkEvent* pEvents,
VkPipelineStageFlags srcStageMask,
VkPipelineStageFlags dstStageMask,
uint32_t memoryBarrierCount, const VkMemoryBarrier* pMemoryBarriers,
uint32_t bufferMemoryBarrierCount, const VkBufferMemoryBarrier* pBufferMemoryBarriers, uint32_t imageMemoryBarrierCount, const VkImageMemoryBarrier* pImageMemoryBarriers);
其中每個參數的說明如下:
參數 | 描述
---|---
commandBuffer | 這是捕獲或記錄命令到其中的命令緩衝區對象。
eventCount | 這是要等待的事件對象的數量。
pEvents | 這是 VkEvent 對象的數組;數組的大小必須等於 eventCount。
srcStageMask | 這是指定管線階段的位掩碼欄位,該階段會設置 pEvents 數組中指定的事件對象的信號。
dstStageMask | 這是指定管線階段的位掩碼欄位,在該階段應執行等待。 memoryBarrierCount | 這是指內存屏障(柵欄 barriers)的數量。
pMemoryBarriers | 這是 VkBufferMemoryBarreir 對象數組,其元素個數等於 memoryBarrierCount。
bufferMemoryBarrierCount | 這是指緩衝區內存屏障(欄柵 barriers)的數量。
pBufferMemoryBarriers | 這指的是元素個數等於 bufferMemoryBarrierCount 的 VkMemoryBarreir 對象數組。
imageMemoryBarrierCount | 這是指圖像類型的內存屏障(柵欄 barriers)的數量。
pImageMemoryBarriers | 這指的是元素個數等於 imageMemoryBarrierCount 的 VkImageMemoryBarrier 對象數組。
注意
在第 6 章「分配圖像資源以及使用 WSI 構建交換鏈」中已經討論並實現了屏障 Barrier 。 有關更多信息,請參閱「使用內存屏障的圖像布局轉換」部分。
調整顯示窗口 display window 的大小
當顯示窗口調整大小時,Vulkan 應用程序會獲得新的窗口尺寸以重新繪製圖形圖像。 在 Windows 平台上,可以使用關聯的窗口過程的 WM_SIZE 消息來指示尺寸大小的更改,如以下高亮顯示的代碼所示。 新添到 WndProc()函數所做的更改以粗體突出顯示;使用 setSwapChainExtent()函數將新的尺寸大小更新到 VulkanSwapChain 類,該函數稍後會用於重新創建新的交換鏈圖像,使用指示的尺寸:
LRESULT CALLBACK VulkanRenderer::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
VulkanApplication* appObj = VulkanApplication::GetInstance(); switch (uMsg)
{
case WM_CLOSE:
PostQuitMessage(0); break;
case WM_PAINT:
// Many lines skipped please, refer to the source code case WM_SIZE:
if (wParam != SIZE_MINIMIZED) {
appObj->rendererObj->width = lParam & 0xffff; appObj->rendererObj->height = (lParam & 0xffff0000) >> 16;
appObj->rendererObj->getSwapChain()-> setSwapChainExtent(appObj->rendererObj-> width, appObj->rendererObj->height); appObj->resize()
}
break;
default: break;
}
return (DefWindowProc(hWnd, uMsg, wParam, lParam));
}
VulkanApplication 類添加了一個名為 resize()的新函數;此函數會處理調整尺寸的操作。 當發生調整尺寸事件時,Vulkan 應用程序的調整尺寸函數由 VulkanRenderer :: WndProc()來調用。
resize()函數銷毀創建的資源並再次重新創建它們。 可以使用 VulkanApplication 中的 isPrepared 標誌來檢查資源的準備狀態;如果此標誌為 false,則表示資源尚未準備好,並且無法執行調整大小。
重新創建資源時,舊的交換鏈圖像會被銷毀並再次創建以匹配新的窗口大小。
注意
交換鏈圖像必須在未被任何未決命令或展示操作使用時才能重新創建。 這可以通過調用 vkDeviceWaitIdle()API 來確保;這個 API 保證這個設備上沒有掛起的操作。 該 API 使主機處於等待狀態,直到設備空閑。
當在 resize 函數中等待操作結束時,它可以繼續並重新創建交換鏈圖像;但在此之前,我們還需要銷毀並重新創建幀緩衝區,命令池,圖形管線,渲染通道,深度緩衝區圖像,圖像視圖,頂點緩衝區等。 以下是顯示該功能的 resize 函數實現:
void VulkanApplication::resize()
{
// If prepared then only proceed for if (!isPrepared) {
return;
}
isResizing = true;
vkDeviceWaitIdle(deviceObj->device); rendererObj->destroyFramebuffers(); rendererObj->destroyCommandPool(); rendererObj->destroyPipeline();
rendererObj->getPipelineObject()->destroyPipelineCache(); rendererObj->destroyRenderpass();
rendererObj->getSwapChain()->destroySwapChain(); rendererObj->destroyDrawableVertexBuffer(); rendererObj->destroyDepthBuffer();
rendererObj->initialize(); prepare();
isResizing = false;
}
應用程序必須在調整大小的時候,防止進行任何渲染操作;為此,isResizing 標誌可用於指示調整尺寸的狀態。 重新初始化是通過調用 VulkanRender 的 initialialize()函數來完成的。 繪圖命令記錄在 Vulkan 應用程序的 prepare 函數中:
void VulkanRenderer::initialize()
{
// We need command buffers, so create a command buffer pool
createCommandPool();
// Lets create the swap chain color images and depth image
buildSwapChainAndDepthImage();
// Build the vertex buffer
createVertexBuffer();
const bool includeDepth = true;
// Create the render pass now..
createRenderPass(includeDepth);
// Use render pass and create frame buffer
createFrameBuffer(includeDepth);
// Create the vertex and fragment shader
createShaders();
// Manage the pipeline state objects
createPipelineStateManagement();
}
void VulkanApplication::prepare()
{
isPrepared = false; rendererObj->prepare(); isPrepared = true;
}
以下屏幕截圖顯示了調整窗口尺寸實現的輸出效果:
總結
在本章中,我們渲染了我們的第一個 Vulkan 中的 Hello World! 程序。 繪製過程由兩個階段組成 - 準備和渲染(執行)。 準備階段記錄命令緩衝區以及渲染階段執行這些命令緩衝區中記錄的指令。 命令緩衝區會被記錄一次並執行多次,除非管線狀態對象中有顯式的變更。
命令緩衝區的準備階段包括記錄 Render Pass 和圖形管線綁定以及繪圖參數,例如頂點緩衝區,視口,裁剪器等等。 最後,指定繪圖 API 命令;我們使用示常式序演示了基於索引和非索引的繪圖 API。
記錄的命令緩衝區在渲染階段執行。 執行過程會獲取交換鏈圖像並將命令緩衝區提交到圖形隊列中。 繪圖操作是在獲得的交換鏈圖像上完成的。 完成此操作後,圖像就會發送到展示引擎以顯示在輸出顯示屏上。 本章還演示了用於繪製索引和非索引幾何圖形的 API 命令。 在本章末尾,我們介紹了 Vulkan 中的同步原語,並學習了如何在主機和設備以及隊列和命令緩衝區之間進行同步。
在下一章中,我們將學習描述符和 push 常量,它允許與 Shader 程序共享資源內容。
推薦閱讀:
