操作系統(tǒng):Windows8.1
顯卡:Nivida GTX965M
開發(fā)工具:Visual Studio 2017
Introduction
頂點(diǎn)緩沖區(qū)現(xiàn)在已經(jīng)可以正常工作,但相比于顯卡內(nèi)部讀取數(shù)據(jù),單純從CPU訪問內(nèi)存數(shù)據(jù)的方式性能不是最佳的。最佳的方式是采用VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT標(biāo)志位,通常來說用在專用的圖形卡,CPU是無法訪問的。在本章節(jié)我們創(chuàng)建兩個(gè)頂點(diǎn)緩沖區(qū)。一個(gè)緩沖區(qū)提供給CPU-HOST內(nèi)存訪問使用,用于從頂點(diǎn)數(shù)組中提交數(shù)據(jù),另一個(gè)頂點(diǎn)緩沖區(qū)用于設(shè)備local內(nèi)存。我們將會(huì)使用緩沖區(qū)拷貝的命令將數(shù)據(jù)從暫存緩沖區(qū)拷貝到實(shí)際的圖形卡內(nèi)存中。
Transfer queue
緩沖區(qū)拷貝的命令需要隊(duì)列簇支持傳輸操作,可以通過VK_QUEUE_TRANSFER_BIT標(biāo)志位指定。好消息是任何支持VK_QUEUE_GRAPHICS_BIT 或者 VK_QUEUE_COMPUTE_BIT標(biāo)志位功能的隊(duì)列簇都默認(rèn)支持VK_QUEUE_TRANSFER_BIT操作。這部分的實(shí)現(xiàn)不需要在queueFlags顯示的列出。
如果需要挑戰(zhàn),甚至可以嘗試為不同的隊(duì)列簇指定具體的傳輸操作。這部分實(shí)現(xiàn)需要對代碼做出如下修改:
修改QueueFamilyIndices和findQueueFamilies,明確指定隊(duì)列簇需要具備VK_QUEUE_TRANSFER標(biāo)志位,而不是VK_QUEUE_GRAPHICS_BIT。
修改createLogicalDevice函數(shù),請求一個(gè)傳輸隊(duì)列句柄。
創(chuàng)建兩個(gè)命令對象池分配命令緩沖區(qū),用于向傳輸隊(duì)列簇提交命令。
修改資源的sharingMode為VK_SHARING_MODE_CONCURRENT,并指定為graphics和transfer隊(duì)列簇。
提交任何傳輸命令,諸如vkCmdCopyBuffer(本章節(jié)使用)到傳輸隊(duì)列,而不是圖形隊(duì)列。
需要一些額外的工作,但是它我們更清楚的了解資源在不同隊(duì)列簇如何共享的。
Abstracting buffer creation
考慮到我們在本章節(jié)需要?jiǎng)?chuàng)建多個(gè)緩沖區(qū),比較理想的是創(chuàng)建輔助函數(shù)來完成。新增函數(shù)createBuffer并將createVertexBuffer中的部分代碼(不包括映射)移入該函數(shù)。
void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, VkDeviceMemory& bufferMemory) { VkBufferCreateInfo bufferInfo = {}; bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufferInfo.size = size; bufferInfo.usage = usage; bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) { throw std::runtime_error("failed to create buffer!"); } VkMemoryRequirements memRequirements; vkGetBufferMemoryRequirements(device, buffer, &memRequirements); VkMemoryAllocateInfo allocInfo = {}; allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; allocInfo.allocationSize = memRequirements.size; allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) { throw std::runtime_error("failed to allocate buffer memory!"); } vkBindBufferMemory(device, buffer, bufferMemory, 0); }
該函數(shù)需要傳遞緩沖區(qū)大小,內(nèi)存屬性和usage最終創(chuàng)建不同類型的緩沖區(qū)。最后兩個(gè)參數(shù)保存輸出的句柄。
我們可以從createVertexBuffer函數(shù)中移除創(chuàng)建緩沖區(qū)和分配內(nèi)存的代碼,并使用createBuffer替代:
void createVertexBuffer() { VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); createBuffer(bufferSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, vertexBuffer, vertexBufferMemory); void* data; vkMapMemory(device, vertexBufferMemory, 0, bufferSize, 0, &data); memcpy(data, vertices.data(), (size_t) bufferSize); vkUnmapMemory(device, vertexBufferMemory); }
運(yùn)行程序確保頂點(diǎn)緩沖區(qū)仍然正常工作。
Using a staging buffer
我們現(xiàn)在改變createVertexBuffer函數(shù),僅僅使用host緩沖區(qū)作為臨時(shí)緩沖區(qū),并且使用device緩沖區(qū)作為最終的頂點(diǎn)緩沖區(qū)。
void createVertexBuffer() { VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); VkBuffer stagingBuffer; VkDeviceMemory stagingBufferMemory; createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); void* data; vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data); memcpy(data, vertices.data(), (size_t) bufferSize); vkUnmapMemory(device, stagingBufferMemory); createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory); }
我們使用stagingBufferMemory劃分新的stagingBuffer暫存緩沖區(qū)用來映射、拷貝頂點(diǎn)數(shù)據(jù)。在本章節(jié)我們使用兩個(gè)新的緩沖區(qū)usage標(biāo)致類型:
VK_BUFFER_USAGE_TRANSFER_SRC_BIT:緩沖區(qū)可以用于源內(nèi)存?zhèn)鬏敳僮鳌?/p>
VK_BUFFER_USAGE_TRANSFER_DST_BIT:緩沖區(qū)可以用于目標(biāo)內(nèi)存?zhèn)鬏敳僮鳌?/p>
vertexBuffer現(xiàn)在使用device類型作為分配的內(nèi)存類型,意味著我們不可以使用vkMapMemory內(nèi)存映射。然而我們可以從stagingBuffer向vertexBuffer拷貝數(shù)據(jù)。我們需要指定stagingBuffer的傳輸源標(biāo)志位,還要為頂點(diǎn)緩沖區(qū)vertexBuffer的usage設(shè)置傳輸目標(biāo)的標(biāo)志位。
我們新增函數(shù)copyBuffer,用于從一個(gè)緩沖區(qū)拷貝數(shù)據(jù)到另一個(gè)緩沖區(qū)。
void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { }
使用命令緩沖區(qū)執(zhí)行內(nèi)存?zhèn)鬏數(shù)牟僮髅?,就像繪制命令一樣。因此我們需要分配一個(gè)臨時(shí)命令緩沖區(qū)?;蛟S在這里希望為短期的緩沖區(qū)分別創(chuàng)建command pool,那么可以考慮內(nèi)存分配的優(yōu)化策略,在command pool生成期間使用VK_COMMAND_POOL_CREATE_TRANSIENT_BIT標(biāo)志位。
void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { VkCommandBufferAllocateInfo allocInfo = {}; allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocInfo.commandPool = commandPool; allocInfo.commandBufferCount = 1; VkCommandBuffer commandBuffer; vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); }
立即使用命令緩沖過去進(jìn)行記錄:
VkCommandBufferBeginInfo beginInfo = {}; beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vkBeginCommandBuffer(commandBuffer, &beginInfo);
應(yīng)用于繪制命令緩沖區(qū)的VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT標(biāo)志位在此不必要,因?yàn)槲覀冎枰褂靡淮蚊罹彌_區(qū),等待該函數(shù)返回,直到復(fù)制操作完成。告知driver驅(qū)動(dòng)程序使用VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT是一個(gè)好的習(xí)慣。
VkBufferCopy copyRegion = {}; copyRegion.srcOffset = 0; // OptionalcopyRegion.dstOffset = 0; // OptionalcopyRegion.size = size; vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region);
緩沖區(qū)內(nèi)容使用vkCmdCopyBuffer命令傳輸。它使用source和destination緩沖區(qū)及一個(gè)緩沖區(qū)拷貝的區(qū)域作為參數(shù)。這個(gè)區(qū)域被定義在VkBufferCopy結(jié)構(gòu)體中,描述源緩沖區(qū)的偏移量,目標(biāo)緩沖區(qū)的偏移量和對應(yīng)的大小。與vkMapMemory命令不同,這里不可以指定VK_WHOLE_SIZE。
vkEndCommandBuffer(commandBuffer);
此命令緩沖區(qū)僅包含拷貝命令,因此我們可以在此之后停止記錄?,F(xiàn)在執(zhí)行命令緩沖區(qū)完成傳輸:
VkSubmitInfo submitInfo = {}; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &commandBuffer; vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); vkQueueWaitIdle(graphicsQueue);
與繪制命令不同的是,這個(gè)時(shí)候我們不需要等待任何事件。我們只是想立即在緩沖區(qū)執(zhí)行傳輸命令。這里有同樣有兩個(gè)方式等待傳輸命令完成。我們可以使用vkWaitForFences等待屏障fence,或者只是使用vkQueueWaitIdle等待傳輸隊(duì)列變?yōu)榭臻gidle。一個(gè)屏障允許安排多個(gè)連續(xù)的傳輸操作,而不是一次執(zhí)行一個(gè)。這給了驅(qū)動(dòng)程序更多的優(yōu)化空間。
vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
不要忘記清理用于傳輸命令的命令緩沖區(qū)。
我們可以從createVertexBuffer函數(shù)中調(diào)用copyBuffer,拷貝頂點(diǎn)數(shù)據(jù)到設(shè)備緩沖區(qū)中:
createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory); copyBuffer(stagingBuffer, vertexBuffer, bufferSize)
當(dāng)從暫存緩沖區(qū)拷貝數(shù)據(jù)到圖形卡設(shè)備緩沖區(qū)完畢后,我們應(yīng)該清理它:
... copyBuffer(stagingBuffer, vertexBuffer, bufferSize); vkDestroyBuffer(device, stagingBuffer, nullptr); vkFreeMemory(device, stagingBufferMemory, nullptr); }
運(yùn)行程序確認(rèn)三角形繪制正常。它是可見的,但其頂點(diǎn)數(shù)據(jù)現(xiàn)在是從高性能的顯存中加載。當(dāng)我們開始渲染更復(fù)雜的幾何圖形時(shí),這個(gè)技術(shù)是非常重要。
Conclusion
需要了解的是,在真實(shí)的生產(chǎn)環(huán)境中的應(yīng)用程序里,不建議為每個(gè)緩沖區(qū)調(diào)用vkAllocateMemory分配內(nèi)存。內(nèi)存分配的最大數(shù)量受到maxMemoryAllocationCount物理設(shè)備所限,及時(shí)在像NVIDIA GTX1080這樣的高端硬件上,也只能提供4096的大小。同一時(shí)間,為大量對象分配內(nèi)存的正確方法是創(chuàng)建一個(gè)自定義分配器,通過使用我們在許多函數(shù)中用到的偏移量offset,將一個(gè)大塊的可分配內(nèi)存區(qū)域劃分為多個(gè)可分配內(nèi)存塊,提供緩沖區(qū)使用。
也可以自己實(shí)現(xiàn)一個(gè)靈活的內(nèi)存分配器,或者使用GOUOpen提供的VulkanMemoryAllocator庫。然而,對于本教程,我們可以做到為每個(gè)資源使用單獨(dú)的分配,因?yàn)槲覀儾粫?huì)觸達(dá)任何資源限制條件。
http://www.cnblogs.com/heitao/p/7057566.html