作業系統:Windows8.1
顯示卡:Nivida GTX965M
開發工具:Visual Studio 2017
在這一章節,我們了解一下将渲染圖像送出到螢幕的基本機制。這種機制稱為交換鍊,并且需要在Vulkan上下文中被明确建立。從螢幕的角度觀察,交換鍊本質上是一個圖像隊列。應用程式作為生産者會擷取圖像進行繪制,然後将其返還給交換鍊圖像隊列,等待螢幕消費。交換鍊的具體配置資訊決定了應用程式送出繪制圖像到隊列的條件以及圖像隊清單現的效果,但交換鍊的通常使用目的是使繪制圖像的最終呈現與螢幕的重新整理頻率同步。可以簡單将交換鍊了解為一個隊列,同步從生産者,即應用程式繪制圖像,到消費者,螢幕重新整理的Produce-Consume關系。在深入内容前看一下官方給出的整體交換鍊示例圖。
當然圖示上有一些陌生的關鍵字會在接下來的章節中逐一介紹,在此有一個整體概念。
Checking for swap chain support
并不是所有的圖形卡具備能力将繪制的圖像直接顯示到螢幕上。比如一個GPU卡是為伺服器設計的,那就不會具備任何有關顯示的輸出。其次,圖像呈現是與surface打交道,而surface又與具體的窗體系統強關聯,從這個角度,我們可以認為它不是Vulkan核心的部分。在查詢圖形卡是否支援後,需要啟用VK_KHR_swapchain裝置級别的擴充。
是以呢,我們首先擴充之前的isDeviceSuitable函數,确認裝置是否支援。之前我們已經了解如何列出VkPhysicalDevice支援的擴充清單,在此就不展開具體細節了。請注意的是,Vulkan頭檔案提供給了一個友善的宏VK_KHR_SWAPCHAIN_EXTENSION_NAME,該宏定義為VK_KHR_swapchain。使用宏的優點就是避免拼寫錯誤。
首先聲明需要的裝置擴充清單,與之前開啟validation layers的清單是相似的。
const std::vector<const char*> deviceExtensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
接下來,建立一個從isDeviceSuitable調用的新函數checkDeviceExtensionSupport作為額外的檢查邏輯:
bool isDeviceSuitable(VkPhysicalDevice device) {
QueueFamilyIndices indices = findQueueFamilies(device); bool extensionsSupported = checkDeviceExtensionSupport(device); return indices.isComplete() && extensionsSupported;
}bool checkDeviceExtensionSupport(VkPhysicalDevice device) { return true;
}
修改函數體以便于枚舉裝置所有集合,并檢測是否所有需要的擴充在其中。
bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
uint32_t extensionCount;
vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr);
std::vector<VkExtensionProperties> availableExtensions(extensionCount);
vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data());
std::set<std::string> requiredExtensions(deviceExtensions.begin(), deviceExtensions.end()); for (const auto& extension : availableExtensions) {
requiredExtensions.erase(extension.extensionName);
} return requiredExtensions.empty();
}
選擇一組字元串來表示未經确認過的擴充名。這樣做可以比較容易的進行增删及周遊的次序。當然也可以像CheckValidationLayerSupport函數那樣做嵌套的循環。性能的差異在這裡是不關緊要的。現在運作代碼驗證圖形卡是否能夠順利建立一個交換鍊。需要注意的是前一個章節中驗證過的presentation隊列有效性,并沒有明确指出交換鍊擴充也必須有效支援。好在擴充必須明确的開啟。
啟用擴充需要對邏輯裝置的建立結構體做一些小的改動:
createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
Querying details of swap chain support
如果僅僅是為了測試交換鍊的有效性是遠遠不夠的,因為它還不能很好的與窗體surface相容。建立交換鍊同樣也需要很多設定,是以我們需要了解一些有關設定的細節。
基本上有三大類屬性需要設定:
- 基本的surface功能屬性(min/max number of images in swap chain, min/max width and height of images)
- Surface格式(pixel format, color space)
- 有效的presentation模式
與findQueueFamilies類似,我們使用結構體一次性的傳遞詳細的資訊。三類屬性封裝在如下結構體中:
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities;
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> presentModes;
};
現在建立新的函數querySwapChainSupport填充該結構體。
SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) {
SwapChainSupportDetails details; return details;
}
本小節涉及如何查詢包含此資訊的結構體,這些結構體的含義及包含的資料将在下一節讨論。
我們現在開始基本的surface功能設定部分。這些屬性可以通過簡單的函數調用查詢,并傳回到單個VkSurfaceCapabilitiesKHR結構體中。
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);
這個函數需要VkPhysicalDevice和VkSurfaceKHR窗體surface決定支援哪些具體功能。所有用于檢視支援功能的函數都需要這兩個參數,因為它們是交換鍊的核心元件。
下一步查詢支援的surface格式。因為擷取到的是一個結構體清單,具體應用形式如下:
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);if (formatCount != 0) {
details.formats.resize(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data());
}
確定集合對于所有有效的格式可擴充。最後查詢支援的presentation模式,同樣的方式,使用vkGetPhysicalDeviceSurfacePresentModesKHR:
uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);if (presentModeCount != 0) {
details.presentModes.resize(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data());
}
現在結構體的相關細節介紹完畢,讓我們擴充isDeviceSuitable函數,進而利用該函數驗證交換鍊足夠的支援。在本章節中交換鍊的支援是足夠的,因為對于給定的窗體surface,它至少支援一個圖像格式,一個presentaion模式。
bool swapChainAdequate = false;if (extensionsSupported) {
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty();
}
比較重要的是嘗試查詢交換鍊的支援是在驗證完擴充有效性之後進行。函數的最後一行代碼修改為:
return indices.isComplete() && extensionsSupported && swapChainAdequate;
Choosing the right settings for the swap chain
如果swapChainAdequate條件足夠,那麼對應的支援的足夠的,但是根據不同的模式仍然有不同的最佳選擇。我們編寫一組函數,通過進一步的設定查找最比對的交換鍊。這裡有三種類型的設定去确定:
- Surface格式 (color depth)
- Presentation mode (conditions for "swapping" image to the screen)
- Swap extent (resolution of images in swap chain)
首先在腦海中對每一個設定都有一個理想的數值,如果達成一緻我們就使用,否則我們一起建立一些邏輯去找到更好的規則、數值。
Surface format
這個函數用來設定surface格式。我們傳遞formats作為函數的參數,類型為SwapChainSupportDetails。
VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {
}
每個VkSurfaceFormatKHR結構都包含一個format和一個colorSpace成員。format成員變量指定色彩通道和類型。比如,VK_FORMAT_B8G8R8A8_UNORM代表了我們使用B,G,R和alpha次序的通道,且每一個通道為無符号8bit整數,每個像素總計32bits。colorSpace成員描述SRGB顔色空間是否通過VK_COLOR_SPACE_SRGB_NONLINEAR_KHR标志支援。需要注意的是在較早版本的規範中,這個标志名為VK_COLORSPACE_SRGB_NONLINEAR_KHR。
如果可以我們盡可能使用SRGB(彩色語言協定),因為它會得到更容易感覺的、精确的色彩。直接與SRGB顔色打交道是比較有挑戰的,是以我們使用标準的RGB作為顔色格式,這也是通常使用的一個格式VK_FORMAT_B8G8R8A8_UNORM。
最理想的情況是surface沒有設定任何偏向性的格式,這個時候Vulkan會通過僅傳回一個VkSurfaceFormatKHR結構表示,且該結構的format成員設定為VK_FORMAT_UNDEFINED。
if (availableFormats.size() == 1 && availableFormats[0].format == VK_FORMAT_UNDEFINED) { return {VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR};
}
如果不能自由的設定格式,那麼我們可以通過周遊清單設定具有偏向性的組合:
for (const auto& availableFormat : availableFormats) { if (availableFormat.format == VK_FORMAT_B8G8R8A8_UNORM && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { return availableFormat;
}
}
如果以上兩種方式都失效了,這個時候我們可以通過“優良”進行打分排序,但是大多數情況下會選擇第一個格式作為理想的選擇。
VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) { if (availableFormats.size() == 1 && availableFormats[0].format == VK_FORMAT_UNDEFINED) { return {VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR};
} for (const auto& availableFormat : availableFormats) { if (availableFormat.format == VK_FORMAT_B8G8R8A8_UNORM && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { return availableFormat;
}
} return availableFormats[0];
}
Presentation mode
presentation模式對于交換鍊是非常重要的,因為它代表了在螢幕呈現圖像的條件。在Vulkan中有四個模式可以使用:
- VK_PRESENT_MODE_IMMEDIATE_KHR: 應用程式送出的圖像被立即傳輸到螢幕呈現,這種模式可能會造成撕裂效果。
- VK_PRESENT_MODE_FIFO_KHR: 交換鍊被看作一個隊列,當顯示内容需要重新整理的時候,顯示裝置從隊列的前面擷取圖像,并且程式将渲染完成的圖像插入隊列的後面。如果隊列是滿的程式會等待。這種規模與視訊遊戲的垂直同步很類似。顯示裝置的重新整理時刻被成為“垂直中斷”。
- VK_PRESENT_MODE_FIFO_RELAXED_KHR: 該模式與上一個模式略有不同的地方為,如果應用程式存在延遲,即接受最後一個垂直同步信号時隊列空了,将不會等待下一個垂直同步信号,而是将圖像直接傳送。這樣做可能導緻可見的撕裂效果。
- VK_PRESENT_MODE_MAILBOX_KHR: 這是第二種模式的變種。當交換鍊隊列滿的時候,選擇新的替換舊的圖像,進而替代阻塞應用程式的情形。這種模式通常用來實作三重緩沖區,與标準的垂直同步雙緩沖相比,它可以有效避免延遲帶來的撕裂效果。
邏輯上看僅僅VR_PRESENT_MODE_FIFO_KHR模式保證可用性,是以我們再次增加一個函數查找最佳的模式:
VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR> availablePresentModes) { return VK_PRESENT_MODE_FIFO_KHR;
}
我個人認為三級緩沖是一個非常好的政策。它允許我們避免撕裂,同時仍然保持相對低的延遲,通過渲染盡可能新的圖像,直到接受垂直同步信号。是以我們看一下清單,它是否可用:
VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR> availablePresentModes) { for (const auto& availablePresentMode : availablePresentModes) { if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) { return availablePresentMode;
}
} return VK_PRESENT_MODE_FIFO_KHR;
}
遺憾的是,一些驅動程式目前并不支援VK_PRESENT_MODE_FIFO_KHR,除此之外如果VK_PRESENT_MODE_MAILBOX_KHR也不可用,我們更傾向使用VK_PRESENT_MODE_IMMEDIATE_KHR:
VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR> availablePresentModes) {
VkPresentModeKHR bestMode = VK_PRESENT_MODE_FIFO_KHR; for (const auto& availablePresentMode : availablePresentModes) { if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) { return availablePresentMode;
} else if (availablePresentMode == VK_PRESENT_MODE_IMMEDIATE_KHR) {
bestMode = availablePresentMode;
}
} return bestMode;
}
Swap extent
還剩下一個屬性,為此我們添加一個函數:
VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
}
交換範圍是指交換鍊圖像的分辨率,它幾乎總是等于我們繪制窗體的分辨率。分辨率的範圍被定義在VkSurfaceCapabilitiesKHR結構體中。Vulkan告訴我們通過設定currentExtent成員的width和height來比對窗體的分辨率。然而,一些窗體管理器允許不同的設定,意味着将currentExtent的width和height設定為特殊的數值表示:uint32_t的最大值。在這種情況下,我們參考窗體minImageExtent和maxImageExtent選擇最比對的分辨率。
VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) { if (capabilities.currentExtent.width != std::numeric_limits<uint32_t>::max()) { return capabilities.currentExtent;
} else {
VkExtent2D actualExtent = {WIDTH, HEIGHT};
actualExtent.width = std::max(capabilities.minImageExtent.width, std::min(capabilities.maxImageExtent.width, actualExtent.width));
actualExtent.height = std::max(capabilities.minImageExtent.height, std::min(capabilities.maxImageExtent.height, actualExtent.height)); return actualExtent;
}
}
max和min函數用于将WIDTH和HEIGHT收斂在實際支援的minimum和maximum範圍中。在這裡确認包含<algorithm>頭檔案。
Creating the swap chain
現在我們已經有了這些輔助函數,用以在運作時幫助我們做出明智的選擇,最終獲得有了建立交換鍊所需要的所有資訊。
建立一個函數createSwapChain,在initVulkan函數中,該函數會在建立邏輯裝置之後調用。
void initVulkan() {
createInstance();
setupDebugCallback();
createSurface();
pickPhysicalDevice();
createLogicalDevice();
createSwapChain();
}void createSwapChain() {
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice);
VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes);
VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities);
}
實際上還有一些小事情需要确定,但是比較簡單,是以沒有單獨建立函數。第一個是交換鍊中的圖像數量,可以了解為隊列的長度。它指定運作時圖像的最小數量,我們将嘗試大于1的圖像數量,以實作三重緩沖。
uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) {
imageCount = swapChainSupport.capabilities.maxImageCount;
}
對于maxImageCount數值為0代表除了記憶體之外沒有限制,這就是為什麼我們需要檢查。
與Vulkan其他對象的建立過程一樣,建立交換鍊也需要填充大量的結構體:
VkSwapchainCreateInfoKHR createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
createInfo.surface = surface;
在指定交換鍊綁定到具體的surface之後,需要指定交換鍊圖像有關的詳細資訊:
createInfo.minImageCount = imageCount;
createInfo.imageFormat = surfaceFormat.format;
createInfo.imageColorSpace = surfaceFormat.colorSpace;
createInfo.imageExtent = extent;
createInfo.imageArrayLayers = 1;
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
imageArrayLayers指定每個圖像組成的層數。除非我們開發3D應用程式,否則始終為1。imageUsage位字段指定在交換鍊中對圖像進行的具體操作。在本小節中,我們将直接對它們進行渲染,這意味着它們作為顔色附件。也可以首先将圖像渲染為單獨的圖像,進行後處理操作。在這種情況下可以使用像VK_IMAGE_USAGE_TRANSFER_DST_BIT這樣的值,并使用記憶體操作将渲染的圖像傳輸到交換鍊圖像隊列。
QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
uint32_t queueFamilyIndices[] = {(uint32_t) indices.graphicsFamily, (uint32_t) indices.presentFamily};if (indices.graphicsFamily != indices.presentFamily) {
createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
createInfo.queueFamilyIndexCount = 2;
createInfo.pQueueFamilyIndices = queueFamilyIndices;
} else {
createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
createInfo.queueFamilyIndexCount = 0; // Optional
createInfo.pQueueFamilyIndices = nullptr; // Optional}
接下來,我們需要指定如何處理跨多個隊列簇的交換鍊圖像。如果graphics隊列簇與presentation隊列簇不同,會出現如下情形。我們将從graphics隊列中繪制交換鍊的圖像,然後在另一個presentation隊列中送出他們。多隊列處理圖像有兩種方法:
- VK_SHARING_MODE_EXCLUSIVE: 同一時間圖像隻能被一個隊列簇占用,如果其他隊列簇需要其所有權需要明确指定。這種方式提供了最好的性能。
- VK_SHARING_MODE_CONCURRENT: 圖像可以被多個隊列簇通路,不需要明确所有權從屬關系。
在本小節中,如果隊列簇不同,将會使用concurrent模式,避免處理圖像所有權從屬關系的内容,因為這些會涉及不少概念,建議後續的章節讨論。Concurrent模式需要預先指定隊列簇所有權從屬關系,通過queueFamilyIndexCount和pQueueFamilyIndices參數進行共享。如果graphics隊列簇和presentation隊列簇相同,我們需要使用exclusive模式,因為concurrent模式需要至少兩個不同的隊列簇。
createInfo.preTransform = swapChainSupport.capabilities.currentTransform;
如果交換鍊支援(supportedTransforms in capabilities),我們可以為交換鍊圖像指定某些轉換邏輯,比如90度順時針旋轉或者水準反轉。如果不需要任何transoform操作,可以簡單的設定為currentTransoform。
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
混合Alpha字段指定alpha通道是否應用與與其他的窗體系統進行混合操作。如果忽略該功能,簡單的填VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR。
createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
presentMode指向自己。如果clipped成員設定為VK_TRUE,意味着我們不關心被遮蔽的像素資料,比如由于其他的窗體置于前方時或者渲染的部分内容存在于可是區域之外,除非真的需要讀取這些像素獲資料進行處理,否則可以開啟裁剪獲得最佳性能。
createInfo.oldSwapchain = VK_NULL_HANDLE;
最後一個字段oldSwapChain。Vulkan運作時,交換鍊可能在某些條件下被替換,比如視窗調整大小或者交換鍊需要重新配置設定更大的圖像隊列。在這種情況下,交換鍊實際上需要重新配置設定建立,并且必須在此字段中指定對舊的引用,用以回收資源。這是一個比較複雜的話題,我們會在後面的章節中詳細介紹。現在假設我們隻會建立一個交換鍊。
現在添加一個類成員變量存儲VkSwapchainKHR對象:
VkSwapchainKHR swapChain;
建立交換鍊隻需要簡單的調用函數:vkCreateSwapchainKHR:
if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) { throw std::runtime_error("failed to create swap chain!");
}
參數是邏輯裝置,交換鍊建立的資訊,可選擇的配置設定器和一個存儲交換後的句柄指針。它也需要在裝置被清理前,進行銷毀操作,通過調用vkDestroySwapchainKHR。
void cleanup() {
vkDestroySwapchainKHR(device, swapChain, nullptr);
...
}
現在運作程式確定交換鍊建立成功!
嘗試移除createInfo.imageExtent = extent;并在validation layers開啟的條件下,validation layers會立刻捕獲到有幫助的異常資訊:
Retrieving the swap chain images
交換鍊建立後,需要擷取VkImage相關的句柄。它會在後續渲染的章節中引用。添加類成員變量存儲該句柄:
std::vector<VkImage> swapChainImages;
圖像被交換鍊建立,也會在交換鍊銷毀的同時自動清理,是以我們不需要添加任何清理代碼。
我們在createSwapChain函數下面添加代碼擷取句柄,在vkCreateSwapchainKHR後調用。擷取句柄的操作同之前擷取數組集合的操作非常類似。首先通過調用vkGetSwapchainImagesKHR擷取交換鍊中圖像的數量,并根據數量設定合适的容器大小儲存擷取到的句柄集合。
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());
需要注意的是,之前建立交換鍊步驟中我們傳遞了期望的圖像大小到字段minImageCount。而實際的運作,允許我們建立更多的圖像數量,這就解釋了為什麼需要再一次擷取數量。
最後,存儲交換鍊格式和範圍到成員變量中。我們會在後續章節使用。
VkSwapchainKHR swapChain;
std::vector<VkImage> swapChainImages;
VkFormat swapChainImageFormat;
VkExtent2D swapChainExtent;
...
swapChainImageFormat = surfaceFormat.format;
swapChainExtent = extent;
現在我們已經設定了一些圖像,這些圖像可以被繪制,并呈現到窗體。下一章節我們開始讨論如何為圖像設定渲染目标,并了解實際的圖像管線 和 繪制指令。