Vulkan教程 - 17 描述符與內存對齊

之前章節的描述符佈局描述了描述符可以綁定的類型。本章我們要對每個VkBuffer資源創建一個描述符集合來將它綁定到統一緩衝描述符上。

描述符集合不能夠直接創建,必須從一個池中分配,就和命令緩衝一樣。同樣的,對應也有描述符池。寫一個新方法createDescriptorPool來建立它,把它放在初始化Vulkan的創建統一緩衝之後:

createUniformBuffers();
createDescriptorPool();

我們需要描述我們的描述符集合打算包含哪種描述符:

VkDescriptorPoolSize poolSize = {};
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSize.descriptorCount = static_cast<uint32_t>(swapChainImages.size());

我們會爲每一幀從這些描述符中分配一個,該池大小會被主VkDescriptorPoolCreateInfo引用:

VkDescriptorPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = 1;
poolInfo.pPoolSizes = &poolSize;

除了可以獲得各個描述符的最大值之外,我們還要指定可以分配的描述符集合的最大值:

poolInfo.maxSets = static_cast<uint32_t>(swapChainImages.size());

該結構體有一個可選標記VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT,和命令池類似,該標記確定了各個描述符集合是否可以被釋放。我們不會在創建後再去接觸描述符集合,所以我們不用該標記。

添加一個新的類成員來存儲描述符池的句柄,調用vkCreateDescriptorPool來創建它。

VkDescriptorPool descriptorPool;
...
if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
    throw std::runtime_error("failed to create descriptor pool!");
}

描述符池應該在交換鏈重建的時候銷燬因爲它依賴圖像個數:

for (size_t i = 0; i < swapChainImages.size(); i++) {
    vkDestroyBuffer(device, uniformBuffers[i], nullptr);
    vkFreeMemory(device, uniformBuffersMemory[i], nullptr);
}

vkDestroyDescriptorPool(device, descriptorPool, nullptr);

然後重建交換鏈的時候進行重建:

createUniformBuffers();
createDescriptorPool();
createCommandBuffers();

現在我們可以分配描述符集合了。添加一個方法createDescriptorSets:

createDescriptorPool();
createDescriptorSets();
createCommandBuffers();

在初始化Vulkan的部分調用如上面所示。重建交換鏈的時候也要調用,如下:

createDescriptorPool();
createDescriptorSets();
createCommandBuffers();

描述符集合分配通過VkDescriptorSetAllocateInfo結構體描述。你需要指定要分配的描述符池,描述符集合要分配的個數,以及描述符佈局:

std::vector<VkDescriptorSetLayout> layouts(swapChainImages.size(), descriptorSetLayout);
VkDescriptorSetAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = static_cast<uint32_t>(swapChainImages.size());
allocInfo.pSetLayouts = layouts.data();

我們這裏會爲每個交換鏈圖像創建一個描述符,都使用一樣的佈局。不幸的是,我們需要所有佈局的副本,因爲下面一個方法會需要一個數組匹配集合個數。

添加一個類成員來保存描述符集合句柄並用vkAllocateDescriptorSets分配:

VkDescriptorPool descriptorPool;
std::vector<VkDescriptorSet> descriptorSets;
...
descriptorSets.resize(swapChainImages.size());
if (vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()) != VK_SUCCESS) {
    throw std::runtime_error("failed to allocate descriptor sets!");
}

你不需要顯式清理描述符集合,因爲它們會在描述符池銷燬的時候自動釋放。vkAllocateDescriptorSets調用會分配描述符集合,每個有一個統一緩衝描述符。

描述符集合已經分配了,但是其中的描述符還需要配置。我們現在需要添加一個循環來產生每個描述符:

for (size_t i = 0; i < swapChainImages.size(); i++) {
    VkDescriptorBufferInfo bufferInfo = {};
    bufferInfo.buffer = uniformBuffers[i];
    bufferInfo.offset = 0;
    bufferInfo.range = sizeof(UniformBufferObject);
}

引用該緩衝的描述符,和我們的統一緩衝描述符類似,是通過VkDescriptorBufferInfo配置的。該結構體指定了緩衝和它中間的包含描述符所需數據的區域。

如果你覆蓋整個緩衝,就像我們這個情況一樣,那麼範圍也可以使用VK_WHOLE_SIZE值。描述符配置使用vkUpdateDescriptorSets方法進行更新,它接收一組VkWriteDescriptorSet結構體作爲參數。

VkWriteDescriptorSet descriptorWrite = {};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSets[i];
descriptorWrite.dstBinding = 0;
descriptorWrite.dstArrayElement = 0;

最開始兩個字段指定要更新和綁定的描述符集合。我們設定統一緩衝綁定索引爲0。記住描述符可以是數組,所以我們需要指定想要更新的數組的第一個索引。我們不用數組,所以就設置索引爲0。

descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrite.descriptorCount = 1;

我們要再次指定描述符類型。可以在一個數組中一次更新多個描述符,就從索引dstArrayElement處開始。descriptorCount字段描述了想要更新多少數組元素。

descriptorWrite.pBufferInfo = &bufferInfo;
descriptorWrite.pImageInfo = nullptr;  // optional
descriptorWrite.pTexelBufferView = nullptr;  // optional

最後的字段用descriptorCount結構體引用一個數組,該數組纔是實際配置描述符的。它依賴於描述符類型,也就是三種之中要用的一個。pBufferInfo字段用於引用緩衝數據的描述符,pImageInfo用於引用圖像數據的描述符,pTexelBufferView用於引用緩衝視圖的描述符。我們的描述符是基於緩衝的,所以我們用pBufferInfo。

vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);

更新操作用vkUpdateDescriptorSets執行,它接收兩種數組作爲參數:一組VkWriteDescriptorSet和一組VkCopyDescriptorSet,後者可以用於將描述符進行互相拷貝。

我們現在要更新createCommandBuffers方法,用cmdBindDescriptorSets來爲每個交換鏈圖像真正綁定正確的描述符集合到着色器中的描述符。這需要在vkCmdDrawIndexed之前完成:

vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS,
    pipelineLayout, 0, 1, &descriptorSets[i], 0, nullptr);

不像是頂點和索引緩衝,描述符集合對圖像管線不是獨一無二的。因此我們需要指定是否想要綁定描述符集合到圖形或者計算管線。下一個參數就是描述符所基於的佈局。接着的三個參數指定了第一個描述符集合索引,要綁定的集合個數以及要綁定的數組。我們之後回來看。最後一個參數指定了一個偏置數組,用於動態描述符。我們以後再看。

你現在運行程序會發現什麼都不顯示,因爲我們在投影矩陣中對Y軸做了反轉,現在頂點就是順時針繪製,而不是逆時針。這就導致後面剔除以阻止幾何體繪製。在createGraphicsPipeline方法中修改VkPipelineRasterizationStateCreateInfo結構體中的frontFace來修復該問題:

rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;

現在運行程序你應該能看到:

矩形已經改變成了正方形,因爲投影矩陣現在會糾正寬高比。updateUniformBuffer會處理屏幕大小改變問題,所以我們不用在重建交換鏈中重建描述符集合。

有一件事情我們一直掩飾到現在,就是C++結構體中的數據到底怎麼和着色器中的統一定義相匹配的。看起來很顯然,就是二者都用相同的類型:

struct UniformBufferObject {
    glm::mat4 model;
    glm::mat4 view;
    glm::mat4 proj;
};

layout(binding = 0) uniform UniformBufferObject {
    mat4 model;
    mat4 view;
    mat4 proj;
} ubo;

但是,這還不是全部原因。例如,修改結構體和着色器代碼如下:

struct UniformBufferObject {
    glm::vec2 foo;
    glm::mat4 model;
    glm::mat4 view;
    glm::mat4 proj;
};

layout(binding = 0) uniform UniformBufferObject {
    vec2 foo;
    mat4 model;
    mat4 view;
    mat4 proj;
} ubo;

重新編譯着色器,運行程序,發現好不容易做的彩色正方形消失了!因爲我們沒有考慮對齊要求。

Vulkan要求你結構體中的數據在內存中以一種特殊方式對齊,例如:

標量必須是N對齊的(如對32位浮點數來說就是4個字節);

vec2必須是2N對齊的(8個字節);

vec3和vec4必須4N對齊(16字節);

內嵌結構體必須由它的成員的基礎對齊值來對齊,會多達16的倍數;

mat4矩陣必須要有和vec4一樣的對齊值。

我們一開始的着色器有三個mat4字段,已經滿足了對齊要求。每個mat4是4*4*4=64字節大小,模型偏置爲0,視圖偏置爲64,投影偏置爲128。這些都是16的倍數,所以都工作正常。

新結構體用vec2開始,只佔用8字節,因此丟掉了所有偏置。現在模型有個偏置8,視圖有個偏置72,投影有個偏置136,沒一個是16的倍數的。解決這個問題可以用C++11中的alignas標識符:

struct UniformBufferObject {
    glm::vec2 foo;
    alignas(16) glm::mat4 model;
    glm::mat4 view;
    glm::mat4 proj;
};

現在運行程序就沒問題了。VS中選擇17標準,因爲14標準會提示std中沒有optional。

幸運的是,有一種方法能讓你大多數情況下都不用考慮對齊要求。我們可以在包含GLM之前定義GLM_FORCE_DEFAULT_ALIGNED_GENTYPES,它會讓GLM使用一種已經滿足我們對齊要求的vec2和mat4版本。如果你添加該定義,那麼你就可以移除alignas標識符了。

但是不幸的是,這種方法可能會失敗,如果你用了嵌入結構體的話。考慮下面的C++代碼:

struct Foo {
    glm::vec2 v;
};

struct UniformBufferObject {
    Foo f1;
    Foo f2;
};

以及着色器定義:

struct Foo {
    vec2 v;
};

layout(binding = 0) uniform UniformBufferObject {
    Foo f1;
    Foo f2;
} ubo;

這種情況下,f2將會有偏置8,但是它卻應該有個偏置爲16,因爲它是嵌入結構體。這時你就必須自己指定對齊了:

struct UniformBufferObject {
    Foo f1;
    alignas(16) Foo f2;
};

這些需要注意的地方就是明確對齊的理由之一,這樣你就不會被奇怪的對齊錯誤症狀抓個正着:

struct UniformBufferObject {
	alignas(16) glm::mat4 model;
	alignas(16) glm::mat4 view;
	alignas(16) glm::mat4 proj;
};

去掉了foo字段後不要忘記重新編譯着色器。

就和一些結構體和方法調用所示,可以同時綁定多個描述符集合。當創建管線佈局的時候,你需要爲每個描述符集合指定一個描述符佈局。着色器就可以像這樣來引用特定描述符集合了:

layout(set = 0, binding = 0) uniform UniformBufferObject { ... }

你可以使用該特性將每個對象上都有所變化的描述符,以及被共享的描述符,放到不同的描述符集合。這樣你就能避免重新在多個繪製命令中綁定大多數描述符,從而提高了效率。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章