Vulkan是Khronos Group組織發布的跨平台圖像渲染引擎,而Khronos Group是由Intel、Nvidia等公司共同創立,致力於創建開放標准的應用程序API。大名鼎鼎的OpenGL、OpenGL ES、WebGL、Vulkan都是來自Khronos組織。而Vulkan號稱為“下一版本的OpenGL”,旨在提供更低的CPU開銷和更多GPU控制。Android API 24以后支持vulkan,iOS在WWDC2014也推出Metal圖像渲染。短期內全面取代OpenGL應該不現實,但從長遠來看,它取代OpenGL應該是大勢所趨的。筆者也是新接觸Vulkan,從初學者的角度來看,它的程序確實比較繁瑣,一個簡單的Triangle程序竟然要1000多行的代碼,但萬丈高樓平地起,像學習OpenGL一樣,任何復雜的工程都必須先從簡單的三角形開始,這里我把Vulkan的三角形先寫下來,給后來者一個參考。
1、配置Vulkan的環境,這里就不細說了,網上有很多教程,這里推薦一篇吧,地址是:https://blog.csdn.net/allflowerup/article/details/89766179,總結下來就是將vulkan下載下來並安裝好,再將glfw和glm兩個庫文件放置到VS的環境設置目錄里,后者(glfw和glm)的設置和OpenGL的開發環境搭建一模一樣。順便說下:
GLFW是配合 OpenGL 使用的輕量級工具程序庫,縮寫自 Graphics Library Framework(圖形庫框架)。GLFW 的主要功能是創建並管理窗口和 OpenGL 上下文,同時還提供了處理手柄、鍵盤、鼠標輸入的功能。
GLM(OpenGL Mathematics )是基於OpenGL着色語言(GLSL)規范的圖形軟件的頭文件C ++數學庫。GLM提供的類和函數使用與GLSL相同的命名約定和功能設計和實現,因此任何知道GLSL的人都可以在C ++中使用GLM。
2、我是用Visual Studio2019,先在VS里創建一個空的項目為HelloTriangle,然后為它添加一個HelloTriangle.cpp的源文件,代碼如下:

