1. 簡介
為什麼要在遊戲中使用 Vulkan?
Vulkan 是 Android 上的主要低階圖形 API,可協助實作專屬遊戲引擎和算繪器的遊戲提升效能。
Vulkan 適用於 Android 7.0 (API 級別 24) 以上版本。自 Android 10.0 以上版本開始,新款 64 位元 Android 裝置都必須要能支援 Vulkan 1.1 版。此外,2022 年版 Android 基準設定檔也將 Vulkan API 1.1 設為最低版本需求。
由於在 OpenGL ES 內發出繪製呼叫的成本較高,如果遊戲含有大量繪製呼叫,或是採用 OpenGL ES,就可能出現明顯的驅動程式負載。這類遊戲可能會在圖形驅動程式中耗費大量影格時間,因而受到 CPU 限制。不過,只要從 OpenGL ES 改成使用 Vulkan,遊戲就可以大幅降低 CPU 利用率和耗電量。如果遊戲中的複雜場景無法有效利用例項來減少繪製呼叫,就特別適合採取這種做法。
建構項目
在本程式碼研究室中,我們會提供一個基本的 C++ Android 應用程式,讓您新增程式碼來設定 Vulkan 算繪管道。然後您就可以實作採用 Vulkan 的程式碼,在畫面上算繪帶有紋理的旋轉三角形。
軟硬體需求
- Android Studio Iguana 以上版本。
- 搭載 Android 10.0 以上版本的 Android 裝置 (需連接啟用「開發人員選項」和「USB 偵錯選項」的電腦)。
2. 開始設定
設定開發環境
如果您之前未在 Android Studio 中使用原生專案,可能需要安裝 Android NDK 和 CMake。如已安裝這些項目,請跳至「設定專案」一節。
檢查是否已安裝 SDK、NDK 和 CMake
請啟動 Android Studio。在畫面顯示「Welcome to Android Studio」視窗時,開啟「Configure」下拉式選單,然後選取「SDK Manager」選項。
如果已開啟現有專案,可以改從「Tools」選單開啟 SDK Manager。請按一下「Tools」選單,然後選取「SDK Manager」,SDK Manager 視窗會隨即開啟。
在側欄中,依序選取「Appearance & Behavior」>「System Settings」>「Android SDK」。在 Android SDK 窗格中選取「SDK Platforms」分頁標籤,畫面就會列出已安裝的工具選項。請確認已安裝 Android SDK 12.0 以上版本。
接著選取「SDK Tools」分頁標籤,確認已安裝 NDK 和 CMake。
注意:只要版本不舊皆可,不一定要使用特定版本。我們目前使用的是 NDK 26.1.10909125 和 CMake 3.22.1。隨著較新的 NDK 版本持續推出,預設安裝的 NDK 版本也會變更。如需安裝特定版本的 NDK,請參閱關於安裝 NDK 的 Android Studio 參考資料,按照「安裝特定版本的 NDK」一節指示操作。
勾選所有必要工具後,點選視窗底部的「Apply」按鈕即可安裝。接著您可以點選「OK」按鈕,關閉 Android SDK 視窗。
設定專案
我們已根據 C++ 範本設定了一個起始專案,放置在 Git 存放區供您參考。這項專案會實作應用程式的初始化和事件處理作業,但目前不會設定或算繪任何圖像。
複製存放區
請在指令列中,變更為要納入根專案目錄的目錄,並從 GitHub 複製該目錄:
git clone -b codelab/start https://github.com/android/getting-started-with-vulkan-on-android-codelab.git --recurse-submodules
開始操作時,請確認您是使用 [codelab] start: empty app
存放區的初始修訂版本。
使用 Android Studio 開啟專案、建構專案,然後在連接的裝置上執行。專案會在空白的黑畫面中啟動,讓您加入以下各節中算繪的圖像。
3. 建立 Vulkan 例項和裝置
如要初始化 Vulkan API 待用,第一步是建立 Vulkan 例項物件 (VkInstance)。
VkInstance 物件代表應用程式在 Vulkan 執行階段的例項,也是 Vulkan API 的根物件,可用於將 Vulkan 裝置物件和任何要啟動的層例項化,並擷取關於這些物件/層的資訊。
應用程式建立 VkInstance 時,一定要提供自身相關資訊,例如名稱、版本,以及所需的 Vulkan 例項擴充功能。
設計上,Vulkan API 涵蓋可提供特定機制的層系統,用於攔截及處理 API 呼叫,之後這些呼叫才會傳達到 GPU 驅動程式。應用程式可以在建立 VkInstance 時指定要啟動的層。最常用的層是 Vulkan 驗證層,這個驗證層提供 API 使用情形的執行階段分析,可說明是否發生錯誤,或有哪些做法導致效能降低。
VkInstance 建立完成後,就可以供應用程式用於查詢系統上可用的實體裝置、建立邏輯裝置,並構建算繪目標介面。
VkInstance 通常會在應用程式啟動時建立一次,而在應用程式終止時刪除。不過,也可能有在同一應用程式中建立多個 VkInstance 的情況,需要使用多個 GPU 或建立多個視窗的應用程式就是一例。
// CODELAB: hellovk.h
void HelloVK::createInstance() {
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Hello Triangle";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
createInfo.enabledExtensionCount = (uint32_t)requiredExtensions.size();
createInfo.ppEnabledExtensionNames = requiredExtensions.data();
createInfo.pApplicationInfo = &appInfo;
createInfo.enabledLayerCount = 0;
createInfo.pNext = nullptr;
VK_CHECK(vkCreateInstance(&createInfo, nullptr, &instance));
}
}
VkPhysicalDevice 是 Vulkan 物件,代表系統上的實體 Vulkan 裝置。Android 裝置多半都只會傳回一個代表 GPU 的 VkPhysicalDevice。不過,電腦或 Android 裝置卻可列舉多部實體裝置,例如電腦就同時包含獨立和整合的 GPU。
您可以查詢 VkPhysicalDevice 的屬性,例如名稱、廠商、驅動程式版本和支援的功能。如要為特定應用程式選擇最適合的實體裝置,這項資訊就能派上用場。
選好 VkPhysicalDevice 後,應用程式就可以從中建立邏輯裝置。邏輯裝置是應用程式專屬的實體裝置表示,有自己的狀態和資源,與可能從同一實體裝置建立的其他邏輯裝置互不隸屬。
不同的佇列系列會產生不同類型的佇列,每個佇列系列都只允許一部分指令。舉例來說,有的佇列系列可能僅允許處理運算指令,有的則僅允許與記憶體轉移相關的指令。
VkPhysicalDevice 可以列舉所有可用的佇列系列類型。這裡我們重點只放在圖像佇列,但可能也是有其他僅支援 COMPUTE
或 TRANSFER
的佇列。佇列系列沒有自身專屬的類型,而是以其父項物件 (VkPhysicalDevice) 內的數字索引類型 uint32_t 表示。
VkPhysicalDevice 本身可以建立多個邏輯裝置。如果應用程式需要使用多個 GPU 或建立多個視窗,這就非常有用。
VkDevice 是代表 Vulkan 邏輯裝置的 Vulkan 物件。這是對實體裝置的精簡抽象程序,提供了建立及管理 Vulkan 資源的所有必要功能,例如緩衝區、圖像和著色器。
應用程式會從 VkPhysicalDevice 中建立專屬的 VkDevice,此物件有自己的狀態和資源,與可能從同一實體裝置建立的其他邏輯裝置互不隸屬。
VkSurfaceKHR 物件代表可做為算繪作業目標位置的介面。如要在裝置螢幕上顯示圖像,您需採用對應用程式視窗物件的參照來建立介面。VkSurfaceKHR 物件建立 完成後,就可以供應用程式用於建立 VkSwapchainKHR 物件。
VkSwapchainKHR 物件代表一個基礎架構,我們會先算繪到其中的緩衝區,然後才在畫面上以影像呈現緩衝區。此物件基本上是等候在畫面上顯示的圖像佇列。我們會取得這類圖像、繪製到該物件中,然後將其傳回到佇列。佇列確切的運作方式,以及從佇列中呈現圖像的條件,都取決於交換鏈的設定方法。不過一般來說,交換鏈的用途是根據畫面刷新率同步呈現圖像。
// CODELAB: hellovk.h - Data Types
struct QueueFamilyIndices {
std::optional<uint32_t> graphicsFamily;
std::optional<uint32_t> presentFamily;
bool isComplete() {
return graphicsFamily.has_value() && presentFamily.has_value();
}
};
struct SwapChainSupportDetails {
VkSurfaceCapabilitiesKHR capabilities;
std::vector<VkSurfaceFormatKHR> formats;
std::vector<VkPresentModeKHR> presentModes;
};
struct ANativeWindowDeleter {
void operator()(ANativeWindow *window) { ANativeWindow_release(window); }
};
如果您需要對應用程式偵錯,可以設定驗證層支援。您也可以檢查遊戲可能需要哪些特定的擴充功能。
// CODELAB: hellovk.h
bool HelloVK::checkValidationLayerSupport() {
uint32_t layerCount;
vkEnumerateInstanceLayerProperties(&layerCount, nullptr);
std::vector<VkLayerProperties> availableLayers(layerCount);
vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data());
for (const char *layerName : validationLayers) {
bool layerFound = false;
for (const auto &layerProperties : availableLayers) {
if (strcmp(layerName, layerProperties.layerName) == 0) {
layerFound = true;
break;
}
}
if (!layerFound) {
return false;
}
}
return true;
}
std::vector<const char *> HelloVK::getRequiredExtensions(
bool enableValidationLayers) {
std::vector<const char *> extensions;
extensions.push_back("VK_KHR_surface");
extensions.push_back("VK_KHR_android_surface");
if (enableValidationLayers) {
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
}
return extensions;
}
找到適當的設定並建立 VkInstance 後,請建立代表算繪目標視窗的 VkSurface。
// CODELAB: hellovk.h
void HelloVK::createSurface() {
assert(window != nullptr); // window not initialized
const VkAndroidSurfaceCreateInfoKHR create_info{
.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR,
.pNext = nullptr,
.flags = 0,
.window = window.get()};
VK_CHECK(vkCreateAndroidSurfaceKHR(instance, &create_info,
nullptr /* pAllocator */, &surface));
}
列舉可用的實體裝置 (GPU) 並選取第一個合適的可用裝置。
// CODELAB: hellovk.h
void HelloVK::pickPhysicalDevice() {
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
assert(deviceCount > 0); // failed to find GPUs with Vulkan support!
std::vector<VkPhysicalDevice> devices(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());
for (const auto &device : devices) {
if (isDeviceSuitable(device)) {
physicalDevice = device;
break;
}
}
assert(physicalDevice != VK_NULL_HANDLE); // failed to find a suitable GPU!
}
如要確認裝置是否合適,我們需找出可支援 GRAPHICS
佇列的裝置。
// CODELAB: hellovk.h
bool HelloVK::isDeviceSuitable(VkPhysicalDevice device) {
QueueFamilyIndices indices = findQueueFamilies(device);
bool extensionsSupported = checkDeviceExtensionSupport(device);
bool swapChainAdequate = false;
if (extensionsSupported) {
SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
swapChainAdequate = !swapChainSupport.formats.empty() &&
!swapChainSupport.presentModes.empty();
}
return indices.isComplete() && extensionsSupported && swapChainAdequate;
}
// CODELAB: hellovk.h
bool HelloVK::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();
}
// CODELAB: hellovk.h
QueueFamilyIndices HelloVK::findQueueFamilies(VkPhysicalDevice device) {
QueueFamilyIndices indices;
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);
std::vector<VkQueueFamilyProperties> queueFamilies(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount,
queueFamilies.data());
int i = 0;
for (const auto &queueFamily : queueFamilies) {
if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) {
indices.graphicsFamily = i;
}
VkBool32 presentSupport = false;
vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport);
if (presentSupport) {
indices.presentFamily = i;
}
if (indices.isComplete()) {
break;
}
i++;
}
return indices;
}
找出要使用的 PhysicalDevice 後,請建立邏輯裝置 (亦即 VkDevice)。這代表初始化的 Vulkan 裝置,已準備好要建立用於應用程式的所有其他物件。
// CODELAB: hellovk.h
void HelloVK::createLogicalDeviceAndQueue() {
QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<uint32_t> uniqueQueueFamilies = {indices.graphicsFamily.value(),
indices.presentFamily.value()};
float queuePriority = 1.0f;
for (uint32_t queueFamily : uniqueQueueFamilies) {
VkDeviceQueueCreateInfo queueCreateInfo{};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = queueFamily;
queueCreateInfo.queueCount = 1;
queueCreateInfo.pQueuePriorities = &queuePriority;
queueCreateInfos.push_back(queueCreateInfo);
}
VkPhysicalDeviceFeatures deviceFeatures{};
VkDeviceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
createInfo.queueCreateInfoCount =
static_cast<uint32_t>(queueCreateInfos.size());
createInfo.pQueueCreateInfos = queueCreateInfos.data();
createInfo.pEnabledFeatures = &deviceFeatures;
createInfo.enabledExtensionCount =
static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
if (enableValidationLayers) {
createInfo.enabledLayerCount =
static_cast<uint32_t>(validationLayers.size());
createInfo.ppEnabledLayerNames = validationLayers.data();
} else {
createInfo.enabledLayerCount = 0;
}
VK_CHECK(vkCreateDevice(physicalDevice, &createInfo, nullptr, &device));
vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue);
vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue);
}
在這個步驟最後,您只能看到未算繪任何內容的全黑視窗,這是因為我們的設定程序尚未完成。如果發生錯誤,您可以比較您的成果與 [codelab] step: create instance and device
存放區中的修訂版本。
4. 建立交換鏈和同步物件
VkSwapchain 這個 Vulkan 物件代表可在螢幕上呈現的圖像佇列。此物件可用於實作雙/三緩衝區,進而減少撕裂情形並提升效能。
如要建立 VkSwapchain,應用程式必須先建立 VkSurfaceKHR 物件。建立例項期間,我們已在設定視窗時建立 VkSurfaceKHR 物件。
VkSwapchainKHR 物件有很多關聯的圖像。這些圖像的用途是儲存算繪的場景。應用程式可以從 VkSwapchainKHR 物件取得圖像、算繪到該圖像,然後呈現在畫面上。
圖像呈現在畫面上後,就不再供應用程式使用。應用程式必須從 VkSwapchainKHR 物件取得另一個圖像,才能再次算繪。
VkSwapchain 通常會在應用程式啟動時建立一次,而在應用程式終止時刪除。不過,也可能有在同一應用程式中建立與刪除多個 VkSwapchain 的情況,需要使用多個 GPU 或建立多個視窗的應用程式就是一例。
同步物件是用於同步作業的物件。Vulkan 有 VkFence, VkSemaphore 和 VkEvent,用於控管多個佇列間的資源存取權。如果您使用多個佇列和算繪通道,就需要這些物件,但我們的簡單範例不需用到。
// CODELAB: hellovk.h
void HelloVK::createSyncObjects() {
imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);
VkSemaphoreCreateInfo semaphoreInfo{};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
VK_CHECK(vkCreateSemaphore(device, &semaphoreInfo, nullptr,
&imageAvailableSemaphores[i]));
VK_CHECK(vkCreateSemaphore(device, &semaphoreInfo, nullptr,
&renderFinishedSemaphores[i]));
VK_CHECK(vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]));
}
}
// CODELAB: hellovk.h
void HelloVK::createSwapChain() {
SwapChainSupportDetails swapChainSupport =
querySwapChainSupport(physicalDevice);
auto 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;
}
}
return availableFormats[0];
};
VkSurfaceFormatKHR surfaceFormat =
chooseSwapSurfaceFormat(swapChainSupport.formats);
// Please check
// https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VkPresentModeKHR.html
// for a discourse on different present modes.
//
// VK_PRESENT_MODE_FIFO_KHR = Hard Vsync
// This is always supported on Android phones
VkPresentModeKHR presentMode = VK_PRESENT_MODE_FIFO_KHR;
uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;
if (swapChainSupport.capabilities.maxImageCount > 0 &&
imageCount > swapChainSupport.capabilities.maxImageCount) {
imageCount = swapChainSupport.capabilities.maxImageCount;
}
pretransformFlag = swapChainSupport.capabilities.currentTransform;
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 = displaySizeIdentity;
createInfo.imageArrayLayers = 1;
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
createInfo.preTransform = pretransformFlag;
QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(),
indices.presentFamily.value()};
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;
createInfo.pQueueFamilyIndices = nullptr;
}
createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR;
createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
createInfo.oldSwapchain = VK_NULL_HANDLE;
VK_CHECK(vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain));
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount,
swapChainImages.data());
swapChainImageFormat = surfaceFormat.format;
swapChainExtent = displaySizeIdentity;
}
// CODELAB: hellovk.h
SwapChainSupportDetails HelloVK::querySwapChainSupport(
VkPhysicalDevice device) {
SwapChainSupportDetails details;
vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface,
&details.capabilities);
uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);
if (formatCount != 0) {
details.formats.resize(formatCount);
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount,
details.formats.data());
}
uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount,
nullptr);
if (presentModeCount != 0) {
details.presentModes.resize(presentModeCount);
vkGetPhysicalDeviceSurfacePresentModesKHR(
device, surface, &presentModeCount, details.presentModes.data());
}
return details;
}
裝置的使用情境改變後,您也需準備重新建立交換鏈。舉例來說,使用者改用其他應用程式時就是這樣。
// CODELAB: hellovk.h
void HelloVK::reset(ANativeWindow *newWindow, AAssetManager *newManager) {
window.reset(newWindow);
assetManager = newManager;
if (initialized) {
createSurface();
recreateSwapChain();
}
}
void HelloVK::recreateSwapChain() {
vkDeviceWaitIdle(device);
cleanupSwapChain();
createSwapChain();
}
在這個步驟最後,您只能看到未算繪任何內容的全黑視窗,這是因為我們的設定程序尚未完成。如果發生錯誤,您可以比較您的成果與 [codelab] step: create swapchain and sync objects
存放區中的修訂版本。
5. 建立 Renderpass 和 Framebuffer
VkImageView 這個 Vulkan 物件可用於說明存取 VkImage 的方式,指出要存取的圖像中的子資源範圍、要使用的像素格式,以及要套用到管道的 swizzle。
VkRenderPass 這個 Vulkan 物件則說明 GPU 應如何算繪場景,指出要使用的附件,以及附件的算繪順序和在算繪管道各階段中的使用方式。
VkFramebuffer 也是 Vulkan 物件,代表一系列圖像檢視畫面,在執行算繪通道期間用做附件。換句話說,此物件可將實際的圖像附件繫結到算繪通道中。
// CODELAB: hellovk.h
void HelloVK::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];
createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
createInfo.format = swapChainImageFormat;
createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
createInfo.subresourceRange.baseMipLevel = 0;
createInfo.subresourceRange.levelCount = 1;
createInfo.subresourceRange.baseArrayLayer = 0;
createInfo.subresourceRange.layerCount = 1;
VK_CHECK(vkCreateImageView(device, &createInfo, nullptr,
&swapChainImageViews[i]));
}
}
Vulkan 中的附件就是一般所謂的算繪目標,通常是用做算繪輸出內容的圖像。這裡您只需要說明格式,舉例來說,算繪通道可能輸出特定顏色格式,或深度模板格式。您也需要指明附件是否應在通道起點,保留、捨棄或清除自身內容。
// CODELAB: hellovk.h
void HelloVK::createRenderPass() {
VkAttachmentDescription colorAttachment{};
colorAttachment.format = swapChainImageFormat;
colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
VkAttachmentReference colorAttachmentRef{};
colorAttachmentRef.attachment = 0;
colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;
VkSubpassDependency dependency{};
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.srcAccessMask = 0;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
VkRenderPassCreateInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = 1;
renderPassInfo.pAttachments = &colorAttachment;
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
renderPassInfo.dependencyCount = 1;
renderPassInfo.pDependencies = &dependency;
VK_CHECK(vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass));
}
Framebuffer 代表可用於附件 (算繪目標) 的實際圖像連結。請指定算繪通道和一系列圖像檢視畫面,藉此建立 Framebuffer 物件。
// CODELAB: hellovk.h
void HelloVK::createFramebuffers() {
swapChainFramebuffers.resize(swapChainImageViews.size());
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;
framebufferInfo.width = swapChainExtent.width;
framebufferInfo.height = swapChainExtent.height;
framebufferInfo.layers = 1;
VK_CHECK(vkCreateFramebuffer(device, &framebufferInfo, nullptr,
&swapChainFramebuffers[i]));
}
}
在這個步驟最後,您只能看到未算繪任何內容的全黑視窗,這是因為我們的設定程序尚未完成。如果發生錯誤,您可以比較您的成果與 [codelab] step: create renderpass and framebuffer
存放區中的修訂版本。
6. 建立著色器和管道
VkShaderModule 這個 Vulkan 物件代表可程式化的著色器。著色器的用途是對圖像資料執行多種操作,例如轉換頂點、替像素著色,以及運算全域效果。
VkPipeline 這個 Vulkan 物件則代表可程式化的圖像管道。VkPipeline 是一系列狀態物件,用於說明 GPU 應如何算繪場景。
VkDescriptorSetLayout 是 VkDescriptorSet 的範本,而後者是一系列描述元。描述元這類控點可啟用著色器來存取資源,例如緩衝區、圖像或取樣器。
// CODELAB: hellovk.h
void HelloVK::createDescriptorSetLayout() {
VkDescriptorSetLayoutBinding uboLayoutBinding{};
uboLayoutBinding.binding = 0;
uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
uboLayoutBinding.descriptorCount = 1;
uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
uboLayoutBinding.pImmutableSamplers = nullptr;
VkDescriptorSetLayoutCreateInfo layoutInfo{};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 1;
layoutInfo.pBindings = &uboLayoutBinding;
VK_CHECK(vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr,
&descriptorSetLayout));
}
定義 createShaderModule
函式,將著色器載入 VkShaderModule 物件。
// CODELAB: hellovk.h
VkShaderModule HelloVK::createShaderModule(const std::vector<uint8_t> &code) {
VkShaderModuleCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
// Satisfies alignment requirements since the allocator
// in vector ensures worst case requirements
createInfo.pCode = reinterpret_cast<const uint32_t *>(code.data());
VkShaderModule shaderModule;
VK_CHECK(vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule));
return shaderModule;
}
建立載入簡易頂點和片段著色器的圖像管道。
// CODELAB: hellovk.h
void HelloVK::createGraphicsPipeline() {
auto vertShaderCode =
LoadBinaryFileToVector("shaders/shader.vert.spv", assetManager);
auto fragShaderCode =
LoadBinaryFileToVector("shaders/shader.frag.spv", assetManager);
VkShaderModule vertShaderModule = createShaderModule(vertShaderCode);
VkShaderModule fragShaderModule = createShaderModule(fragShaderCode);
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";
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";
VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo,
fragShaderStageInfo};
VkPipelineVertexInputStateCreateInfo vertexInputInfo{};
vertexInputInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO;
vertexInputInfo.vertexBindingDescriptionCount = 0;
vertexInputInfo.pVertexBindingDescriptions = nullptr;
vertexInputInfo.vertexAttributeDescriptionCount = 0;
vertexInputInfo.pVertexAttributeDescriptions = nullptr;
VkPipelineInputAssemblyStateCreateInfo inputAssembly{};
inputAssembly.sType =
VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO;
inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
inputAssembly.primitiveRestartEnable = VK_FALSE;
VkPipelineViewportStateCreateInfo viewportState{};
viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO;
viewportState.viewportCount = 1;
viewportState.scissorCount = 1;
VkPipelineRasterizationStateCreateInfo rasterizer{};
rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO;
rasterizer.depthClampEnable = VK_FALSE;
rasterizer.rasterizerDiscardEnable = VK_FALSE;
rasterizer.polygonMode = VK_POLYGON_MODE_FILL;
rasterizer.lineWidth = 1.0f;
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE;
rasterizer.depthBiasEnable = VK_FALSE;
rasterizer.depthBiasConstantFactor = 0.0f;
rasterizer.depthBiasClamp = 0.0f;
rasterizer.depthBiasSlopeFactor = 0.0f;
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;
multisampling.pSampleMask = nullptr;
multisampling.alphaToCoverageEnable = VK_FALSE;
multisampling.alphaToOneEnable = VK_FALSE;
VkPipelineColorBlendAttachmentState colorBlendAttachment{};
colorBlendAttachment.colorWriteMask =
VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
colorBlendAttachment.blendEnable = VK_FALSE;
VkPipelineColorBlendStateCreateInfo colorBlending{};
colorBlending.sType =
VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO;
colorBlending.logicOpEnable = VK_FALSE;
colorBlending.logicOp = VK_LOGIC_OP_COPY;
colorBlending.attachmentCount = 1;
colorBlending.pAttachments = &colorBlendAttachment;
colorBlending.blendConstants[0] = 0.0f;
colorBlending.blendConstants[1] = 0.0f;
colorBlending.blendConstants[2] = 0.0f;
colorBlending.blendConstants[3] = 0.0f;
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
pipelineLayoutInfo.pushConstantRangeCount = 0;
pipelineLayoutInfo.pPushConstantRanges = nullptr;
VK_CHECK(vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr,
&pipelineLayout));
std::vector<VkDynamicState> dynamicStateEnables = {VK_DYNAMIC_STATE_VIEWPORT,
VK_DYNAMIC_STATE_SCISSOR};
VkPipelineDynamicStateCreateInfo dynamicStateCI{};
dynamicStateCI.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamicStateCI.pDynamicStates = dynamicStateEnables.data();
dynamicStateCI.dynamicStateCount =
static_cast<uint32_t>(dynamicStateEnables.size());
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;
pipelineInfo.pColorBlendState = &colorBlending;
pipelineInfo.pDynamicState = &dynamicStateCI;
pipelineInfo.layout = pipelineLayout;
pipelineInfo.renderPass = renderPass;
pipelineInfo.subpass = 0;
pipelineInfo.basePipelineHandle = VK_NULL_HANDLE;
pipelineInfo.basePipelineIndex = -1;
VK_CHECK(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo,
nullptr, &graphicsPipeline));
vkDestroyShaderModule(device, fragShaderModule, nullptr);
vkDestroyShaderModule(device, vertShaderModule, nullptr);
}
在這個步驟最後,您只能看到未算繪任何內容的全黑視窗,這是因為我們的設定程序尚未完成。如果發生錯誤,您可以比較您的成果與 [codelab] step: create shader and pipeline
存放區中的修訂版本。
7. DescriptorSet 和統一緩衝區
VkDescriptorSet 這個 Vulkan 物件代表一系列描述元資源。描述元資源用於提供著色器輸入內容,例如統一緩衝區、圖像取樣器和儲存空間緩衝區。如要建立 VkDescriptorSet,我們須先建立 VkDescriptorPool。
VkBuffer 這個記憶體緩衝區可用於在 GPU 和 CPU 間分享資料。如果當成統一緩衝區使用,VkBuffer 會將資料以統一變數的格式傳遞到著色器。統一變數是管道中所有著色器都可以存取的常數。
// CODELAB: hellovk.h
void HelloVK::createDescriptorPool() {
VkDescriptorPoolSize poolSize{};
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSize.descriptorCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
VkDescriptorPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = 1;
poolInfo.pPoolSizes = &poolSize;
poolInfo.maxSets = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
VK_CHECK(vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool));
}
建立從指定的 VkDescriptorPool 分配的 VkDescriptorSet。
// CODELAB: hellovk.h
void HelloVK::createDescriptorSets() {
std::vector<VkDescriptorSetLayout> layouts(MAX_FRAMES_IN_FLIGHT,
descriptorSetLayout);
VkDescriptorSetAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.descriptorPool = descriptorPool;
allocInfo.descriptorSetCount = static_cast<uint32_t>(MAX_FRAMES_IN_FLIGHT);
allocInfo.pSetLayouts = layouts.data();
descriptorSets.resize(MAX_FRAMES_IN_FLIGHT);
VK_CHECK(vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()));
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
VkDescriptorBufferInfo bufferInfo{};
bufferInfo.buffer = uniformBuffers[i];
bufferInfo.offset = 0;
bufferInfo.range = sizeof(UniformBufferObject);
VkWriteDescriptorSet descriptorWrite{};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSets[i];
descriptorWrite.dstBinding = 0;
descriptorWrite.dstArrayElement = 0;
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrite.descriptorCount = 1;
descriptorWrite.pBufferInfo = &bufferInfo;
vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
}
}
指定統一緩衝區結構體,並建立統一緩衝區。別忘了使用 vkAllocateMemory 從 VkDeviceMemory 分配記憶體,並使用 vkBindBufferMemory 將緩衝區繫結到記憶體。
// CODELAB: hellovk.h
struct UniformBufferObject {
std::array<float, 16> mvp;
};
void HelloVK::createUniformBuffers() {
VkDeviceSize bufferSize = sizeof(UniformBufferObject);
uniformBuffers.resize(MAX_FRAMES_IN_FLIGHT);
uniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT);
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
uniformBuffers[i], uniformBuffersMemory[i]);
}
}
// CODELAB: hellovk.h
void HelloVK::createBuffer(VkDeviceSize size, VkBufferUsageFlags usage,
VkMemoryPropertyFlags properties, VkBuffer &buffer,
VkDeviceMemory &bufferMemory) {
VkBufferCreateInfo bufferInfo{};
bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
bufferInfo.size = size;
bufferInfo.usage = usage;
bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_CHECK(vkCreateBuffer(device, &bufferInfo, nullptr, &buffer));
VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(device, buffer, &memRequirements);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex =
findMemoryType(memRequirements.memoryTypeBits, properties);
VK_CHECK(vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory));
vkBindBufferMemory(device, buffer, bufferMemory, 0);
}
輔助函式可找出正確的記憶體類型。
// CODELAB: hellovk.h
/*
* Finds the index of the memory heap which matches a particular buffer's memory
* requirements. Vulkan manages these requirements as a bitset, in this case
* expressed through a uint32_t.
*/
uint32_t HelloVK::findMemoryType(uint32_t typeFilter,
VkMemoryPropertyFlags properties) {
VkPhysicalDeviceMemoryProperties memProperties;
vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties);
for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) {
if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags &
properties) == properties) {
return i;
}
}
assert(false); // failed to find a suitable memory type!
return -1;
}
在這個步驟最後,您只能看到未算繪任何內容的全黑視窗,這是因為我們的設定程序尚未完成。如果發生錯誤,您可以比較您的成果與 [codelab] step: descriptorset and uniform buffer
存放區中的修訂版本。
8. 指令緩衝區 - 建立、記錄並繪製
VkCommandPool 這個簡易物件可用於分配 CommandBuffer,會連結到特定的佇列系列。
VkCommandBuffer 這個 Vulkan 物件代表 GPU 執行的指令清單。這個低階物件可精密控管 GPU。
// CODELAB: hellovk.h
void HelloVK::createCommandPool() {
QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);
VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();
VK_CHECK(vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool));
}
// CODELAB: hellovk.h
void HelloVK::createCommandBuffer() {
commandBuffers.resize(MAX_FRAMES_IN_FLIGHT);
VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = commandBuffers.size();
VK_CHECK(vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()));
}
在這個步驟最後,您只能看到未算繪任何內容的全黑視窗,這是因為我們的設定程序尚未完成。如果發生錯誤,您可以比較您的成果與 [codelab] step: create command pool and command buffer
存放區中的修訂版本。
更新統一緩衝區、記錄指令緩衝區並繪製
Vulkan 中的指令 (例如繪圖作業和記憶體轉移) 並非直接透過函式呼叫執行,相反地,所有待處理的作業都記錄在指令緩衝區物件中。這樣做的優點是,當我們準備好向 Vulkan 下達指令時,所有指令都會一起提交,而 Vulkan 可以更有效處理指令,因為所有指令都會一起提供。此外,如果需要的話,此做法允許在多個執行緒中記錄指令。
Vulkan 中的所有算繪作業都發生在 RenderPass 內。在我們的範例中,RenderPass 會算繪到先前設定的 FrameBuffer。
// CODELAB: hellovk.h
void HelloVK::recordCommandBuffer(VkCommandBuffer commandBuffer,
uint32_t imageIndex) {
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = 0;
beginInfo.pInheritanceInfo = nullptr;
VK_CHECK(vkBeginCommandBuffer(commandBuffer, &beginInfo));
VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex];
renderPassInfo.renderArea.offset = {0, 0};
renderPassInfo.renderArea.extent = swapChainExtent;
VkViewport viewport{};
viewport.width = (float)swapChainExtent.width;
viewport.height = (float)swapChainExtent.height;
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);
VkRect2D scissor{};
scissor.extent = swapChainExtent;
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);
static float grey;
grey += 0.005f;
if (grey > 1.0f) {
grey = 0.0f;
}
VkClearValue clearColor = {grey, grey, grey, 1.0f};
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;
vkCmdBeginRenderPass(commandBuffer, &renderPassInfo,
VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
graphicsPipeline);
vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
pipelineLayout, 0, 1, &descriptorSets[currentFrame],
0, nullptr);
vkCmdDraw(commandBuffer, 3, 1, 0, 0);
vkCmdEndRenderPass(commandBuffer);
VK_CHECK(vkEndCommandBuffer(commandBuffer));
}
您可能還需要更新統一緩衝區,因為我們對算繪的所有頂點都使用相同的轉換矩陣。
// CODELAB: hellovk.h
void HelloVK::updateUniformBuffer(uint32_t currentImage) {
SwapChainSupportDetails swapChainSupport =
querySwapChainSupport(physicalDevice);
UniformBufferObject ubo{};
getPrerotationMatrix(swapChainSupport.capabilities, pretransformFlag,
ubo.mvp);
void *data;
vkMapMemory(device, uniformBuffersMemory[currentImage], 0, sizeof(ubo), 0,
&data);
memcpy(data, &ubo, sizeof(ubo));
vkUnmapMemory(device, uniformBuffersMemory[currentImage]);
}
現在可以算繪了!找出您編寫的指令緩衝區,然後提交到佇列中。
// CODELAB: hellovk.h
void HelloVK::render() {
if (orientationChanged) {
onOrientationChange();
}
vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE,
UINT64_MAX);
uint32_t imageIndex;
VkResult result = vkAcquireNextImageKHR(
device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame],
VK_NULL_HANDLE, &imageIndex);
if (result == VK_ERROR_OUT_OF_DATE_KHR) {
recreateSwapChain();
return;
}
assert(result == VK_SUCCESS ||
result == VK_SUBOPTIMAL_KHR); // failed to acquire swap chain image
updateUniformBuffer(currentFrame);
vkResetFences(device, 1, &inFlightFences[currentFrame]);
vkResetCommandBuffer(commandBuffers[currentFrame], 0);
recordCommandBuffer(commandBuffers[currentFrame], imageIndex);
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]};
VkPipelineStageFlags waitStages[] = {
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = waitSemaphores;
submitInfo.pWaitDstStageMask = waitStages;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffers[currentFrame];
VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]};
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = signalSemaphores;
VK_CHECK(vkQueueSubmit(graphicsQueue, 1, &submitInfo,
inFlightFences[currentFrame]));
VkPresentInfoKHR presentInfo{};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
VkSwapchainKHR swapChains[] = {swapChain};
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;
presentInfo.pResults = nullptr;
result = vkQueuePresentKHR(presentQueue, &presentInfo);
if (result == VK_SUBOPTIMAL_KHR) {
orientationChanged = true;
} else if (result == VK_ERROR_OUT_OF_DATE_KHR) {
recreateSwapChain();
} else {
assert(result == VK_SUCCESS); // failed to present swap chain image!
}
currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
}
重新建立交換鏈,處理螢幕方向變更。
// CODELAB: hellovk.h
void HelloVK::onOrientationChange() {
recreateSwapChain();
orientationChanged = false;
}
整合至應用程式生命週期中。
// CODELAB: vk_main.cpp
/**
* Called by the Android runtime whenever events happen so the
* app can react to it.
*/
static void HandleCmd(struct android_app *app, int32_t cmd) {
auto *engine = (VulkanEngine *)app->userData;
switch (cmd) {
case APP_CMD_START:
if (engine->app->window != nullptr) {
engine->app_backend->reset(app->window, app->activity->assetManager);
engine->app_backend->initVulkan();
engine->canRender = true;
}
case APP_CMD_INIT_WINDOW:
// The window is being shown, get it ready.
LOGI("Called - APP_CMD_INIT_WINDOW");
if (engine->app->window != nullptr) {
LOGI("Setting a new surface");
engine->app_backend->reset(app->window, app->activity->assetManager);
if (!engine->app_backend->initialized) {
LOGI("Starting application");
engine->app_backend->initVulkan();
}
engine->canRender = true;
}
break;
case APP_CMD_TERM_WINDOW:
// The window is being hidden or closed, clean it up.
engine->canRender = false;
break;
case APP_CMD_DESTROY:
// The window is being hidden or closed, clean it up.
LOGI("Destroying");
engine->app_backend->cleanup();
default:
break;
}
}
/*
* Entry point required by the Android Glue library.
* This can also be achieved more verbosely by manually declaring JNI functions
* and calling them from the Android application layer.
*/
void android_main(struct android_app *state) {
VulkanEngine engine{};
vkt::HelloVK vulkanBackend{};
engine.app = state;
engine.app_backend = &vulkanBackend;
state->userData = &engine;
state->onAppCmd = HandleCmd;
android_app_set_key_event_filter(state, VulkanKeyEventFilter);
android_app_set_motion_event_filter(state, VulkanMotionEventFilter);
while (true) {
int ident;
int events;
android_poll_source *source;
while ((ident = ALooper_pollAll(engine.canRender ? 0 : -1, nullptr, &events,
(void **)&source)) >= 0) {
if (source != nullptr) {
source->process(state, source);
}
}
HandleInputEvents(state);
engine.app_backend->render();
}
}
在這個步驟最後,畫面終於呈現彩色的三角形了!
確認一切無誤,否則請比較您的成果與 [codelab] step: update uniform buffer, record command buffer and draw
存放區中的修訂版本。
9. 旋轉三角形
如要旋轉三角形,我們需先對 MVP 矩陣套用旋轉機制,然後再將矩陣傳遞給著色器。這是為了避免重複替模型中的頂點逐一計算同個矩陣。
如要在應用程式側計算 MVP 矩陣,就需要用到旋轉轉換矩陣。GLM 程式庫 是 C++ 語言的數學程式庫,內含建構旋轉機制矩陣所需的旋轉函式,有助您根據 GLSL 規範編寫圖像軟體。
// CODELAB: hellovk.h
// Additional includes to make our lives easier than composing
// transformation matrices manually
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
// change our UniformBufferObject construct to use glm::mat4
struct UniformBufferObject {
glm::mat4 mvp;
};
// CODELAB: hellovk.h
/*
* getPrerotationMatrix handles screen rotation with 3 hardcoded rotation
* matrices (detailed below). We skip the 180 degrees rotation.
*/
void getPrerotationMatrix(const VkSurfaceCapabilitiesKHR &capabilities,
const VkSurfaceTransformFlagBitsKHR &pretransformFlag,
glm::mat4 &mat, float ratio) {
// mat is initialized to the identity matrix
mat = glm::mat4(1.0f);
// scale by screen ratio
mat = glm::scale(mat, glm::vec3(1.0f, ratio, 1.0f));
// rotate 1 degree every function call.
static float currentAngleDegrees = 0.0f;
currentAngleDegrees += 1.0f;
if ( currentAngleDegrees >= 360.0f ) {
currentAngleDegrees = 0.0f;
}
mat = glm::rotate(mat, glm::radians(currentAngleDegrees), glm::vec3(0.0f, 0.0f, 1.0f));
}
在這個步驟最後,您會看到畫面上的三角形開始轉動!確認一切無誤,否則請比較您的成果與 [codelab] step: rotate triangle
存放區中的修訂版本。
10. 套用紋理
如要對三角形套用紋理,首先需要以未壓縮格式將圖像檔載入到記憶體中。這個步驟會採用 stb 圖像庫,將圖像資料載入並解碼到 RAM 中,然後整個複製到 Vulkan 的緩衝區 (VkBuffer)。
// CODELAB: hellovk.h
void HelloVK::decodeImage() {
std::vector<uint8_t> imageData = LoadBinaryFileToVector("texture.png",
assetManager);
if (imageData.size() == 0) {
LOGE("Fail to load image.");
return;
}
unsigned char* decodedData = stbi_load_from_memory(imageData.data(),
imageData.size(), &textureWidth, &textureHeight, &textureChannels, 0);
if (decodedData == nullptr) {
LOGE("Fail to load image to memory, %s", stbi_failure_reason());
return;
}
size_t imageSize = textureWidth * textureHeight * textureChannels;
VkBufferCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
createInfo.size = imageSize;
createInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
createInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_CHECK(vkCreateBuffer(device, &createInfo, nullptr, &stagingBuffer));
VkMemoryRequirements memRequirements;
vkGetBufferMemoryRequirements(device, stagingBuffer, &memRequirements);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VK_CHECK(vkAllocateMemory(device, &allocInfo, nullptr, &stagingMemory));
VK_CHECK(vkBindBufferMemory(device, stagingBuffer, stagingMemory, 0));
uint8_t *data;
VK_CHECK(vkMapMemory(device, stagingMemory, 0, memRequirements.size, 0,
(void **)&data));
memcpy(data, decodedData, imageSize);
vkUnmapMemory(device, stagingMemory);
stbi_image_free(decodedData);
}
接著,請找出先前步驟中填入圖像資料的 VkBuffer,從中建立 VkImage。
VkImage 是保存實際紋理資料的物件,會將像素資料保存到紋理的主要記憶體中,但不含讀取方式的大量資訊。因此,我們需要在下一節建立 VkImageView。
// CODELAB: hellovk.h
void HelloVK::createTextureImage() {
VkImageCreateInfo imageInfo{};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D;
imageInfo.extent.width = textureWidth;
imageInfo.extent.height = textureHeight;
imageInfo.extent.depth = 1;
imageInfo.mipLevels = 1;
imageInfo.arrayLayers = 1;
imageInfo.format = VK_FORMAT_R8G8B8_UNORM;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageInfo.usage =
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_CHECK(vkCreateImage(device, &imageInfo, nullptr, &textureImage));
VkMemoryRequirements memRequirements;
vkGetImageMemoryRequirements(device, textureImage, &memRequirements);
VkMemoryAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_CHECK(vkAllocateMemory(device, &allocInfo, nullptr, &textureImageMemory));
vkBindImageMemory(device, textureImage, textureImageMemory, 0);
}
// CODELAB: hellovk.h
void HelloVK::copyBufferToImage() {
VkImageSubresourceRange subresourceRange{};
subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
subresourceRange.baseMipLevel = 0;
subresourceRange.levelCount = 1;
subresourceRange.layerCount = 1;
VkImageMemoryBarrier imageMemoryBarrier{};
imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
imageMemoryBarrier.image = textureImage;
imageMemoryBarrier.subresourceRange = subresourceRange;
imageMemoryBarrier.srcAccessMask = 0;
imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
VkCommandBuffer cmd;
VkCommandBufferAllocateInfo cmdAllocInfo{};
cmdAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cmdAllocInfo.commandPool = commandPool;
cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
cmdAllocInfo.commandBufferCount = 1;
VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &cmd));
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
vkBeginCommandBuffer(cmd, &beginInfo);
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_HOST_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0,
nullptr, 1, &imageMemoryBarrier);
VkBufferImageCopy bufferImageCopy{};
bufferImageCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
bufferImageCopy.imageSubresource.mipLevel = 0;
bufferImageCopy.imageSubresource.baseArrayLayer = 0;
bufferImageCopy.imageSubresource.layerCount = 1;
bufferImageCopy.imageExtent.width = textureWidth;
bufferImageCopy.imageExtent.height = textureHeight;
bufferImageCopy.imageExtent.depth = 1;
bufferImageCopy.bufferOffset = 0;
vkCmdCopyBufferToImage(cmd, stagingBuffer, textureImage,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &bufferImageCopy);
imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr,
0, nullptr, 1, &imageMemoryBarrier);
vkEndCommandBuffer(cmd);
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &cmd;
VK_CHECK(vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE));
vkQueueWaitIdle(graphicsQueue);
}
接著,請建立 VkImageView 和 VkSampler,供片段著色器用於對算繪的像素顏色逐一取樣。
VkImageView 是 VkImage 上方的包裝函式,可保存有關如何解釋紋理資料的資訊;舉例來說,您可能只想存取某個區域或圖層,或想以特定方式重組像素管道。
VkSampler 保存的資料可供特定著色器存取紋理。此物件保留有關如何混和像素/執行 mipmap 的資訊。您可以將取樣器與描述元中的 VkImageView 併用。
// CODELAB: hellovk.h
void HelloVK::createTextureImageViews() {
VkImageViewCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
createInfo.image = textureImage;
createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
createInfo.format = VK_FORMAT_R8G8B8_UNORM;
createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
createInfo.subresourceRange.baseMipLevel = 0;
createInfo.subresourceRange.levelCount = 1;
createInfo.subresourceRange.baseArrayLayer = 0;
createInfo.subresourceRange.layerCount = 1;
VK_CHECK(vkCreateImageView(device, &createInfo, nullptr, &textureImageView));
}
我們也需要建立取樣器,傳遞到著色器中。
// CODELAB: hellovk.h
void HelloVK::createTextureSampler() {
VkSamplerCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
createInfo.magFilter = VK_FILTER_LINEAR;
createInfo.minFilter = VK_FILTER_LINEAR;
createInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
createInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
createInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
createInfo.anisotropyEnable = VK_FALSE;
createInfo.maxAnisotropy = 16;
createInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
createInfo.unnormalizedCoordinates = VK_FALSE;
createInfo.compareEnable = VK_FALSE;
createInfo.compareOp = VK_COMPARE_OP_ALWAYS;
createInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
createInfo.mipLodBias = 0.0f;
createInfo.minLod = 0.0f;
createInfo.maxLod = VK_LOD_CLAMP_NONE;
VK_CHECK(vkCreateSampler(device, &createInfo, nullptr, &textureSampler));
}
最後,請修改著色器以對圖像取樣,不要使用頂點顏色。紋理座標是浮點位置,可將紋理上的位置對應到幾何介面的位置。在範例中,如要完成這個程序,我們要將 vTexCoords
定義為頂點著色器的輸出內容,由於是正規化 ({1, 1} 尺寸) 三角形,因此請直接用頂點的 texCoords
填充著色器。
// CODELAB: shader.vert
#version 450
// Uniform buffer containing an MVP matrix.
// Currently the vulkan backend only sets the rotation matrix
// required to handle device rotation.
layout(binding = 0) uniform UniformBufferObject {
mat4 MVP;
} ubo;
vec2 positions[3] = vec2[](
vec2(0.0, 0.577),
vec2(-0.5, -0.289),
vec2(0.5, -0.289)
);
vec2 texCoords[3] = vec2[](
vec2(0.5, 1.0),
vec2(0.0, 0.0),
vec2(1.0, 0.0)
);
layout(location = 0) out vec2 vTexCoords;
void main() {
gl_Position = ubo.MVP * vec4(positions[gl_VertexIndex], 0.0, 1.0);
vTexCoords = texCoords[gl_VertexIndex];
}
片段著色器會使用取樣器和紋理。
// CODELAB: shader.frag
#version 450
layout(location = 0) in vec2 vTexCoords;
layout(binding = 1) uniform sampler2D samp;
// Output colour for the fragment
layout(location = 0) out vec4 outColor;
void main() {
outColor = texture(samp, vTexCoords);
}
在這個步驟最後,您會看到旋轉的三角形加上紋理!
確認一切無誤,否則請比較您的成果與 [codelab] step: apply texture
存放區中的修訂版本。
11. 新增驗證層
驗證層是選用元件,可掛接到 Vulkan 函數呼叫中,以便套用以下其他作業:
- 驗證參數的值,偵測濫用情形
- 追蹤物件的建立和刪除程序,找出資源是否洩漏
- 確認執行緒安全
- 記錄呼叫,以便剖析及重播
由於驗證層的下載內容相當大,我們選擇不在 APK 中提供驗證層。因此,如要啟用驗證層,請按照以下簡易步驟操作:
從以下位置下載最新的 Android 二進位檔:https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases
將二進位檔放在各自的 ABI 資料夾中:app/src/main/jniLibs
按照下列步驟啟用驗證層。
// CODELAB: hellovk.h
void HelloVK::createInstance() {
assert(!enableValidationLayers ||
checkValidationLayerSupport()); // validation layers requested, but not available!
auto requiredExtensions = getRequiredExtensions(enableValidationLayers);
VkApplicationInfo appInfo{};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Hello Triangle";
appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.pEngineName = "No Engine";
appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0);
appInfo.apiVersion = VK_API_VERSION_1_0;
VkInstanceCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
createInfo.enabledExtensionCount = (uint32_t)requiredExtensions.size();
createInfo.ppEnabledExtensionNames = requiredExtensions.data();
createInfo.pApplicationInfo = &appInfo;
if (enableValidationLayers) {
VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{};
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;
}
VK_CHECK(vkCreateInstance(&createInfo, nullptr, &instance));
uint32_t extensionCount = 0;
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
std::vector<VkExtensionProperties> extensions(extensionCount);
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount,
extensions.data());
LOGI("available extensions");
for (const auto &extension : extensions) {
LOGI("\t %s", extension.extensionName);
}
}
12. 恭喜
恭喜!您已順利設定 Vulkan 算繪管道,可以開發遊戲了!
我們將在日後為 Android 的 Vulkan 推出更多新功能,敬請持續關注。
如要進一步瞭解如何開始在 Android 上使用 Vulkan,請參閱「開始使用 Android 上的 Vulkan」。