#define GLFW_INCLUDE_VULKAN #include <GLFW/glfw3.h> #include <iostream> #include <stdexcept> #include <cstdlib> #include <vector> #include <map> #include <optional> #include <set> #include <fstream> using namespace std; void processInput(GLFWwindow* window); //[2]驗證層Debug時開啟 #ifdef NDEBUG //不調試 const bool enableValidationLayers = false; #else const bool enableValidationLayers = true; #endif int errorCounter = 0; const std::string getAssetPath() { #if defined(VK_USE_PLATFORM_ANDROID_KHR) return ""; #elif defined(VK_EXAMPLE_DATA_DIR) return VK_EXAMPLE_DATA_DIR; #else return "./../data/"; #endif } //[2]所有有用的標准驗證都捆綁到SDK的一個層中,稱為VK_LAYER_KHRONOS_validation層。 const std::vector<const char*> validationLayers = { "VK_LAYER_KHRONOS_validation" }; //[5]表示滿足需求的隊列族 struct QueueFamilyIndices { //我們經常遇到這樣的情況:我們可能返回/傳遞/使用某種類型的對象。也就是說,我們可以有某個類型的值,也可以沒有任何值。因此,我們需要一種方法來模擬類似指針的語義,在指針中,我們可以使用nullptr來表示沒有值。處理這個問題的方法是定義一個特定類型的對象,並用一個額外的布爾成員 / 標志來表示值是否存在。std::optional<>以一種類型安全的方式提供了這樣的對象。std::optional對象只是包含對象的內部內存加上一個布爾標志。因此,大小通常比包含的對象大一個字節 std::optional<uint32_t> graphicsFamily; //該字段表示滿足需求的隊列族 std::optional<uint32_t> presentFamily; //支持表現的隊列族索引 //[8]為了方便起見,我們還將向結構本身添加一個泛型檢查 bool isComplete() { return graphicsFamily.has_value() && presentFamily.has_value(); } }; //[5] struct SwapChainSupportDetails { VkSurfaceCapabilitiesKHR capabilities; std::vector<VkSurfaceFormatKHR> formats; std::vector<VkPresentModeKHR> presentModes; }; //[5]設備擴展列表,檢測VK_KHR_swapchain const std::vector<const char*> deviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; //[7] const int WIDTH = 800; //[7] const int HEIGHT = 600; //[10]ate: Start reading at the end of the file //[10]binary: Read the file as binary file (avoid text transformations) static std::vector<char> readFile(const std::string& filename) { std::ifstream file(filename, std::ios::ate | std::ios::binary); if (!file.is_open()) { throw std::runtime_error("failed to open file!"); } //[10]ate 的優勢是,可以獲取文件的大小 size_t fileSize = (size_t)file.tellg(); std::vector<char> buffer(fileSize); //[10]指針跳到頭 file.seekg(0); file.read(buffer.data(), fileSize); file.close(); return buffer; } //[14] const int MAX_FRAMES_IN_FLIGHT = 2; //可以同時並行處理的幀數 class HelloTriangle { public: void run() { //[1] initWindow(); initVulkan(); mainLoop(); cleanup(); } public: //[1] GLFWwindow* window; //[2] VkInstance instance; //創建instance //[3] VkSurfaceKHR surface; //窗口表面 //[4] VkDebugUtilsMessengerEXT debugMessenger; //[5]當vkinInstance被銷毀時,該對象將被隱式銷毀,因此不需要在cleanup函數中執行銷毀。 VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; //[6] VkDevice device; //邏輯設備對象作為類成員 //[6] VkQueue graphicsQueue; //[6] VkQueue presentQueue; //呈現隊列 //[7] VkSwapchainKHR swapChain; //交換鏈 //[7] std::vector<VkImage> swapChainImages; //交換鏈的圖像句柄, 在交換鏈清除時自動被清除 //[7] VkFormat swapChainImageFormat; //交換鏈圖像格式 //[7] VkExtent2D swapChainExtent; //交換鏈圖像范圍 //[8] std::vector<VkImageView> swapChainImageViews; //存儲圖像視圖 //[9] VkRenderPass renderPass; //[10] VkPipelineLayout pipelineLayout; //[10] VkPipeline graphicsPipeline; //[11] std::vector<VkFramebuffer> swapChainFramebuffers; //存儲所有幀緩沖對象 //[12] VkCommandPool commandPool; //創建指令池對象, //指令池對象,管理指令緩沖對象使用的內存,並負責指令緩沖對象的分配 //[13] Command buffers will be automatically freed when their command pool is destroyed std::vector<VkCommandBuffer> commandBuffers; //指令池對象,管理指令緩沖對象使用的內存,並負責指令緩沖對象的分配 //[14]為每一幀創建屬於它們自己的信號量,信號量發出圖像已經被獲取,可以開始渲染的信號 std::vector<VkSemaphore> imageAvailableSemaphores; //[14]信號量發出渲染已經有結果,可以開始呈現的信號 std::vector<VkSemaphore> renderFinishedSemaphores; //[14] 需要使用柵欄(fence) 來進行 CPU 和 GPU 之間的同步, 來防止有超過 MAX_FRAMES_IN_FLIGHT幀的指令同時被提交執行。 // 柵欄(fence) 和信號量(semaphore) 類似,可以用來發出信號和等待信號 std::vector<VkFence> inFlightFences; //每一幀創建一個VkFence 柵欄對象 //[14]-[15]是否需要釋放? std::vector<VkFence> imagesInFlight; //[15] size_t currentFrame = 0; //追蹤當前渲染的是哪一幀 //[16] bool framebufferResized = false; //標記窗口大小是否發生改變 //[1] void initWindow() { glfwInit(); //初始化glfw庫 //因為glfw最初是為創建OpenGL上下文而設計的,所以我們要告訴它不要通過后續調用創建OpenGL上下文:GLFW_NO_API glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); //glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); //窗口大小設置為不可變 window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan Study Yue Qingxuan", nullptr, nullptr); //創建窗口 //[16-1]儲存當前對象指針 glfwSetWindowUserPointer(window, this); //[1] 檢測窗體實際大小,我們可以使用 GLFW 框架中的 glfwSetFramebufferSizeCallback 函數來設置回調: glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); } void initVulkan() { //[2] createInstance(); //[3] createSurface(); //[4] setupDebugMessenger(); //[5] pickPhysicalDevice(); //[6] createLogicalDevice(); //[7] createSwapChain(); //[8] createImageViews(); //[9] createRenderPass(); //[10] createGraphicsPipeline(); //[11] createFramebuffers(); //[12] createCommandPool(); //[13] createCommandBuffers(); //[14] createSemaphores(); } void mainLoop() { //[1] while (!glfwWindowShouldClose(window)) { processInput(window); glfwPollEvents(); //[15] drawFrame(); } //[16]可以用作執行同步的基本的方法. vkDeviceWaitIdle(device); } //VkInstance 應該在應用程序結束前進行清除操作 void cleanup() { //[17] cleanupSwapChain();//銷毀交換鏈對象,在邏輯設備被清除前調用 //[14] //清除為每一幀創建的信號量和VkFence for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {//所有它所同步的指令執行結束后,對它進行清除 vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr); vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr); vkDestroyFence(device, inFlightFences[i], nullptr); } //17將這些釋放資源的函數移動到cleanupSwapChain中 //[12] //vkDestroyCommandPool(device, commandPool, nullptr); //清除創建的指令池對象 //[11] //for (auto framebuffer : swapChainFramebuffers) { // vkDestroyFramebuffer(device, framebuffer, nullptr); //} //[10] //vkDestroyPipeline(device, graphicsPipeline, nullptr); //[10] //vkDestroyPipelineLayout(device, pipelineLayout, nullptr); //[9] //vkDestroyRenderPass(device, renderPass, nullptr); //[8] //for (auto imageView : swapChainImageViews) { // vkDestroyImageView(device, imageView, nullptr); //} //[7] //vkDestroySwapchainKHR(device, swapChain, nullptr); //[6]邏輯設備並不直接與 Vulkan 實例交互,所以創建邏輯設備時不需要使用 Vulkan 實例作為參數 vkDestroyDevice(device, nullptr); //清除邏輯設備對象 //[4] if (enableValidationLayers) { DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr); cout << "000 cleanup enableValidationLayers=" << enableValidationLayers << endl; } else { cout << "111 cleanup enableValidationLayers=" << enableValidationLayers << endl; } //[3] vkDestroySurfaceKHR(instance, surface, nullptr);//銷毀窗口表面對象,表面對象的清除需要在 Vulkan 實例被清除之前完成 //[2] vkDestroyInstance(instance, nullptr); //銷毀Vulkan實例 //[1] glfwDestroyWindow(window); //銷毀窗口 glfwTerminate(); //結束glfw } /// <summary> ///[2]創建實例,實例描述了應用程序和可以支持的API擴展,比如VkApplicationInfo里設置應用程序名稱、引擎名稱 /// VkInstanceCreateInfo:創建VkInstance,設置全局的擴展和驗證層,第一個擴展關於窗口所需要的擴展,有了實例才可以選擇物理設備 /// </summary> void createInstance() { //[2]這里檢測是否啟用驗證層,這里enableValidationLayers=false,checkValidationLayerSupport()=true if (enableValidationLayers && !checkValidationLayerSupport()) { throw std::runtime_error("validation layers requested, but not available!"); } else { cout << "createInstance enableValidationLayers=" << enableValidationLayers << ", !checkValidationLayerSupport()= " << !checkValidationLayerSupport() << std::endl<<"函數返回" << std::endl; } // 創建一個實例首先必須填寫一個包含有關我們應用程序的信息的結構: VkApplicationInfo // 這些數據在技術上不是必須的,而是可選的,但它可為驅動程序提供一些有用的信息,以便針對我們的特定應用進行優化 //[2]well-known graphics engine VkApplicationInfo appInfo = {}; //創建用於描述應用程序信息的結構體 //[2]結構體必須指明類型,pNext指向拓展信息 // Vulkan中的許多結構要求在sType成員中明確指定類型,這也是具有pNext成員的許多結構中的一個,該成員可以在將來指向擴展信息。 appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; appInfo.pNext = nullptr; //該行我自己添加的 appInfo.pApplicationName = "Hello Triangle"; //appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); appInfo.applicationVersion = VK_MAKE_VERSION(1, 1, 0); appInfo.pEngineName = "No Engine"; //appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); //appInfo.apiVersion = VK_API_VERSION_1_0; appInfo.engineVersion = VK_MAKE_VERSION(1, 1, 0); appInfo.apiVersion = VK_API_VERSION_1_3; //[2]Vulkan驅動程序使用哪些全局擴展和驗證,后續后詳細說明 // Vulkan中的很多信息都是通過結構而不是函數參數傳遞的,我們必須再填充一個結構體 VkInstanceCreateInfo 來為創建實例提供足夠的信息。VkInstanceCreateInfo結構是必須指明的,它告訴Vulkan驅動程序我們想要使用哪些全局擴展和驗證層。 VkInstanceCreateInfo createInfo = {}; //幾乎每個Vulkan結構中用於向API傳遞參數的第一個成員是sType字段,它告訴Vulkan這是什么類型的結構。sType字段均有一個約定俗成的套路,可以對照類名給sType賦值 createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; createInfo.pApplicationInfo = &appInfo; //用於放置用於描述應用程序信息的結構體(的地址) //[2]指定全局擴展 // Vulkan是一個與平台無關的API,這意味着需要一個與窗口系統接口的擴展。 // GLFW有一個方便的內置函數,它返回它需要做的擴展,我們可以傳遞給結構體:VkInstanceCreateInfo uint32_t glfwExtensionCount = 0; const char** glfwExtensions; // 此函數返回GLFW所需的Vulkan實例擴展名的數組,以便為GLFW窗口創建Vulkan surface。 // 如果成功,列表將始終包含`VK_KHR_surface`,因此如果您不需要任何其他擴展,則可以將此列表直接傳遞給`VkInstanceCreateInfo`結構。 // 如果機器上沒有Vulkan,則此函數返回“NULL”並生成 GLFW_API_UNAVAILABLE錯誤。 glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); createInfo.enabledExtensionCount = glfwExtensionCount; createInfo.ppEnabledExtensionNames = glfwExtensions; //[2]the global validation layers to enable createInfo.enabledLayerCount = 0; //后續有說明 //[2]驗證層信息 //[2]如果檢查成功,那么vkCreateInstance不會返回VK_ERROR_LAYER_NOT_PRESENT錯誤 if (enableValidationLayers) { // 結構體的最后兩個成員確定要啟用的全局驗證層 createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size()); createInfo.ppEnabledLayerNames = validationLayers.data(); } else { createInfo.enabledLayerCount = 0; } //[2]GLFW auto extensions = getRequiredExtensions(); createInfo.enabledExtensionCount = static_cast<uint32_t>(extensions.size()); createInfo.ppEnabledExtensionNames = extensions.data(); //[2]重用 //[2]通過該方式創建一個額外的調試信息,它將在vkCreateInstance和vkDestroyInstance期間自動創建和銷毀 VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo; if (enableValidationLayers) { createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size()); createInfo.ppEnabledLayerNames = validationLayers.data(); populateDebugMessengerCreateInfo(debugCreateInfo); createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*)&debugCreateInfo; } else { createInfo.enabledLayerCount = 0; createInfo.pNext = nullptr; } //[2]or /*if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { throw std::runtime_error("failed to set up debug messenger!"); }*/ //[2] VK_SUCCESS or Error Code //[2]VkResult result = vkCreateInstance(&createInfo, nullptr, &instance); //[2]or //[2]創建實例 if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { throw std::runtime_error("failed to create instance,創建Vulkan實例失敗!"); /* * //[2]驗證層說明,Vulkan每次調用都會進行相應的驗證,通過返回值判定函數是否執行成功 VkResult vkCreateInstance( const VkInstanceCreateInfo * pCreateInfo, const VkAllocationCallbacks * pAllocator, VkInstance * instance) { if (pCreateInfo == nullptr || instance == nullptr) { log("Null pointer passed to required parameter!"); return VK_ERROR_INITIALIZATION_FAILED; } return real_vkCreateInstance(pCreateInfo, pAllocator, instance); } */ } //[2]the number of extensions //[2]支持擴展的數量 uint32_t extensionCount = 0; vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); //[2]an array of VkExtensionProperties to store details of the extensions. //[2]an array to hold the extension details //[2]支持的擴展詳細信息,返回支持的擴展列表 std::vector<VkExtensionProperties> extensionsProperties(extensionCount); vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensionsProperties.data()); //[2]query the extension details //[2]Each VkExtensionProperties struct contains the name and version of an extension. //[2]查詢擴展的詳細信息 std::cout << "createInstance:返回可支持的擴展列表,available extensions:" << std::endl; std::cout<<"當前文件名為:"<<__FILE__<<",行號為:"<<__LINE__<<std::endl; int i = 1; for (const auto& extension : extensionsProperties) { std::cout << i << "," << "\t" << extension.extensionName << std::endl; i++; } std::cout << "當前文件名為:" << __FILE__ << ",行號為:" << __LINE__ << std::endl; } //[2]list all of the available layers //[2]列出所有驗證層的信息,請求所有可用的校驗層 bool checkValidationLayerSupport() { uint32_t layerCount; //第一次調用,確定層的數量 vkEnumerateInstanceLayerProperties(&layerCount, nullptr); std::vector<VkLayerProperties> availableLayers(layerCount); //調整數組大小 //第二次調用,傳入數組 vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data()); //[2]查詢[vulkan實例]級的層,是否存在驗證層信息 layerName = VK_LAYER_KHRONOS_validation for (const char* layerName : validationLayers) { bool layerFound = false; int i = 1; for (const auto& layerProperties : availableLayers) { std::cout << std::endl << "i=" << i << ",layerName=" << layerName << ", layerProperties.layerName=" << layerProperties.layerName; if (strcmp(layerName, layerProperties.layerName) == 0) { layerFound = true; std::cout << std::endl << "layerName=" << layerName << ",layerProperties.layerName=" << layerProperties.layerName << ",這里找到layer,layerFound返回true" << std::endl; //break; } i++; } if (!layerFound) { return false; } } return true; } //[2]we have to set up a debug messenger with a callback using the VK_EXT_debug_utils extension. //[2]我們必須使用VK_EXT_debug_utils擴展,設置一個帶有回調的debug messenger,設置回調函數來接受調試信息。 //本函數的功能:是否根據啟用驗證層,返回所需的擴展名列表 std::vector<const char*> getRequiredExtensions() { //[5]指定GLFW擴展,但是debug messenger 擴展是有條件添加的 uint32_t glfwExtensionCount = 0; const char** glfwExtensions; glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); std::cout << "* glfwExtensions=" << *glfwExtensions << std::endl; std::vector<const char*> extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); if (enableValidationLayers) {//啟用驗證層 //[2]在這里使用VK_EXT_DEBUG_UTILS_EXTENSION_NAME宏,它等於字符串“VK_EXT_debug_utils”。 //[2]使用此宏可以避免輸入錯誤,push_back會負責將一個值當成vector對象的尾元素“壓到(push)”vector對象的“尾端(back)” extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); } else { std::cout << "系統沒有啟用校驗層,enableValidationLayers=" << enableValidationLayers << std::endl; } std::cout<< "extensions.size()="<< extensions.size() <<std::endl; return extensions; } //[2]仔細閱讀擴展文檔,就會發現有一種方法可以專門為這兩個函數調用創建單獨的 debug utils messenger. //[2]它要求您只需在VkInstanceCreateInfo的pNext擴展字段中 //[2]傳遞一個指向VkDebugUtilsMessengerCreateInfoEXT結構的指針。 //[2]首先將messenger創建信息的填充提取到單獨的函數中: void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) { createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | //診斷信息 VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | //警告信息 VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; //不合法和可能造成崩潰的操作信息 createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; createInfo.pfnUserCallback = debugCallback; } //[2]Add a new static member function called debugCallback with //[2] the PFN_vkDebugUtilsMessengerCallbackEXT prototype. //[2]使用PFN_vkDebugUtilsMessengerCallbackEXT屬性添加一個靜態函數 //[2]The VKAPI_ATTR and VKAPI_CALL ensure that the function has the //[2] right signature for Vulkan to call it. //[2]使用VKAPI_ATTR和VKAPI_CALL 確保函數具有正確的簽名,以便Vulkan調用它 static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( //[2]VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT 診斷信息 //[2]VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT 信息性消息,如資源的創建 //[2]關於行為的消息,其不一定是錯誤,但很可能是應用程序中的BUG //[2]VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT //[2]關於無效且可能導致崩潰的行為的消息 //[2]VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT //[2]可以使用比較操作來檢查消息是否與某個嚴重性級別相等或更差,例如: //[2]if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { //[2] // Message is important enough to show //[2]} VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, //指明了消息的嚴重程度 //[2]VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT 發生了一些與規范或性能無關的事件 //[2]VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT 發生了違反規范或一個可能顯示的錯誤 //[2]VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT 非最優的方式使用Vulkan,進行了可能影響Vulkan性能的行為 VkDebugUtilsMessageTypeFlagsEXT messageType, //指明了消息消息的類型 //[2]消息本身的詳細信息, 包括其重要成員: //[2]pMessage 以null結尾的調試消息字符串 //[2]pObjects 與消息相關的Vulkan對象句柄數組 //[2]objectCount 數組中的對象個數 const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, //該結構體包含了消息更多的細節內容 //最后一個參數pUserData 包含在回調設置期間指定的指針,並允許您將自己的數據傳遞給它 void* pUserData) { std::cerr <<"errorCounter=" <<errorCounter << ",validation layer: " << pCallbackData->pMessage << std::endl; errorCounter++; //通常只在測試校驗層本身時會返回true,其余情況下均返回VK_FALSE; return VK_FALSE; } //[2]-------------------------------------------------------------------------------------------------------- //[3]-------------------------------------------------------------------------------------------------------- /// <summary> /// 使用glfw創建窗口表面 /// </summary> void createSurface() { //Windows的創建方法 //VkWin32SurfaceCreateInfoKHR createInfo = {}; //createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; //createInfo.hwnd = glfwGetWin32Window(window); // ////createInfo.hwnd = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", NULL, NULL);; //createInfo.hinstance = GetModuleHandle(nullptr); //if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) { // throw std::runtime_error("failed to create window surface!"); //} //Linux的創建方法與上面類似 vkCreateXcbSurfaceKHR //使用GLFW創建Window surface,參數依次是 VkInstance 對象,GLFW 窗口指針,自定義內存分配器,存儲返回的 VkSurfaceKHR 對象的內存地址 if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) { throw std::runtime_error("failed to create window surface!"); } else { std::cout << "succeed to create window surface" << std::endl; } } //[3]-------------------------------------------------------------------------------- //[4]-------------------------------------------------------------------------------- void setupDebugMessenger() { if (!enableValidationLayers) return; //該程序從這里直接退出來 VkDebugUtilsMessengerCreateInfoEXT createInfo = {}; populateDebugMessengerCreateInfo(createInfo); //[4] messenger 創建信息的填充提取到單獨的函數中 if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { std::cout << "Failed to setup debug messenger" << std::endl; //throw std::runtime_error("failed to set up debug messenger!"); } else { std::cout << "Succeed to setup debug messenger" << std::endl; } //[4]or createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; //[4]指定希望調用回調嚴重性類型 createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | //診斷消息 VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | //有關行為的消息不一定是錯誤,但很可能是應用程序中的錯誤 VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; //有關無效行為的消息,可能導致崩潰 //[4]過濾回調通知的消息類型,這里啟用了所有類型 createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | //發生了一些與規范或性能無關的事件 VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | //發生了違反規范或一些可能顯示的錯誤,出現了違反規范的情況或發生了一個可能的錯誤 VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; //非最優的方式使用Vulkan,進行了可能影響Vulkan性能的行為 //[4]指定指向回調函數的指針 createInfo.pfnUserCallback = debugCallback; //[4]返回的回調函數 createInfo.pUserData = nullptr; } //[4]創建代理函數 VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) { auto func = (PFN_vkCreateDebugUtilsMessengerEXT)vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT"); if (func != nullptr) { return func(instance, pCreateInfo, pAllocator, pDebugMessenger); } else {//[4]如果無法加載,函數將返回nullptr。 return VK_ERROR_EXTENSION_NOT_PRESENT; } } //[4]銷毀CreateDebugUtilsMessengerEXT void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) { auto func = (PFN_vkDestroyDebugUtilsMessengerEXT)vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT"); if (func != nullptr) { func(instance, debugMessenger, pAllocator); } } //[4]-------------------------------------------------------------------------------------------------------- //--------------[5]------------- /// <summary> /// pickPhysicalDevice函數最終使我們選擇的圖形顯卡存儲在類成員VkPhysicalDevice句柄中。每個GPU設備有一個VkPhysicalDevice類型的句柄。通過GPU設備的句柄,我們可以查詢GPU設備的名稱,屬性,功能等。當VkInstance銷毀時,這個對象將會被隱式銷毀,所以我們並不需要在cleanup函數中做任何操作。 /// </summary> void pickPhysicalDevice() { //[5]查詢GPU數量 uint32_t deviceCount = 0; //第一次用vkEnumeratePhysicalDevices函數找到系統中安裝的Vulkan兼容設備 vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr); //獲取圖形卡列表,deviceCount為out類型的,存儲顯卡的數量 if (deviceCount == 0) { throw std::runtime_error("failed to find GPUs with Vulkan support!"); } else { std::cout << "找到支持Vulkan的顯卡(GPU)數量:" << deviceCount << std::endl; } //[5]獲取驅動信息,分配數組存儲所有的VkPhysicalDevice句柄 std::vector<VkPhysicalDevice> devices(deviceCount); //第二次用vkEnumeratePhysicalDevices函數把數組devices.data()傳入pPhysicalDevices,便可獲得支持的[物理設備]的列表 vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()); //[5]選擇適合該程序的GPU int i = 0; for (const auto& device : devices) { //對顯卡進行評估,以確保設備可以在我們創建的表面上顯示圖像 std::cout<<"devices.size()"<< devices.size()<<std::endl; if (isDeviceSuitable(device)) { physicalDevice = device; std::cout << "i=" << i << ", True, devices.data()=" << devices[i] << std::endl; break; //如果找到合適的顯卡,即退出來 } i++; } if (physicalDevice == VK_NULL_HANDLE) { //這里的判斷條件改用devices.size()==0也是可以的 //未曾賦值,直接退出 throw std::runtime_error("failed to find a suitable GPU!"); } //or //[5]使用有序Map,通過分數自動對顯卡排序,multimap保存的有序的鍵/值對,但它可以保存重復的元素 std::multimap<int, VkPhysicalDevice> candidates; for (const auto& device : devices) { int score = rateDeviceSuitability(device); candidates.insert(std::make_pair(score, device)); std::cout << "本顯卡得分:score=" << score << std::endl; } //[5]Check if the best candidate is suitable at all,檢查最佳候選的(物理設備)是否合適 if (candidates.rbegin()->first > 0) { physicalDevice = candidates.rbegin()->second; } else { throw std::runtime_error("failed to find a suitable GPU!"); } } //---------------[5]--------------- /// <summary> /// 評估顯卡(GPU)設備是否適合該程序的要求 /// </summary> /// <param name="device"></param> /// <returns></returns> bool isDeviceSuitable(VkPhysicalDevice device) { //[5]查詢顯卡屬性,包括:名稱,支持Vulkan的版本號 VkPhysicalDeviceProperties deviceProperties; //第一個參數device為輸入參數,傳入物理設備,第二個參數deviceProperties為輸出參數,返回其屬性 vkGetPhysicalDeviceProperties(device, &deviceProperties); std::cout << "在這里面: isDeviceSuitable, deviceProperties.deviceName=" << deviceProperties.deviceName << ",deviceProperties.limits.maxComputeSharedMemorySize=" << deviceProperties.limits.maxComputeSharedMemorySize << std::endl; if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) std::cout << "本顯卡為獨立顯卡" << std::endl; else { std::cout << "本顯卡為集成顯卡" << std::endl; } //[5]擴展支持 bool extensionsSupported = checkDeviceExtensionSupport(device); //[5]swap chain support //檢測交換鏈的能力是否滿足需求,我們只能在驗證交換鏈擴展可用后查詢交換鏈的細節信息 //我們只需要交換鏈至少支持一種圖像格式和一種支持我們的窗口表面的呈現模式 bool swapChainAdequate = false; if (extensionsSupported) { std::cout << "isDeviceSuitable extensionsSupported=" << extensionsSupported << std::endl; SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); } //[5]查詢顯卡特性,包括:紋理壓縮,64位浮點述。多視口渲染(VR非常有用) //VkPhysicalDeviceFeatures deviceFeatures; //vkGetPhysicalDeviceFeatures(device, &deviceFeatures); //[5]是否為專業顯卡(a dedicated graphics card )(獨顯),或稱為離散顯卡,是否支持幾何着色器 //return deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU && deviceFeatures.geometryShader; //or QueueFamilyIndices indices = findQueueFamilies(device); //檢測交換鏈是否支持 //return indices.graphicsFamily.has_value(); //該值等於true std::cout << "isDeviceSuitable indices.graphicsFamily.has_value()=" << indices.graphicsFamily.has_value() << std::endl; //or return indices.isComplete() && extensionsSupported && swapChainAdequate; } /// <summary> /// 查詢交換鏈支持細節 /// 只檢查交換鏈是否可用還不夠,交換鏈可能與我們的窗口表面不兼容。 創建交換鏈所要進行的設置要比 Vulkan 實例和設備創建多得多,在進行交換鏈創建之前需要我們查詢更多的信息.有三種最基本的屬性,需要我們檢查: /// 1.基礎表面特性(交換鏈的最小 / 最大圖像數量,最小 / 最大圖像寬度、高度) /// 2.表面格式(像素格式,顏色空間) /// 3.可用的呈現模式 /// 與交換鏈信息查詢有關的函數都需要VkPhysicalDevice 對象和 VkSurfaceKHR作為參數, 它們是交換鏈的核心組件 /// </summary> /// <param name="device"></param> /// <returns></returns> SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) { SwapChainSupportDetails details; //[5]basic surface capabilities 基本性能 vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);//查詢基礎表面特性 //[5]the supported surface formats //查詢表面支持的格式,確保向量的空間足以容納所有格式結構體 uint32_t formatCount; vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr); if (formatCount != 0) { details.formats.resize(formatCount); vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data()); } //[5]the supported presentation modes,查詢支持的呈現模式 uint32_t presentModeCount; vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr); if (presentModeCount != 0) { details.presentModes.resize(presentModeCount); vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data()); } return details; } //Vulkan所有的操作,從繪制到加載紋理都需要將操作指令提交給一個隊列后才能執行,查找出滿足我們需求的隊列族。Vulkan 有多種不同類型的隊列,它們屬於不同的隊列族,每個隊列族的隊列只允許執行特定的一部分指令 /// <summary> /// 獲取有圖形能力的隊列族,檢測設備支持的隊列族,查找出滿足我們需求的隊列族,這一函數會返回滿足需求得隊列族的索引,隊列族是一組具有相同功能但能夠並行運行的隊列。隊列族的數量,每個族的能力以及屬於每個族的隊列的數量都是物理設備的屬性 /// </summary> /// <param name="device"></param> /// <returns></returns> QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { //檢測設備支持的隊列族,以及它們中哪些支持我們需要使用的指令 //[5]Logic to find graphics queue family QueueFamilyIndices indices; //[5]Logic to find queue family indices to populate struct with //[5]C++ 17引入了optional數據結構來區分存在或不存在的值的情況。 std::optional<uint32_t> graphicsFamily; std::cout << "000 findQueueFamilies graphicsFamily.has_value() = " << std::boolalpha << graphicsFamily.has_value() << std::endl; // false graphicsFamily = 0; //需要的隊列族只需要支持圖形指令即可 std::cout << "111 findQueueFamilies graphicsFamily.has_value() = " << std::boolalpha << graphicsFamily.has_value() << std::endl; // true uint32_t queueFamilyCount = 0; //第一次調用獲取設備的隊列族個數,然后分配數組存儲隊列族的 VkQueueFamilyProperties 對象,第一個參數為In類型的物理設備,第二個為Out類型的,返回物理設備隊列族的個數 vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr); //獲取設備的隊列族個數 std::cout << endl<<"本物理設備(顯卡)的隊列族個數:queueFamilyCount=" << queueFamilyCount << std::endl; //vkQueueFamilyProperties包含隊列族的很多信息, 比如支持的操作類型,該隊列族可以創建的隊列個數 std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount); //VkQueueFamilyProperties結構體中queueFlags的參數解釋見:https://blog.csdn.net/qq_36584063/article/details/71219188?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.topblog&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.topblog&utm_relevant_index=2 //第二次調用,返回物理設備的隊列族列表的屬性 vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data()); //我們需要找到至少一個支持VK_QUEUE_GRAPHICS_BIT的族,此系列中的隊列支持圖形操作,例如繪制點,線和三角形。 int i = 0; for (const auto& queueFamily : queueFamilies) { //[5]尋找一個隊列族,它能夠鏈接window,查找帶有呈現圖像到窗口表面能力的隊列族 VkBool32 presentSupport = false; //檢查物理設備是否具有呈現能力,查找帶有呈現圖像到窗口表面能力的隊列族 vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport); if (presentSupport) { //根據隊列族中的隊列數量和是否支持表現確定使用的表現隊列族的索引 indices.presentFamily = i; } if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) { //找到一個支持VK_QUEUE_GRAPHICS_BIT的隊列族,VK_QUEUE_GRAPHICS_BIT表示支持圖形指令 indices.graphicsFamily = i; std::cout << "i=" << i <<", queueFamily.queueCount="<<queueFamily.queueCount<<" ," << std::endl; //說明:即使繪制指令隊列族和呈現隊列族是同一個隊列族, 我們也按照它們是不同的隊列族來對待。 顯式地指定繪制和呈現隊列族是同一個的物理設備來提高性能表現。 if (indices.isComplete()) { break; } } i++; } //std::cout<<indices.graphicsFamily.value <<std::endl; return indices; } //[5] /// <summary> /// 檢測是否支持交換鏈,實際上,如果設備支持呈現隊列,那么它就一定支持交換鏈。但我們最好還是顯式地進行交換鏈擴展的檢測,然后顯式地啟用交換鏈擴展。 /// </summary> /// <param name="device"></param> /// <returns></returns> 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()); int i = 0; for (const auto& extension : availableExtensions) { std::cout << "i=" << i << ",checkDeviceExtensionSupport extension.extensionName=" << extension.extensionName << std::endl; requiredExtensions.erase(extension.extensionName); i++; } bool empty = requiredExtensions.empty(); std::cout << "requiredExtensions.empty()=" << empty << std::endl; return requiredExtensions.empty(); } /// <summary> /// 給顯卡評分,在多個顯卡都合適的情況下,給每一個設備權重值,選擇最高的一個 /// </summary> /// <param name="device"></param> /// <returns></returns> int rateDeviceSuitability(VkPhysicalDevice device) { //[5]查詢顯卡屬性,包括:ID、類型、名稱,支持Vulkan的版本號 VkPhysicalDeviceProperties deviceProperties; vkGetPhysicalDeviceProperties(device, &deviceProperties); //輸入顯卡(物理設備),得到相應屬性 std::cout << std::endl << "deviceProperties.deviceID=" << deviceProperties.deviceID << " deviceProperties.vendorID=" << deviceProperties.vendorID << " deviceProperties.deviceType=" << deviceProperties.deviceType << " deviceProperties.deviceName=" << deviceProperties.deviceName //設備名稱,我的是GeForce GTX 1660 Ti和AMD Radeon(TM) Graphics << " deviceProperties.driverVersion=" << deviceProperties.driverVersion //包含用於控制設備的驅動程序的版本 << " deviceProperties.apiVersion=" << deviceProperties.apiVersion //設備支持的最高版本的Vulkan << std::endl; int score = 0; //[5]離散GPU具有顯著的性能優勢 if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { score += 1000; std::cout << "有離散GPU,分值增加1000" << std::endl; } else { std::cout << "無離散GPU, VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU(有集成GPU), deviceProperties.deviceType=" << deviceProperties.deviceType << std::endl; } //[5]支持紋理的最大值,影響圖形質量 score += deviceProperties.limits.maxImageDimension2D; printf("支持的最大1D紋理值:deviceProperties.limits.maxImageDimension1D=%d\n", deviceProperties.limits.maxImageDimension1D); printf("支持的最大2D紋理值:deviceProperties.limits.maxImageDimension2D=%d\n", deviceProperties.limits.maxImageDimension2D); printf("支持的最大3D紋理值:deviceProperties.limits.maxImageDimension3D=%d\n", deviceProperties.limits.maxImageDimension3D); printf("支持的最大DimensionCube:deviceProperties.limits.maxImageDimensionCube=%d\n", deviceProperties.limits.maxImageDimensionCube); //[5]查詢顯卡特性,包括:紋理壓縮,64位浮點述。多視口渲染(VR) VkPhysicalDeviceFeatures deviceFeatures; //第一個參數為In類型的,傳入物理設備,第二個參數為Out類型的,返回設備特點的結構體 vkGetPhysicalDeviceFeatures(device, &deviceFeatures); std::cout << "deviceFeatures.multiViewport=" << deviceFeatures.multiViewport << " deviceFeatures.textureCompressionETC2=" << deviceFeatures.textureCompressionETC2 << " deviceFeatures.textureCompressionBC=" << deviceFeatures.textureCompressionBC << std::endl; //[5]不支持幾何着色器 if (!deviceFeatures.geometryShader) { score = 0; //return 0; } else { std::cout << "支持幾何着色器 score=" << score << std::endl; } if (deviceFeatures.tessellationShader) { std::cout << "支持曲面細分着色器,tessellationShader" << std::endl; } return score; } /// <summary> /// 創建邏輯設備以作為物理設備的交互接口,邏輯設備用於描述可以使用的物理設備的特性。邏輯設備創建過程與instance創建過程類似,也需要描述我們需要使用的功能。因為我們已經查詢過哪些隊列簇可用,在這里需要進一步為邏輯設備創建具體類型的命令隊列。如果有不同的需求,也可以基於同一個物理設備創建多個邏輯設備。 /// </summary> void createLogicalDevice() { //獲取帶有圖形能力的隊列族 QueueFamilyIndices indices = findQueueFamilies(physicalDevice); VkDeviceQueueCreateInfo queueCreateInfo = {}; //創建邏輯設備需要填寫結構體,它描述了針對一個隊列族我們所需的隊列數量 queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value(); //目前而言,對於每個隊列族,驅動程序只允許創建很少數量的隊列,但實際上,對於每一個隊列族,我們很少需要一個以上的隊列。我們可以在多個線程創建指令緩沖,然后在主線程一次將它們全部提交,降低調用開銷。 Vulkan需要我們賦予隊列一個0.0到1.0之間的浮點數作為優先級來控制指令緩沖的執行順序。 queueCreateInfo.queueCount = 1; //[6]Vulkan使用0.0到1.0之間的浮點數為隊列分配優先級, 來進行緩沖區執行的調度。即使只有一個隊列,這也是必需的,顯式地賦予隊列優先級: float queuePriority = 1.0f; queueCreateInfo.pQueuePriorities = &queuePriority; //[6]device features,指定應用程序使用的設備特性 VkPhysicalDeviceFeatures deviceFeatures = {}; VkDeviceCreateInfo createInfo = {}; //填寫邏輯設備的結構體 createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; createInfo.pQueueCreateInfos = &queueCreateInfo; //pQueueCreateInfos 指針指向 queueCreateInfo 的地址 createInfo.queueCreateInfoCount = 1; createInfo.pEnabledFeatures = &deviceFeatures; //pEnabledFeatures 指針指向 deviceFeatures 的地址 //[6]VK_KHR_swapchain 將該設備的渲染圖像顯示到windows //[6]之前版本Vulkan實現對實例和設備特定的驗證層進行了區分,但現在情況不再如此。 //[6]這意味着VkDeviceCreateInfo的enabledLayerCount和ppEnabledLayerNames字段被最新的實現忽略。不過,還是應該將它們設置為與較舊的實現兼容: createInfo.enabledExtensionCount = 0; if (enableValidationLayers) { //以對設備和 Vulkan 實例使用相同地校驗層 createInfo.enabledLayerCount = static_cast<uint32_t>(validationLayers.size()); createInfo.ppEnabledLayerNames = validationLayers.data(); std::cout << "createLogicalDevice(),有啟用enableValidationLayers" << std::endl; } else {//進入了該分支 createInfo.enabledLayerCount = 0; std::cout << "createLogicalDevice(),沒有啟用enableValidationLayers" << std::endl; } //[6]create a queue from both families std::vector<VkDeviceQueueCreateInfo> queueCreateInfos; std::set<uint32_t> uniqueQueueFamilies = { indices.graphicsFamily.value(), indices.presentFamily.value() }; queuePriority = 1.0f; for (uint32_t queueFamily : uniqueQueueFamilies) { //描述了針對一個隊列族我們所需的隊列數量。填寫[邏輯設備]的隊列的構造信息 VkDeviceQueueCreateInfo queueCreateInfo = {}; //創建所有使用到的隊列族 queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; queueCreateInfo.pNext = nullptr; //我添加的 queueCreateInfo.queueFamilyIndex = queueFamily; //有圖形能力的隊列 queueCreateInfo.queueCount = 1; //申請使用隊列的數量 queueCreateInfo.pQueuePriorities = &queuePriority; queueCreateInfos.push_back(queueCreateInfo); //push_back會負責將一個值當成vector對象的尾元素“壓到(push)”vector對象的“尾端(back)” } //[6]將隊列信息加入驅動info createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size()); createInfo.pQueueCreateInfos = queueCreateInfos.data(); //[6]開啟擴展支持,啟用交換鏈 createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size()); createInfo.ppEnabledExtensionNames = deviceExtensions.data(); //[6]創建邏輯設備,參數physicalDevice, &createInfo, nullptr為in類型的,device為Out類型的,返回邏輯設備 /** vkCreateDevice 函數的參數: 1.創建的邏輯設備進行交互的物理設備對象 2.指定的需要使用的隊列信息 3.可選的分配器回調,若設置為nullptr,則使用vulkan內置的內存分配器 4.用來存儲返回的邏輯設備對象的內存地址 邏輯設備並不直接與Vulkan實例交互,所以創建邏輯設備時不需要使用Vulkan實例作為參數 */ if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) { throw std::runtime_error("failed to create logical device!"); } //[6]獲取驅動隊列 //獲取指定隊列族的隊列句柄 //它的參數依次是邏輯設備對象,隊列族索引,隊列索引,用來存儲返回的隊列句柄的內存地址 //因為我們只從這個族中創建一個隊列,所以我們只使用索引0 vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue); //[6]顯示隊列 vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue); } //[6]---------------------------------------------------------------------------- /// <summary> /// 交換鏈是渲染目標的集合,Vulkan 沒有默認幀緩沖的概念,它需要一個能夠緩沖渲染操作的組件。在 Vulkan 中,這一組件就是交換鏈。Vulkan 的交換鏈必須顯式地創建,不存在默認的交換鏈。交換鏈本質上是一個包含了若干等待呈現的圖像隊列。我們的應用程序從交換鏈獲取一張圖像,然后在圖像上進行渲染操作,完成后,將圖像返回到交換鏈的隊列中。交換鏈的隊列的工作方式和它呈現圖像到表面的條件依賴於交換鏈的設置。但通常來說,交換鏈被用來同步圖像呈現和屏幕刷新 /// </summary> void createSwapChain() { SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice); VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats); VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes); VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities); //[7]簡單地堅持這個最小值意味着我們有時可能需要等待驅動程序完成內部操作, //[7]然后才能獲取另一個要渲染的圖像。因此,建議請求至少比最小值多一個圖像: //[7]uint32_t imageCount = swapChainSupport.capabilities.minImageCount; /** 設置包括交換鏈中的圖像個數,也就是交換鏈的隊列可以容納的圖像個數。 我們使用交換鏈支持的最小圖像個數 +1 數量的圖像來實現三倍緩沖。maxImageCount 的值為 0 表明,只要內存可以滿足,我們可以使用任意數量的圖像。 */ uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1; if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) { imageCount = swapChainSupport.capabilities.maxImageCount; } //創建交換對象需要的結構體 VkSwapchainCreateInfoKHR createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; createInfo.surface = surface; createInfo.minImageCount = imageCount; createInfo.imageFormat = surfaceFormat.format; createInfo.imageColorSpace = surfaceFormat.colorSpace; createInfo.imageExtent = extent; //[7]imageArrayLayers指定每個圖像包含的層的數量。除非您正在開發3D應用程序,否則該值始終為1。 //指定每個圖像所包含的層次.但對於 VR 相關的應用程序來說,會使用更多的層次 createInfo.imageArrayLayers = 1; //指定我們將在圖像上進行怎樣的操作--這里是作為傳輸的目的圖像 createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; //[7]兩種方法可以處理從多個隊列訪問的圖像: //[7]VK_SHARING_MODE_CONCURRENT //[7]VK_SHARING_MODE_EXCLUSIVE cout << "in createSwapChain()" << endl; /** 指定在多個隊列族使用交換鏈圖像的方式。這一設置對於圖形隊列和呈現隊列不是同一個隊列的情況有着很大影響。 我們通過圖形隊列在交換鏈圖像上進行繪制操作,然后將圖像提交給呈現隊列來顯示。有兩種控制在多個隊列訪問圖像的方式: VK_SHARING_MODE_EXCLUSIVE:一張圖像同一時間只能被一個隊列族所擁有, 在另一隊列族使用它之前,必須顯式地改變圖像所有權。這一模式下性能表現最佳。 VK_SHARING_MODE_CONCURRENT:圖像可以在多個隊列族間使用,不需要顯式地改變圖像所有權。 如果圖形和呈現不是同一個隊列族,我們使用協同模式來避免處理圖像所有權問題。協同模式需要我們使用 queueFamilyIndexCount 和pQueueFamilyIndices 來指定共享所有權的隊列族。 如果圖形隊列族和呈現隊列族是同一個隊列族 (大部分情況下都是這樣), 我們就不能使用協同模式,協同模式需要我們指定至少兩個不同的隊列族。 */ QueueFamilyIndices indices = findQueueFamilies(physicalDevice); uint32_t queueFamilyIndices[] = { indices.graphicsFamily.value(), indices.presentFamily.value() }; std::cout << "---------indices.graphicsFamily=" << indices.graphicsFamily.value() << ",indices.presentFamily=" << indices.presentFamily.value()<<", indices.graphicsFamily.has_value="<< indices.graphicsFamily.has_value() << ", indices.graphicsFamily.has_value()="<< indices.presentFamily.has_value() << std::endl; 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 } //[7]指定對交換鏈中的圖像應用某種變換,我們可以為交換鏈中的圖像指定一個固定的變換操作(需要交換鏈具有supportedTransforms 特性),比如順時針旋轉 90 度或是水平翻轉。如果不需要進行任何變換操作,指定使用 currentTransform 變換即可。 createInfo.preTransform = swapChainSupport.capabilities.currentTransform; //[7]alpha channel should be used for blending //成員變量用於指定 alpha 通道是否被用來和窗口系統中的其它窗口進行混合操作。 通常,我們將其設置為 VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR來忽略掉alpha通道。 createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; createInfo.presentMode = presentMode; //設置呈現模式 createInfo.clipped = VK_TRUE; //設置為True時,表示我們不關心被窗口系統中的其它窗口遮擋的像素的顏色 //[7]窗口重置是取緩存區圖像方式,oldSwapchain需要指定它,是因為應用程序在運行過程中交換鏈可能會失效。 比如,改變窗口大小后,交換鏈需要重建,重建時需要之前的交換鏈 createInfo.oldSwapchain = VK_NULL_HANDLE; //創建交換鏈 if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) { throw std::runtime_error("failed to create swap chain!"); } /** 我們在創建交換鏈時指定了一個minImageCount成員變量來請求最小需要的交換鏈圖像數量。 Vulkan 的具體實現可能會創建比這個最小交換鏈圖像數量更多的交換鏈圖像,我們在這里,我們仍然需要顯式地查詢交換鏈圖像數量,確保不會出錯。 */ //獲取交換鏈圖像句柄 vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr); swapChainImages.resize(imageCount); vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data()); //存儲我們設置的交換鏈圖像格式和范圍 swapChainImageFormat = surfaceFormat.format; swapChainExtent = extent; } //[7],選擇適當的表面格式 /** 每一個 VkSurfaceFormatKHR 條目包含了一個 format 和 colorSpace成員變量 *format 成員變量用於指定顏色通道和存儲類型,如VK_FORMAT_B8G8R8A8_UNORM 表示我們以B,G,R 和 A 的順序, 每個顏色通道用 8 位無符號整型數表示,總共每像素使用 32 位表示 * colorSpace 成員變量用來表示 SRGB 顏色空間是否被支持,是否使用 VK_COLOR_SPACE_SRGB_NONLINEAR_KHR 標志。對於顏色空間,如果SRGB被支持,我們就使用SRGB,它可以得到更加准確的顏色表示 * 這里使用VK_FORMAT_B8G8R8A8_UNORM表示RGB 作為顏色格式 */ VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) { for (const auto& availableFormat : availableFormats) { //檢測格式列表中是否有我們想要設定的格式是否存在 if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { return availableFormat; } } //[12]如果查詢失敗返回第一個,如果不能在列表中找到我們想要的格式,我們可以對列表中存在的格式進行打分,選擇分數最高的那個作為我們使用的格式,當然,大多數情況下,直接使用列表中的第一個格式也是非常不錯的選擇。 return availableFormats[0]; } /// <summary> /// 查找最佳的可用呈現模式,呈現模式是交換鏈中最重要的設置,它決定了什么條件下圖像才會顯示到屏幕。Vulkan提供了4種可用的呈現模式: /// [7]VK_PRESENT_MODE_IMMEDIATE_KHR,應用程序提交的圖像會被立即傳輸到屏幕上,可能會導致撕裂現象 // [7]VK_PRESENT_MODE_FIFO_KHR,保證一定可用交換鏈變成一個先進先出的隊列,每次從隊列頭部取出一張圖像進行顯示,應用程序渲染的圖像提交給交換鏈后,會被放在隊列尾部。當隊列為滿時,應用程序需要進行等待。這一模式非常類似現在常用的垂直同步。刷新顯示的時刻也被叫做垂直回掃。 // [7]VK_PRESENT_MODE_FIFO_RELAXED_KHR 這一模式和上一模式的唯一區別是,如果應用程序延遲,導致交換鏈的隊列在上一次垂直回掃時為空, 那么,如果應用程序在下一次垂直回掃前提交圖像,圖像會立即被顯示。這一模式可能會導致撕裂現象 // [7]VK_PRESENT_MODE_MAILBOX_KHR,表現最佳,這一模式是第二種模式的另一個變種。它不會在交換鏈的隊列滿時阻塞應用程序,隊列中的圖像會被直接替換為應用程序新提交的圖像。這一模式可以用來實現三倍緩沖,避免撕裂現象的同時減小了延遲問題 /// </summary> /// <param name="availablePresentModes"></param> /// <returns></returns> VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) { //[7]三級緩存更好,如果有就開啟 for (const auto& availablePresentMode : availablePresentModes) { if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) { return availablePresentMode; } } return VK_PRESENT_MODE_FIFO_KHR; } /// <summary> /// 在minImageExtent和maxImageExtent內選擇與窗口最匹配的分辨率,設置交換范圍。 /// 交換范圍是交換鏈中圖像的分辨率,它幾乎總是和我們要顯示圖像的窗口的分辨率相同。 VkSurfaceCapabilitiesKHR 結構體定義了可用的分辨率范圍。 Vulkan 通過 currentExtent 成員變量來告知適合我們窗口的交換范圍。 一些窗口系統會使用一個特殊值,uint32_t 變量類型的最大值, 表示允許我們自己選擇對於窗口最合適的交換范圍,但我們選擇的交換范圍需要在 minImageExtent 與 maxImageExtent 的范圍內。 /// 代碼中 max 和 min 函數用於在允許的范圍內選擇交換范圍的高度值和寬度值,需要在源文件中包含 algorithm 頭文件才能夠使用它們 /// </summary> /// <param name="capabilities"></param> /// <returns></returns> VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) { if (capabilities.currentExtent.width != UINT32_MAX) { return capabilities.currentExtent; } else { //[16]為了正確地處理窗口大小調整,還需要查詢幀緩沖區的當前窗口大小,以確保交swap chain(new)中圖像大小正確。 int width, height; glfwGetFramebufferSize(window, &width, &height); VkExtent2D actualExtent = { static_cast<uint32_t>(width), static_cast<uint32_t>(height) }; //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; } } //[7]-------------------------------------------------- //[8]--------ImageView,圖像視圖用於指定圖像的大小--------------------------------------- void createImageViews() { swapChainImageViews.resize(swapChainImages.size()); //分配足夠的數組空間來存儲圖像視圖 for (size_t i = 0; i < swapChainImages.size(); i++) //遍歷所有交換鏈圖像,創建圖像視圖 {//設置圖像視圖結構體相關信息 VkImageViewCreateInfo createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; createInfo.image = swapChainImages[i]; //[8]選擇視圖類型 1D 2D 3D //viewType 和 format 成員變量用於指定圖像數據的解釋方式。 //viewType用於指定圖像被看作是一維紋理、二維紋理、三維紋理還是立方體貼圖 createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; createInfo.format = swapChainImageFormat; //[8]components字段允許旋轉顏色通道。 //components 成員變量用於進行圖像顏色通道的映射。比如,對於單色紋理,我們可以將所有顏色通道映射到紅色通道。我們也可以直接將顏色通道的值映射為常數 0 或 1。在這里, 我們只使用默認的映射 createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; //default createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; //[8]描述圖像的用途以及應訪問圖像的哪個部分。 //subresourceRange用於指定圖像的用途和圖像的哪一部分可以被訪問 //在這里,我們的圖像被用作渲染目標,並且沒有細分級別,只存在一個圖層 createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; createInfo.subresourceRange.baseMipLevel = 0; createInfo.subresourceRange.levelCount = 1; createInfo.subresourceRange.baseArrayLayer = 0; createInfo.subresourceRange.layerCount = 1; //如果讀者在編寫 VR 一類的應用程序,可能會使用支持多個層次的交換鏈。這時,讀者應該為每個圖像創建多個圖像視圖,分別用來訪問左眼和右眼兩個不同的圖層 if (vkCreateImageView(device, &createInfo, nullptr, &swapChainImageViews[i]) != VK_SUCCESS) {//創建圖像視圖,有了圖像視圖,就可以將圖像作為紋理使用,但作為渲染目標,還需要幀緩沖對象 throw std::runtime_error("failed to create image views!"); } } } //[8]-------------------------------------------------------------------------------------------------------- //[9]-------------------------------------------------------------------------------------------------------- void createRenderPass() { VkAttachmentDescription colorAttachment = {}; //[9]colorAttachment的format應與swapChain圖像的格式匹配 colorAttachment.format = swapChainImageFormat; //[9]沒有使用多重采樣(multisampling),將使用1個樣本。 colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; //[9]clear the framebuffer to black before drawing a new frame //[9]VK_ATTACHMENT_LOAD_OP_LOAD 保留Attachment的現有內容 //[9]VK_ATTACHMENT_LOAD_OP_CLEAR 開始時將值初始化為常數 //[9]VK_ATTACHMENT_LOAD_OP_DONT_CARE 現有內容未定義; 忽略 colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; //[9]color and depth data //[9]VK_ATTACHMENT_STORE_OP_STORE 渲染的內容將存儲在內存中,以后可以讀取 //[9]VK_ATTACHMENT_STORE_OP_DONT_CARE 渲染操作后,幀緩沖區的內容將是未定義的 colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; //[9]stencil data colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; //[9]Textures and framebuffers //[9]指定在渲染開始之前圖像將具有的布局。 colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; //[9]指定在渲染完成時自動過渡到的布局。 //[9]VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL used as color attachment //[9]VK_IMAGE_LAYOUT_PRESENT_SRC_KHR Images the swap chain 中要顯示的圖像 //[9]VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL 用作存儲器復制操作目的圖像 colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; //[9]引用Attachment VkAttachmentReference colorAttachmentRef = {}; //[9]attachment參數通過attachment描述數組中的索引指定要引用的attachment colorAttachmentRef.attachment = 0; //[9]布局指定了我們希望attachment在使用此引用的subpass中具有哪種布局。 colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; //[9]Vulkan may also support compute subpasses in the future VkSubpassDescription subpass = {}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; //[9]引用colorAttachment //[9]數組中attachment的索引是直接從fragment shader 引用的(location = 0)out vec4 outColor指令! //[9]subpass可以引用以下其他類型的attachment //[9]pInputAttachments read from a shader //[9]pResolveAttachments used for multisampling attachments //[9]pDepthStencilAttachment depth and stencil data //[9]pPreserveAttachments not used by this subpass but for which the data must be preserved(保存) subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &colorAttachmentRef; //[9] 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!"); } } //[9]---------------------------------------------------------------------------------------- /// <summary> /// 10 圖形管線:描述圖形顯卡分配的狀態,例如視口大小和深度緩沖操作和着色器 /////圖形管線是一系列將我們提交的頂點和紋理轉換為渲染目標上的像素的操作。它的簡化過程如下: // input assembler:獲取頂點數據,頂點數據的來源可以是應用程序提交的原始頂點數據,或根據索引緩沖提取的頂點數據。 // vertex shader 對每個頂點進行模型空間到屏幕空間的變換,然后將頂點數據傳遞給圖形管線的下一階段。 // tessellation shaders 根據一定的規則對幾何圖形進行細分,從而提高網格質量。通常被用來使類似牆面這類不光滑表面看起來更自然。 // geometry shader 可以以圖元(三角形,線段,點) 為單位處理幾何圖形,它可以剔除圖元,輸出圖元。有點類似於 tessellation shader,但更靈活。但目前已經不推薦應用程序使用它,geometry shader 的性能在除了Intel 集成顯卡外的大多數顯卡上表現不佳。 // rasterization 階段將圖元離散為片段。片段被用來在幀緩沖上填充像素。位於屏幕外的片段會被丟棄,頂點着色器輸出的頂點屬性會在片段之間進行插值,開啟深度測試后,位於其它片段之后的片段也會被丟棄。 // fragment shader 對每一個未被丟棄的片段進行處理,確定片段要寫入的幀緩沖,它可以使用來自 vertex shader 的插值數據,比如紋理坐標和頂點法線。 // color blending 階段對寫入幀緩沖同一像素位置的不同片段進行混合操作。片段可以直接覆蓋之前寫入的片段,也可以基於之前片段寫入的信息進行混合操作。 // 原文鏈接:https ://blog.csdn.net/yuxing55555/article/details/88976664 /// </summary> void createGraphicsPipeline() { auto vertShaderCode = readFile("vert.spv"); auto fragShaderCode = readFile("frag.spv"); //[10] VkShaderModule vertShaderModule = createShaderModule(vertShaderCode); VkShaderModule fragShaderModule = createShaderModule(fragShaderCode); //[10]可以將多個着色器組合到一個ShaderModule中,並使用不同的entry points來區分它們的行為。 VkPipelineShaderStageCreateInfo vertShaderStageInfo = {}; vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; vertShaderStageInfo.module = vertShaderModule; vertShaderStageInfo.pName = "main"; //[10]指定着色器常量的值。 //[10]單個着色器模塊,在管道中創建常量,給予不同的值來配置其行為。 //[10]比在渲染時使用變量配置着色器更為有效, 編譯器可以進行優化,例如消除依賴於這些值的if語句 //[10]如果沒有這樣的常量,則可以將成員設置為nullptr,初始化會自動執行該操作。 //[10]pSpecializationInfo //[10] VkPipelineShaderStageCreateInfo fragShaderStageInfo = {}; fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT; fragShaderStageInfo.module = fragShaderModule; fragShaderStageInfo.pName = "main"; //[10] VkPipelineShaderStageCreateInfo shaderStages[] = { vertShaderStageInfo, fragShaderStageInfo }; /** 描述內容主要包括下面兩個方面: 綁定:數據之間的間距和數據是按逐頂點的方式還是按逐實例的方式進行組織 屬性描述:傳遞給頂點着色器的屬性類型,用於將屬性綁定到頂點着色器中的變量 由於我們直接在頂點着色器中硬編碼頂點數據,這里不載入任何頂點數據 */ //[10]//描述傳遞給頂點着色器的頂點數據格式--頂點輸入 VkPipelineVertexInputStateCreateInfo vertexInputInfo = {}; vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; vertexInputInfo.vertexBindingDescriptionCount = 0; //[10]描述上述用於加載頂點數據的詳細信息 //用於指向描述頂點數據組織信息地結構體數組 vertexInputInfo.pVertexBindingDescriptions = nullptr; vertexInputInfo.vertexAttributeDescriptionCount = 0; //[10]描述上述用於加載頂點數據的詳細信息 vertexInputInfo.pVertexAttributeDescriptions = nullptr; //[10] VkPipelineInputAssemblyStateCreateInfo inputAssembly = {}; inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; inputAssembly.primitiveRestartEnable = VK_FALSE; //[10]viewport VkViewport viewport = {}; viewport.x = 0.0f; viewport.y = 0.0f; viewport.width = (float)swapChainExtent.width; viewport.height = (float)swapChainExtent.height; //[10]minDepth和maxDepth 在0.0f到1.0f之間 viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; //[10]裁剪矩形定義哪些區域像素被存儲 VkRect2D scissor = {}; scissor.offset = { 0, 0 }; scissor.extent = swapChainExtent; //[10]viewport 和scissor可以有多個 VkPipelineViewportStateCreateInfo viewportState = {}; viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; viewportState.viewportCount = 1; viewportState.pViewports = &viewport; viewportState.scissorCount = 1; viewportState.pScissors = &scissor; VkPipelineRasterizationStateCreateInfo rasterizer = {}; rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; //[10]保留近平面和遠平面之外的片元 rasterizer.depthClampEnable = VK_FALSE; //[10]true 幾何圖形不會通過光柵化階段。 禁用對幀緩沖區的任何輸出 rasterizer.rasterizerDiscardEnable = VK_FALSE; //[10]VK_POLYGON_MODE_FILL 用片段填充多邊形區域 //[10]VK_POLYGON_MODE_LINE 多邊形邊緣繪制為線 //[10]VK_POLYGON_MODE_POINT 多邊形頂點繪制為點 rasterizer.polygonMode = VK_POLYGON_MODE_FILL; //[10]支持的最大線寬取決於硬件,任何比1.0f粗的線都需要啟用wideLines。 rasterizer.lineWidth = 1.0f; //[10]確定要使用的面部剔除類型。 rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; //[10]指定面片視為正面的頂點順序,可以是順時針或逆時針 rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE; //[10]Rasterization可以通過添加常量或基於片段的斜率對深度值進行偏置來更改深度值。 //[10]多用於陰影貼圖,如不需要將depthBiasEnable設置為VK_FALSE。 rasterizer.depthBiasEnable = VK_FALSE; rasterizer.depthBiasConstantFactor = 0.0f; rasterizer.depthBiasClamp = 0.0f; rasterizer.depthBiasSlopeFactor = 0.0f; //[10] VkPipelineMultisampleStateCreateInfo multisampling = {}; multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; multisampling.sampleShadingEnable = VK_FALSE; multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; multisampling.minSampleShading = 1.0f; // Optional multisampling.pSampleMask = nullptr; // Optional multisampling.alphaToCoverageEnable = VK_FALSE; // Optional multisampling.alphaToOneEnable = VK_FALSE; // Optional //[10]specification詳見說明文檔 //[10]每個附加的幀緩沖區的混合規則 VkPipelineColorBlendAttachmentState colorBlendAttachment = {}; colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; //[6]片段着色器的顏色直接輸出 colorBlendAttachment.blendEnable = VK_FALSE; colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; /* if (blendEnable) { finalColor.rgb = (srcColorBlendFactor * newColor.rgb) < colorBlendOp > (dstColorBlendFactor * oldColor.rgb); finalColor.a = (srcAlphaBlendFactor * newColor.a) < alphaBlendOp > (dstAlphaBlendFactor * oldColor.a); } else { finalColor = newColor; } finalColor = finalColor & colorWriteMask; */ //[10]透明混合 /* finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor; finalColor.a = newAlpha.a; */ //[10]VK_TRUE colorBlendAttachment.blendEnable = VK_TRUE; colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; //[10]全局顏色混合設置。 VkPipelineColorBlendStateCreateInfo colorBlending = {}; colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; //[10]bitwise combination 請注意,這將自動禁用第一種方法 colorBlending.logicOpEnable = VK_FALSE; colorBlending.logicOp = VK_LOGIC_OP_COPY; // Optional colorBlending.attachmentCount = 1; colorBlending.pAttachments = &colorBlendAttachment; colorBlending.blendConstants[0] = 0.0f; // Optional colorBlending.blendConstants[1] = 0.0f; // Optional colorBlending.blendConstants[2] = 0.0f; // Optional colorBlending.blendConstants[3] = 0.0f; // Optional //[10] VkDynamicState dynamicStates[] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_LINE_WIDTH }; //[10] VkPipelineDynamicStateCreateInfo dynamicState = {}; dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; dynamicState.dynamicStateCount = 2; dynamicState.pDynamicStates = dynamicStates; //[10] VkPipelineLayoutCreateInfo pipelineLayoutInfo = {}; pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; pipelineLayoutInfo.setLayoutCount = 0; // Optional pipelineLayoutInfo.pSetLayouts = nullptr; // Optional pipelineLayoutInfo.pushConstantRangeCount = 0; // Optional pipelineLayoutInfo.pPushConstantRanges = nullptr; // Optional //[10] if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) { throw std::runtime_error("failed to create pipeline layout!"); } //[10] VkGraphicsPipelineCreateInfo pipelineInfo = {}; pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; pipelineInfo.stageCount = 2; pipelineInfo.pStages = shaderStages; 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 pipelineInfo.layout = pipelineLayout; //[10]引用將使用圖形管線的renderPass和subpass索引。 //[10]也可以使用其他渲染管線,但是它們必須與renderPass兼容 pipelineInfo.renderPass = renderPass; pipelineInfo.subpass = 0; //[10]Vulkan允許通過從現有管線中派生來創建新的圖形管線。 //[10]管線派生的思想是,當管線具有與現有管線共有的許多功能時,建立管線的成本較低, //[10]並且可以更快地完成同一父管線之間的切換。 //[10]可以使用basePipelineHandle指定現有管線的句柄,也可以使用basePipelineIndex引用索引創建的另一個管線。 //[10]現在只有一個管線,因此我們只需指定一個空句柄和一個無效索引。 //[10]僅當在VkGraphicsPipelineCreateInfo的flags字段中還指定了VK_PIPELINE_CREATE_DERIVATIVE_BIT標志時,才使用這些值。 pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional pipelineInfo.basePipelineIndex = -1; // Optional //[10]第二個參數VK_NULL_HANDLE引用了一個可選的VkPipelineCache對象。 //[10]管線Cache可用於存儲和重用管線創建相關的數據 //[10]可以加快管線的創建速度,后有詳解 if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) { throw std::runtime_error("failed to create graphics pipeline!"); } //[10] //在創建圖形管線之前,不會將SPIR-V字節碼編譯並鏈接到機器代碼以供GPU執行。 //這意味着在管道創建完成后,就可以立即銷毀ShaderModule vkDestroyShaderModule(device, fragShaderModule, nullptr); vkDestroyShaderModule(device, vertShaderModule, nullptr); } VkShaderModule createShaderModule(const std::vector<char>& code) { VkShaderModuleCreateInfo createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; createInfo.codeSize = code.size(); createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data()); VkShaderModule shaderModule; if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) { throw std::runtime_error("failed to create shader module!"); } return shaderModule; } //[10]-------------------------------------------------------------------------- //[11]-------------------------------------------------------------------------- /// <summary> /// 創建幀緩沖對象,幀緩沖區保存圖像視圖來指定使用的顏色,深度和模板 /// </summary> void createFramebuffers() { //[11]調整容器大小以容納所有幀緩沖區 swapChainFramebuffers.resize(swapChainImageViews.size()); //分配足夠的空間來存儲所有幀緩沖對象 //[11] create framebuffers for (size_t i = 0; i < swapChainImageViews.size(); i++) { //為交換鏈的每一個圖像視圖對象創建對應的幀緩沖 VkImageView attachments[] = { swapChainImageViews[i] }; VkFramebufferCreateInfo framebufferInfo = {}; framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; //指定幀緩沖需要兼容的渲染流程對象. 之后的渲染操作,我們可以使用與這個指定的渲染流程對象相兼容的其它渲染流程對象.一般來說,使用相同數量,相同類型附着的渲染流程對象是相兼容的。 framebufferInfo.renderPass = renderPass; framebufferInfo.attachmentCount = 1; //指定附着個數 framebufferInfo.pAttachments = attachments; //指定渲染流程對象用於描述附着信息的 pAttachment 數組 //指定幀緩沖的大小 framebufferInfo.width = swapChainExtent.width; framebufferInfo.height = swapChainExtent.height; framebufferInfo.layers = 1; //指定圖像層數,這里的交換鏈圖像都是單層的 if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &swapChainFramebuffers[i]) != VK_SUCCESS) { throw std::runtime_error("failed to create framebuffer!"); } } } //[11]------------------------------------------------------------------------------- //[12]------------------------------------------------------------------------------- //Vulkan 下的指令,比如繪制指令和內存傳輸指令並不是直接通過函數調用執行的。我們需要將所有要執行的操作記錄在一個指令緩沖對象,然后提交給可以執行這些操作的隊列才能執行。這使得我們可以在程序初始化時就准備好所有要指定的指令序列,在渲染時直接提交執行。也使得多線程提交指令變得更加容易。我們只需要在需要指定執行的使用,將指令緩沖對象提交給 Vulkan 處理接口。 /// <summary> /// Command pool 分配command buffers,創建指令緩沖池 /// 指令緩沖對象在被提交給我們之前獲取的隊列后,被 Vulkan 執行。每個指令池對象分配的指令緩沖對象只能提交給一個特定類型的隊列。在這 里,我們使用的是繪制指令,它可以被提交給支持圖形操作的隊列。 //有下面兩種用於指令池對象創建的標記, 可以提供有用的信息給Vulkan的驅動程序進行一定優化處理: //VK_COMMAND_POOL_CREATE_TRANSIENT_BIT: 使用它分配的指令緩沖對象被頻繁用來記錄新的指令 (使用這一標記可能會改變幀緩沖對象的內存分配策略) //VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT: 指令緩沖對象之間相互獨立,不會被一起重置。 不使用這一標記,指令緩沖對象會被放在一起重置 /// </summary> void createCommandPool() { std::cout << "in createCommandPool" << std::endl; QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice); VkCommandPoolCreateInfo poolInfo = {}; //指令池的創建只需要2個參數 poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value(); //[12]There are two possible flags for command pools //[12]VK_COMMAND_POOL_CREATE_TRANSIENT_BIT 提示CommandPool經常用新命令重新記錄(可能會改變內存分配行為) //[12]VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT 允許單獨重新記錄命CommandPool,否則都必須一起重置 poolInfo.flags = 0; //可選的不使用flag //指令池對象的創建 if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) { throw std::runtime_error("failed to create command pool!"); } } //[13]--------------------------------------------------------------------------------------- /// <summary> /// Command buffers用於記錄指令操作,比如渲染 /// 指令緩沖對象,用來記錄繪制指令。由於繪制操作是在幀緩沖上進行的,我們需要為交換鏈中的每一個圖像分配一個指令緩沖對象 /// </summary> void createCommandBuffers() { commandBuffers.resize(swapChainFramebuffers.size()); //指定分配使用的指令池和需要分配的指令緩沖對象個數 //VkCommandBuffer需要使用VkCommandPool來分配。我們可以為每個線程使用一個獨立的VkCommandPool來避免進行同步,不同VkCommandPool使用自己的內存資源分配VkCommandBuffer。開始記錄VkCommandBuffer后,調用的GPU指令,會被寫入VkCommandBuffer。等待提交給隊列執行 VkCommandBufferAllocateInfo allocInfo = {}; allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; allocInfo.commandPool = commandPool; /** level 成員變量用於指定分配的指令緩沖對象是主要指令緩沖對象或者是輔助指令緩沖對象: VK_COMMAND_BUFFER_LEVEL_PRIMARY: 可以被提交到隊列進行執行,但不能被其它指令緩沖對象調用。 VK_COMMAND_BUFFER_LEVEL_SECONDARY: 不能直接被提交到隊列進行執行,但可以被主要指令緩沖對象調用執行 在這里,我們沒有使用輔助指令緩沖對象,但輔助治理給緩沖對象的好處是顯而易見的,我們可以把一些常用的指令存在輔助指令緩沖對象, 然后在主要指令緩沖對象中調用執行 */ //[13]指定primary 或 secondary command buffers. allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; allocInfo.commandBufferCount = (uint32_t)commandBuffers.size(); //分配指令緩沖對象 if (vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()) != VK_SUCCESS) { throw std::runtime_error("failed to allocate command buffers!"); } //[13]recording a command buffer //記錄指令到指令緩沖 for (size_t i = 0; i < commandBuffers.size(); i++) {//指定一些有關指令緩沖的使用細節 VkCommandBufferBeginInfo beginInfo = {}; beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; /** flags 成員變量用於指定我們將要怎樣使用指令緩沖。它的值可以是下面這些: VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT: 指令緩沖在執行一次后,就被用來記錄新的指令. VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT: 這是一個只在一個渲染流程內使用的輔助指令緩沖. VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT: 在指令緩沖等待執行時,仍然可以提交這一指令緩沖 */ //[13]指定指定我們將如何使用command buffers. //[13]VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT 在執行一次后立即rerecord。 //[13]VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT 這是一個輔助command buffers, 將在單個渲染通道中使用 //[13] VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT command buffer 可以在已經掛起執行時重新提交。 beginInfo.flags = 0; // Optional //[13]僅與secondary command buffers 相關。 它指定從primarycommand buffers 繼承哪些狀態。 //用於輔助指令緩沖,可以用它來指定從調用它的主要指令緩沖繼承的狀態 beginInfo.pInheritanceInfo = nullptr; // Optional //指令緩沖對象記錄指令后,調用vkBeginCommandBuffer函數會重置指令緩沖對象 //開始指令緩沖的記錄操作 if (vkBeginCommandBuffer(commandBuffers[i], &beginInfo) != VK_SUCCESS) { throw std::runtime_error("failed to begin recording command buffer!"); } //[13]Starting a render pass//指定使用的渲染流程對象 VkRenderPassBeginInfo renderPassInfo = {}; //[13]the render pass itself and the attachments to bind renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; renderPassInfo.renderPass = renderPass; //指定使用的渲染流程對象 renderPassInfo.framebuffer = swapChainFramebuffers[i];//指定使用的幀緩沖對象 //[13]the size of the render area /**renderArea指定用於渲染的區域。位於這一區域外的像素數據會處於未定義狀態。通常,我們將這一區域設置為和我們使用的附着大小完全一樣. */ renderPassInfo.renderArea.offset = { 0, 0 }; //It should match the size of the attachments for best performance renderPassInfo.renderArea.extent = swapChainExtent; /** 所有可以記錄指令到指令緩沖的函數的函數名都帶有一個 vkCmd 前綴, 並且這些函數的返回值都是 void,也就是說在指令記錄操作完全結束前, 不用進行任何錯誤處理。 這類函數的第一個參數是用於記錄指令的指令緩沖對象。第二個參數 是使用的渲染流程的信息。最后一個參數是用來指定渲染流程如何提供繪 制指令的標記,它可以是下面這兩個值之一: VK_SUBPASS_CONTENTS_INLINE: 所有要執行的指令都在主要指令緩沖中,沒有輔助指令緩沖需要執行 VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS: 有來自輔助指令緩沖的指令需要執行。 */ VkClearValue clearColor = { 0.0f, 0.0f, 0.0f, 1.0f }; renderPassInfo.clearValueCount = 1; //指定標記后,使用的清除值 renderPassInfo.pClearValues = &clearColor; //[13]最后一個參數:how the drawing commands within the render pass will be provided //[13]VK_SUBPASS_CONTENTS_INLINE render pass commands 將嵌入 primary command buffer, 並且不會執行 secondary command buffers //[13]VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERSrender pass commands 將從 secondary command buffers 執行 //開始一個渲染流程 vkCmdBeginRenderPass(commandBuffers[i], &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); //凡是以vkCmd開頭的調用都是錄制(Record)命令,凡是以vkQueue開頭的都是提交(Submit)命令, //[13]Basic drawing commands //[13]第二個參數指定管線對象是圖形管線還是計算管線。 vkCmdBindPipeline(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); //[13]vertexCount 即使沒有頂點緩沖區,從技術上講,仍然有3個頂點要繪制 //[13]instanceCount 用於實例化渲染,如果不進行實例化使用1 //[13]firstVertex 頂點緩沖區的偏移量,定義gl_VertexIndex的最小值 //[13]firstInstance 用作實例渲染的偏移量,定義gl_InstanceIndex的最小值 /** 至此,我們已經提交了需要圖形管線執行的指令,以及片段着色器使用的附着 */ /** vkCmdDraw參數: 1.記錄有要執行的指令的指令緩沖對象 2. vertexCount: 盡管這里我們沒有使用頂點緩沖,但仍然需要指定三個頂點用於三角形的繪制。 3.instanceCount:用於實例渲染,為 1 時表示不進行實例渲染 4.firstVertex:用於定義着色器變量 gl_VertexIndex 的值 5.firstInstance:用於定義着色器變量 gl_InstanceIndex 的值 */ vkCmdDraw(commandBuffers[i], 3, 1, 0, 0);//開始調用指令進行三角形的繪制操作 //[13]The render pass can now be ended if (vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS) { //結束記錄指令到指令緩沖 throw std::runtime_error("failed to record command buffer!"); } } } //[13]--------------------------------------------------------------------------- //[14]--------------------------------------------------------------------------- void createSemaphores() { //創建每一幀需要的信號量對象 imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT); renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT); //[14] inFlightFences.resize(MAX_FRAMES_IN_FLIGHT); //[15] imagesInFlight.resize(swapChainImages.size(), VK_NULL_HANDLE); //[14] Vulkan API 會擴展 flags 和 pNext 參數。 VkSemaphoreCreateInfo semaphoreInfo = {}; semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; //[14] VkFenceCreateInfo fenceInfo = {}; fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; /** 默認情況下,柵欄 (fence) 對象在創建后是未發出信號的狀態。 這就意味着如果我們沒有在 vkWaitForFences 函數調用 之前發出柵欄 (fence) 信號,vkWaitForFences 函數調用將會一直處於等待狀態。 這里設置初始狀態為已發出信號 */ //[14]if we had rendered an initial frame that finished fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; //[14] for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {//創建信號量和VkFence 對象 if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS || vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS || //[14] vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) != VK_SUCCESS) { throw std::runtime_error("failed to create semaphores for a frame!"); } } } //[15]-----------------------異步執行的--從交換鏈獲取圖像------------------------------------------------- //[15]acquire an image from the swap chain /// <summary> /// * 1.從交換鏈獲取一張圖像 /// *2.對幀緩沖附着執行指令緩沖中的渲染指令 /// * 3.返回渲染后的圖像到交換鏈進行呈現操作 //上面這些操作每一個都是通過一個函數調用設置的, 但每個操作的實際執行卻是異步進行的。函數調用會在操作實際結束前返回,並且操作的實 //際執行順序也是不確定的。而我們需要操作的執行能按照一定的順序,所以就需要進行同步操作。 //有兩種用於同步交換鏈事件的方式:柵欄(fence) 和信號量(semaphore)。它們都可以完成同步操作。 //柵欄(fence) 和信號量(semaphore) 的不同之處是,我們可以通過調用 vkWaitForFences 函數查詢柵欄(fence) 的狀態,但不能查詢信號量 //(semaphore) 的狀態。 // 通常,我們使用柵欄(fence) 來對應用程序本身和渲染操作進行同步。 // 使用信號量(semaphore) 來對一個指令隊列內的操作或多個不同指令隊列的操作進行同步。 //這里,我們想要通過指令隊列中的繪制操作和呈現操作,顯然,使用信號量(semaphore) 更加合適。 /// </summary> void drawFrame() { //[15]wait for the frame to be finished //[15]vkWaitForFences函數接受一個fences數組,並等待其中任何一個或所有fences在返回之前發出信號。 //[15]這里傳遞的VK_TRUE表示要等待所有的fences,但是對於單個fences來說,這顯然無關緊要。 //[15]就像vkAcquireNextImageKHR一樣,這個函數也需要超時。與信號量不同,我們需要通過vkresetfines調用將fence重置為unsignaled狀態。 //vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); //or uint32_t imageIndex; //[15]第三個參數指定可獲得圖像的超時時間(以納秒為單位)。 //[15]接下來的兩個參數指定了同步對象,這些對象將在引擎使用完圖像時發出信號。 可以指定semaphore、fence或都指定 //[15]用於輸出可用swap chain中圖像的索引。 //vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex); //or //[15] //vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); //or //[16] /** vkAcquireNextImageKHR參數: 1.使用的邏輯設備對象 2.我們要獲取圖像的交換鏈, 3.圖像獲取的超時時間,我們可以通過使用無符號 64 位整型所能表示的 最大整數來禁用圖像獲取超時 4,5.指定圖像可用后通知的同步對象.可以指定一個信號量對象或柵欄對象, 或是同時指定信號量和柵欄對象進行同步操作。 在這里,我們指定了一個叫做 imageAvailableSemaphore 的信號量對象 6.用於輸出可用的交換鏈圖像的索引,我們使用這個索引來引用我們的 swapChainImages數組中的VkImage對象,並使用這一索引來提交對應的指令緩沖 */ //從交換鏈獲取一張圖像,交換鏈是一個擴展特性,所以與它相關的操作都會有 KHR 這一擴展后綴 VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); //從交換鏈獲取圖片 //[16]VK_ERROR_OUT_OF_DATE_KHR: Swap chain 與 surface 不兼容,無法再用於渲染。 通常發生在窗口調整大小之后 //[16]VK_SUBOPTIMAL_KHR Swap: chain仍可用於成功呈現到surface,但surface屬性不再完全匹配 if (result == VK_ERROR_OUT_OF_DATE_KHR) { //[18]如果SwapChain在嘗試獲取圖像時已過時,則無法再顯示給它。因此,我們應該立即重新創建SwapChain,並在下一個drawFrame調用中重試。 recreateSwapChain(); return; } //[16]如果SwapChain是次優的,可以調用recreateSwapChain,但還是選擇繼續,因為我們已經獲得了一個圖像。VK_SUCCESS和VK_SUBOPTIMAL_KHR都被認為是“成功” 。 else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { throw std::runtime_error("failed to acquire swap chain image!"); } /** vkWaitForFences 函數可以用來等待一組柵欄 (fence) 中的一個或全部柵欄 (fence) 發出信號。上面代碼中我們對它使用的 VK_TRUE 參數用來指定它等待所有在數組中指定的柵欄 (fence)。我們現在只有一個柵欄 (fence) 需要等待,所以不使用 VK_TRUE 也是可以的。和 vkAcquireNextImageKHR 函數一樣,vkWaitForFences 函數也有一個超時參數。和信號量不同,等待柵欄發出信號后,我們需要調用 vkResetFences 函數手動將柵欄 (fence) 重置為未發出信號的狀態。 */ //[15]Check if a previous frame is using this image (i.e. there is its fence to wait on) if (imagesInFlight[imageIndex] != VK_NULL_HANDLE) { vkWaitForFences(device, 1, &imagesInFlight[imageIndex], VK_TRUE, UINT64_MAX); //等待我們當前幀所使用的指令緩沖結束執行 } //[15] Mark the image as now being in use by this frame imagesInFlight[imageIndex] = inFlightFences[currentFrame]; //提交信息給指令隊列 VkSubmitInfo submitInfo = {}; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; //VkSemaphore waitSemaphores[] = { imageAvailableSemaphore }; //or //[15] VkSemaphore waitSemaphores[] = { imageAvailableSemaphores[currentFrame] }; //這里需要寫入顏色數據到圖像,所以我們指定等待圖像管線到達可以寫入顏色附着的管線階段 VkPipelineStageFlags waitStages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT }; //[15]前三個參數指定在執行開始之前等待哪些Semaphore,以及在管線的哪個階段等待。 /** waitSemaphoreCount、pWaitSemaphores 和pWaitDstStageMask 成員變量用於指定隊列開始執行前需要等待的信號量,以及需要等待的管線階段 */ submitInfo.waitSemaphoreCount = 1; submitInfo.pWaitSemaphores = waitSemaphores; submitInfo.pWaitDstStageMask = waitStages; //waitStages 數組中的條目和 pWaitSemaphores 中相同索引的信號量相對應。 //[15]指定實際提交執行的commandBuffer。 //指定實際被提交執行的指令緩沖對象 //我們應該提交和我們剛剛獲取的交換鏈圖像相對應的指令緩沖對象 submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &commandBuffers[imageIndex]; //[15]指定在commandBuffer完成執行后發送信號的Semaphore。 //VkSemaphore signalSemaphores[] = { renderFinishedSemaphore}; //or //[15] VkSemaphore signalSemaphores[] = { renderFinishedSemaphores[currentFrame] }; //指定實際被提交執行的指令緩沖對象 //我們應該提交和我們剛剛獲取的交換鏈圖像相對應的指令緩沖對象 submitInfo.signalSemaphoreCount = 1; submitInfo.pSignalSemaphores = signalSemaphores; //[15] vkResetFences(device, 1, &inFlightFences[currentFrame]); //[15]最后一個參數引用了可選的 fenc, 當 command buffer 執行完成時,它將發出信號。 //[15]我們使用信號量進行同步,所以我們傳遞VK_NULL_HANDLE。 //if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) { // throw std::runtime_error("failed to submit draw command buffer!"); //} //or //凡是以vkCmd開頭的調用都是錄制(Record)命令,凡是以vkQueue開頭的都是提交(Submit)命令, /** vkQueueSubmit 函數使用vkQueueSubmit結構體數組作為參數,可以同時大批量提交數.。 vkQueueSubmit 函數的最后一個參數是一個可選的柵欄對象, 可以用它同步提交的指令緩沖執行結束后要進行的操作。 在這里,我們使用信號量進行同步,沒有使用它,將其設置為VK_NULL_HANDLE */ if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {//選擇適當的Command buffers提交給Queue,提交指令緩沖給圖形指令隊列 throw std::runtime_error("failed to submit draw command buffer!"); } //將渲染的圖像返回給交換鏈進行呈現操作 VkPresentInfoKHR presentInfo = {}; //配置呈現信息 presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; //[15]在呈現(Present)之前等待哪些信號量 presentInfo.waitSemaphoreCount = 1; //指定開始呈現操作需要等待的信號量 presentInfo.pWaitSemaphores = signalSemaphores; //指定了用於呈現圖像的交換鏈,以及需要呈現的圖像在交換鏈中的索引 VkSwapchainKHR swapChains[] = { swapChain }; presentInfo.swapchainCount = 1; //[15]為每個swapChain指定呈現的圖像和圖像索引 presentInfo.pSwapchains = swapChains; presentInfo.pImageIndices = &imageIndex; //[15]check for every individual swap chain if presentation was successful. (array of VkResult) /** 我們可以通過 pResults 成員變量獲取每個交換鏈的呈現操作是否成功的信息。在這里,由於我們只使用了一個交換鏈,可以直接使用呈現函數 的返回值來判斷呈現操作是否成功 */ presentInfo.pResults = nullptr; // Optional //[15]submits the request to present an image to the swap chain. //vkQueuePresentKHR(presentQueue, &presentInfo); //or //[16]vkQueuePresentKHR 函數返回具有相同vkAcquireNextImageKHR返回值含義相同的值。 //[16]在這種情況下,如果SwapChain不是最優的,我們也會重新創建它,因為我們想要最好的結果。 result = vkQueuePresentKHR(presentQueue, &presentInfo); //請求交換鏈進行圖像呈現操作 //添加 framebufferResized 確保semaphores處於一致狀態,否則可能永遠不會正確等待發出信號的semaphores。 if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) { framebufferResized = false; recreateSwapChain(); } else if (result != VK_SUCCESS) { throw std::runtime_error("failed to present swap chain image!"); } /** 如果開啟校驗層后運行程序,觀察應用程序的內存使用情況, 可以發現我們的應用程序的內存使用量一直在慢慢增加。這是由於我 們的 drawFrame 函數以很快地速度提交指令,但卻沒有在下一次指令提交時檢查上一次提交的指令是否已經執行結束。也就是說 CPU 提交 指令快過 GPU 對指令的處理速度,造成 GPU 需要處理的指令大量堆積。更糟糕的是這種情況下,我們實際上對多個幀同時使用了相同的 imageAvailableSemaphore 和 renderFinishedSemaphore 信號量。 最簡單的解決上面這一問題的方法是使用 vkQueueWaitIdle 函數來等待上一次提交的指令結束執行,再提交下一幀的指令: 但這樣做,是對 GPU 計算資源的大大浪費。圖形管線可能大部分時間都處於空閑狀態. */ //[15] //更新currentFrame currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; //[15] vkQueueWaitIdle(presentQueue); //等待一個特定指令隊列結束執行 } //[16]recreate 之前先 cleanup void cleanupSwapChain() { for (size_t i = 0; i < swapChainFramebuffers.size(); i++) {//銷毀幀緩沖 vkDestroyFramebuffer(device, swapChainFramebuffers[i], nullptr); } //[16]可以重新創建Command池,但相當浪費的。相反,使用vkfreecandbuffers函數清理現有CommandBuffer。就可以重用現有的池來分配新的CommandBuffer。 //清除分配的指令緩沖對象 vkFreeCommandBuffers(device, commandPool, static_cast<uint32_t>(commandBuffers.size()), commandBuffers.data()); //釋放資源 vkDestroyPipeline(device, graphicsPipeline, nullptr); //銷毀管線對象 vkDestroyPipelineLayout(device, pipelineLayout, nullptr); //銷毀管線布局對象 vkDestroyRenderPass(device, renderPass, nullptr); //銷毀渲染流程對象 for (size_t i = 0; i < swapChainImageViews.size(); i++) {//銷毀圖像視圖 vkDestroyImageView(device, swapChainImageViews[i], nullptr); } vkDestroySwapchainKHR(device, swapChain, nullptr); //銷毀交換鏈對象,在邏輯設備被清除前調用 } //[16] recreate SwapChain, pipeline must rebuilt void recreateSwapChain() { //[16]處理最小化 int width = 0, height = 0; glfwGetFramebufferSize(window, &width, &height); while (width == 0 || height == 0) {//設置應用程序在窗口最小化后停止渲染,直到窗口重新可見時重建交換鏈 glfwGetFramebufferSize(window, &width, &height); glfwWaitEvents(); } //[16]等待資源完成使用。 vkDeviceWaitIdle(device); //等待設備處於空閑狀態,避免在對象的使用過程中將其清除重建 cleanupSwapChain(); //清除交換鏈相關 createSwapChain(); //重新創建交換鏈 createImageViews(); //圖形視圖是直接依賴於交換鏈圖像的,所以也需要被重建 createRenderPass(); //渲染流程依賴於交換鏈圖像的格式,窗口大小改變不會引起使用的交換鏈圖像格式改變 //視口和裁剪矩形在管線創建時被指定,窗口大小改變,這些設置也需要修改 //我們可以通過使用動態狀態來設置視口和裁剪矩形來避免重建管線 createGraphicsPipeline(); //幀緩沖和指令緩沖直接依賴於交換鏈圖像 createFramebuffers(); createCommandBuffers(); } //[16] 為靜態函數才能將其用作回調函數 static void framebufferResizeCallback(GLFWwindow* window, int width, int height) { //[16-2]當前對象指針賦值 auto app = reinterpret_cast<HelloTriangle*>(glfwGetWindowUserPointer(window)); app->framebufferResized = true; } }; int main() { HelloTriangle app; try { app.run(); } catch (const std::exception& e) { std::cerr << e.what() << std::endl; return EXIT_FAILURE; } return EXIT_FAILURE; } //處理所有的輸入,查詢glfw看是否有相關的鍵被按下或松開,並作出相應的響應處理 void processInput(GLFWwindow* window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS || glfwGetKey(window, GLFW_KEY_SPACE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); }
3、在項目中再建兩個着色器文件,分別為:頂點着色器的shader.vert和片元着色器的shader.frag
shader.vert

#version 450 layout(location = 0) out vec3 fragColor; vec3 positions[3] = vec3[]( //vec3(0.0, -0.5,0.0), //三角形上面的頂點 vec3(0.5, 0.5,0.0), //右邊的點 vec3(-0.5, 0.5,0.0) //左邊的點 ); vec3 colors[3] = vec3[]( vec3(1.0, 0.0, 0.0), //紅色 vec3(0.0, 1.0, 0.0), //綠色 vec3(0.0, 0.0, 1.0) //藍色 ); void main() { gl_Position = vec4(positions[gl_VertexIndex], 1.0); fragColor = colors[gl_VertexIndex]; }
這里要注意,Vulkan的標准化設備坐標的NDC(Normalized Device Coordinates)采用的是右手坐標系,X軸坐標是左右方向的,同OpenGL相同;
Z 軸坐標的范圍和 Direct3D 相同,為 0 到 1,和OpenGL的XYZ三個方向都是-1到1的區間也完全不同;
而且Vulkan的Y軸是向下的,和OpenGL的Y軸向上完全相反的方向。
shader.frag

#version 450 #extension GL_ARB_separate_shader_objects : enable layout(location = 0) in vec3 fragColor; layout(location = 0) out vec4 outColor; void main() { outColor = vec4(fragColor, 1.0); }
4、將shader.vert和shader.frag分別編譯成vert.spv和frag.spv,筆者自己的環境是在F盤,如下:

F:\Study\Vulkan\HelloTriangle1>glslc.exe shader.vert -o vert.spv F:\Study\Vulkan\HelloTriangle1>glslc.exe shader.frag -o frag.spv F:\Study\Vulkan\HelloTriangle1>dir 驅動器 F 中的卷是 新加卷 卷的序列號是 86A1-6462 F:\Study\Vulkan\HelloTriangle1 的目錄 2022/03/19 17:13 <DIR> . 2022/03/19 17:13 <DIR> .. 2022/03/19 17:13 608 frag.spv 2022/03/19 17:13 61,696 HelloTriangle.cpp 2022/03/19 17:08 30,563 HelloTriangle1.vcxproj 2022/03/19 17:08 1,204 HelloTriangle1.vcxproj.filters 2022/03/15 18:08 168 HelloTriangle1.vcxproj.user 2022/03/19 17:08 218 shader.frag 2022/03/19 17:07 391 shader.vert 2022/03/19 17:13 1,504 vert.spv 2022/03/15 18:12 <DIR> x64 9 個文件 96,352 字節 3 個目錄 66,113,679,360 可用字節 F:\Study\Vulkan\HelloTriangle1>
這里對spriv進行簡單描述,它是Standard, Portable Intermediate Representation - V (SPIR-V)的縮寫,是一種用於GPU通用計算和圖形學的中間語言,在OpenGL4.6版本后才支持SPIR-V,由Khronos開發設計,最初是為OpenCL規范准備的,和Vulkan的標准差不多同時提出,也還在不斷發展完善中,詳細介紹請移步:https://blog.csdn.net/aoxuestudy/article/details/112350292
5、按VS上的F5執行,結果如下: