[Vulkan Tutorial] 10-Create Swap Chain

목차: 01-Overview (Link)
이전 글: 09-Create Logical Device (Link)
다음 글: 11-Create Image Views (Link)


What is Swap Chain?

이번 장에서는 Swap Chain을 생성할 차례이다. 이전에 Swap Chain에 대해서 정리한 적이 있다 (출처 2). Vulkan Tutorial에서 설명하는 Swap Chain에 대해서 추가로 정리해보았다. 결과적으로 출처 2에 작성한 내용과 큰(?) 차이는 없으나, 조금 더 상세하게 설명을 작성하였다. 내용은 출처 1, 3, 4 내용을 기반으로 작성하였다.

그림 1: GPU, Swap Chain, Image, Surface 관계

그림 1은 Swap Chain, Image, Surface (06-Create Window Surface에서 설명한 내용)의 관계를 보여준다. 출처 1에 설명에 따르면 Swap Chain은 “a Queue of Images”이다. 정확히 설명하면 여러 개의 Image로 구성된 FIFO (First In First Out) 형태의 Array를 의미한다. Swap Chain (Image Queue)에 있는 Image는 화면에 출력되기를 기다리고 있는 Image이다 (The swap chain is essentially a queue of images that are waiting to be presented to the screen). 우리가 작성하는 Application은 Image를 받아서 Rendering 한 결과를 작성한다 (그림을 그린다). 이렇게 그려진 Image는 Swap Chain (Image Queue)로 Insert(전달) 되고, 그 결과 Image는 나중에 Window Surface(화면)에 출력된다.

Android 단말의 경우 보통 Triple Buffering을 사용한다 (출처 5). 이전에 Triple Buffering을 사용하는 이유에 대해서도 작성한 적이 있다 (출처 6). Android 단말의 경우 그림 1과 같이 Swap Chain에 3개의 Image를 가지게 된다. 원하는 경우 3개 이상의 Image로 구성된 Swap Chain 역시 생성이 가능하다.


Creating Swap Chain

Swap Chain을 생성할 차례이다. 너무 당연한 이야기이지만 Vulkan을 사용하는 경우 Default Framebuffer 같은 개념이 존재하지 않기 때문에 Swap Chain, Image, Framebuffer 등 모든 부분을 직접 생성해야 한다. 

추가로 모든 GPU가 Image Presentation (출력) 기능을 지원하는 것은 아니다. Vulkan API를 사용해서 Application 작성 시 GPU가 Image Presentation 기능을 지원하는지 먼저 확인이 필요하다. 출처 1에서는 Presentation 지원 여부를 확인하는 코드를 추가로 작성한다. 하지만, 현재 실험을 진행하고 있는 안드로이드 단말의 GPU는 Image Presentation 기능을 지원한다. 그래서 Swap Chain 생성 과정에 Image Presentation 지원 여부를 판단하는 코드는 작성하지 않았다.

MK: Vulkan API를 지원하는 Android 단말에서 Presentation 기능을 지원하지 않는 단말이 없는 것으로 알고 있다.

아래 코드 1은 Swap Chain을 생성하는 코드를 추가한 코드이다. 

코드 1: Swap Chain 생성 부분을 추가한 코드

#include <iostream>
#include <stdexcept>
#include <functional>
#include <cstdlib>
#include <util_init.hpp>
#include <set>

#define APP_SHORT_NAME "mkVulkanExample"

#define MK_GET_INSTANCE_PROC_ADDR(inst, entrypoint)												\
{																								\
	fp##entrypoint = (PFN_vk##entrypoint)vkGetInstanceProcAddr(instance, "vk" #entrypoint);		\
	if (fp##entrypoint == NULL) {																\
		std::cout << "vkGetDeviceProcAddr failed to find vk" #entrypoint;						\
		exit(-1);																				\
	}																							\
}


class mkTriangle{
	public:
		void run(){
			LOGI("MK: mkTriangle-->run Function");
			initVulkan();
			mainLoop();
			cleanup();
		}

	private:
		void initVulkan(){
			createInstance();
			createSurface();
			pickPhysicalDevice();
			searchQueue();
			createLogicalDevice();

			//MK: (코드 1-2) createSwapChain() 함수 호출
			createSwapChain();
		}

		void mainLoop(){
		}

		void cleanup(){
			LOGI("MK: cleanup Function");

			//MK: (코드 1-14) Swap Chain 제거 함수 호출
			vkDestroySwapchainKHR(device, swapChain, NULL);

			vkDestroyDevice(device, NULL);
			vkDestroySurfaceKHR(instance, surface, NULL);
			vkDestroyInstance(instance, NULL);
		}

		void createInstance(){

			LOGI("MK: createInstance() Function");
			VkApplicationInfo appInfo = {};
			appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
			appInfo.pNext = NULL;
			appInfo.pApplicationName = APP_SHORT_NAME;
			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;

			uint32_t extensionCount = 0;
			vkEnumerateInstanceExtensionProperties(NULL, &extensionCount, NULL);
			std::vector<VkExtensionProperties> extensions(extensionCount);
			vkEnumerateInstanceExtensionProperties(NULL, &extensionCount, extensions.data());
			LOGI("MK: Instance Extensions Count - %d", extensionCount);
			std::vector<const char *> instanceExtensions;
			for (const auto& extension : extensions){
				LOGI("MK: Instance Extension - %s", extension.extensionName);
				//MK: 필요 Extnesion은 String으로 보내야 함
				instanceExtensions.push_back(extension.extensionName);
			}

			uint32_t layerCount = 0;
			vkEnumerateInstanceLayerProperties(&layerCount, NULL);
			std::vector<VkLayerProperties> layers(layerCount);
			vkEnumerateInstanceLayerProperties(&layerCount, layers.data());

			LOGI("MK: Instance Layer Count - %d", layerCount);
			std::vector<const char *> instanceLayers;
			for(const auto& layer : layers){
				LOGI("MK: Instance Layer - %s", layer.layerName);
				//MK: 필요 layer은 String으로 보내야함
				instanceLayers.push_back(layer.layerName);
			}

			//LOGI("MK: Create VkInstanceCreateInfo");
			VkInstanceCreateInfo createInfo = {};
			createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
			createInfo.pNext = NULL;
			createInfo.flags = 0;
			createInfo.pApplicationInfo = &appInfo;
			createInfo.enabledLayerCount = layerCount;
			createInfo.ppEnabledLayerNames = layerCount ? instanceLayers.data() : NULL;
			createInfo.enabledExtensionCount = extensionCount;
			createInfo.ppEnabledExtensionNames = extensionCount ? instanceExtensions.data() : NULL;

			VkResult result = vkCreateInstance(&createInfo, NULL, &instance);
			
			assert(result == VK_SUCCESS);
			LOGI("MK: Successfully Create Instance (If not, should not see this print)");

		}

		void createSurface(){
			
			LOGI("MK: createSurface Function");
			VkAndroidSurfaceCreateInfoKHR createInfo = {};
			createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
			createInfo.pNext = NULL;
			createInfo.flags = 0;
			createInfo.window = AndroidGetApplicationWindow();

			MK_GET_INSTANCE_PROC_ADDR(instance, CreateAndroidSurfaceKHR);
			
			VkResult result = fpCreateAndroidSurfaceKHR(instance, &createInfo, NULL, &surface);

			assert(result == VK_SUCCESS);
			LOGI("MK: Successfully Create Surface (If not, should not see this print)");
		}

		void pickPhysicalDevice(){
			LOGI("MK: pickPhysicalDevice Function");

			uint32_t deviceCount = 0;
			vkEnumeratePhysicalDevices(instance, &deviceCount, NULL);
			assert(deviceCount != 0);

			LOGI("MK: Physical Device Count - %d", deviceCount);

			std::vector<VkPhysicalDevice> devices(deviceCount);
			vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data());

			for(const auto &device : devices){
				printPhysicalDeviceInfo(device);
			}

			physicalDevice = devices[0];

			assert(physicalDevice != VK_NULL_HANDLE);
			LOGI("MK: Successfully Select Physical Device (If not, should not see this print)");
		}

		void printPhysicalDeviceInfo(VkPhysicalDevice device){
			VkPhysicalDeviceProperties deviceProperties;
			VkPhysicalDeviceFeatures deviceFeatures;
			vkGetPhysicalDeviceProperties(device, &deviceProperties);
			vkGetPhysicalDeviceFeatures(device, &deviceFeatures);

			LOGI("MK: Physical Device Name - %s", deviceProperties.deviceName);
			LOGI("MK: Physical Device - geometryShader (%d)", deviceFeatures.geometryShader);
			LOGI("MK: Physical Device - shaderInt64 (%d)", deviceFeatures.shaderInt64);
		}

		void searchQueue(){
			LOGI("MK: searchQueue Function");

			uint32_t queueCount = 0;
			vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueCount, NULL);
			assert(queueCount > 0);
			std::vector<VkQueueFamilyProperties> queueFamilies(queueCount);
			vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueCount, queueFamilies.data());

			LOGI("MK: Queue Family Count - %d", queueCount);

			bool graphicsFound = false;
			bool presentFound = false;
			for(int i = 0; i < queueCount; i++){
				LOGI("MK: %d QueueFamily has a total of %d", i, queueFamilies[i].queueCount);
				if(queueFamilies[i].queueFlags & VK_QUEUE_GRAPHICS_BIT){
					graphicQueueFamilyIndex = i;
					graphicsFound = true;
				}

				VkBool32 presentSupport = false;
				vkGetPhysicalDeviceSurfaceSupportKHR(physicalDevice, i, surface, &presentSupport);

				if(presentSupport){
					presentQueueFamilyIndex = i;
					presentFound = true;
				}

				if(presentFound && graphicsFound){
					break;
				}
			}
			assert(graphicsFound);
			assert(presentFound);

			LOGI("MK: Found Graphic Queue Family = %d", graphicQueueFamilyIndex);
			LOGI("MK: Found Queue Family with Present Support = %d", presentQueueFamilyIndex);

			LOGI("MK: Found Graphics Queue with Present Support (If not, should not see this print)");
		}

		void createLogicalDevice(){

			LOGI("MK: createLogicalDevice Function");
			
			std::set<uint32_t> uniqueQueueFamilies = {graphicQueueFamilyIndex, presentQueueFamilyIndex};

			std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;

			float queuePriority = 1.0f;
			for(uint32_t queueFamily : uniqueQueueFamilies){
				VkDeviceQueueCreateInfo queueCreateInfo = {};
				queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
				queueCreateInfo.pNext = NULL;
				queueCreateInfo.flags = 0;
				queueCreateInfo.queueFamilyIndex = queueFamily;
				queueCreateInfo.queueCount = 1;
				queueCreateInfo.pQueuePriorities = &queuePriority;
				queueCreateInfos.push_back(queueCreateInfo);
			}

			//MK: (코드 1-3) Swap Chain Extension 추가
			//MK: create logical device 설명 때 추가함
			VkPhysicalDeviceFeatures deviceFeatures = {};
			std::vector<const char *> deviceExtensions;
			deviceExtensions.push_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME);

			VkDeviceCreateInfo createInfo = {};
			createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
			createInfo.pNext = NULL;
			createInfo.flags = 0;
			createInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
			createInfo.pQueueCreateInfos = queueCreateInfos.data();
			//createInfo.enabledLayerCount
			//createInfo.ppEnabledLayerNames
			createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
			createInfo.ppEnabledExtensionNames = deviceExtensions.data();
			createInfo.pEnabledFeatures = &deviceFeatures;

			VkResult result = vkCreateDevice(physicalDevice, &createInfo, NULL, &device);

			assert(result == VK_SUCCESS);
			LOGI("MK: Successfully Create Logical Device (If not, should not see this print)");

			vkGetDeviceQueue(device, graphicQueueFamilyIndex, 0, &graphicsQueue);
			vkGetDeviceQueue(device, presentQueueFamilyIndex, 0, &presentQueue);
		}

		//MK: (코드 1-1) createSwapChain() 함수 생성
		void createSwapChain(){
			LOGI("MK: (Begin) createSwapChain Function");
			
			//MK: (코드 1-5) Surface Capability 로딩  코드
			VkResult result = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(physicalDevice, surface, &surfaceCapabilities);
			assert(result == VK_SUCCESS);
			LOGI("\tMK: Successfully Getting Surface Capability");
			LOGI("\t\tMK: minImageCount - %d, maxImageCount - %d", surfaceCapabilities.minImageCount, surfaceCapabilities.maxImageCount);

			//MK: (코드 1-6) Surface Format 로딩 코드
			uint32_t formatCount;
			result = vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, NULL);
			assert(result == VK_SUCCESS);
			assert(formatCount > 0);
			surfaceFormats.resize(formatCount);
			result = vkGetPhysicalDeviceSurfaceFormatsKHR(physicalDevice, surface, &formatCount, surfaceFormats.data());
			LOGI("\tMK: Successfully Getting Surface Formats (Total Format Count - %d)", formatCount);
			for(int i = 0; i < formatCount; i++){
				LOGI("\t\tMK: Surface Format - %d, Color Space - %d", surfaceFormats[i].format, surfaceFormats[i].colorSpace);
			}

			//MK: (코드 1-7) Presentation Mode 로딩 코드
			uint32_t presentModeCount;
			result = vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModeCount, NULL);
			assert(result == VK_SUCCESS);
			assert(presentModeCount > 0);
			presentModes.resize(presentModeCount);
			result = vkGetPhysicalDeviceSurfacePresentModesKHR(physicalDevice, surface, &presentModeCount, presentModes.data());
			LOGI("\tMK: Successfully Getting Presentation Modes (Total Presentation Mode Count - %d)", presentModeCount);
			for(int i = 0; i < presentModeCount; i++){
				LOGI("\t\tMK: Present Mode - %d", presentModes[i]);
			}

			//MK: (코드 1-9) Surface Format 선택하는 코드
			bool found = false;
			for(int i = 0; i < formatCount; i++){
				if(surfaceFormats[i].format == VK_FORMAT_R8G8B8A8_UNORM){
					LOGI("\tMK: Found VK_FORMAT_R8G8B8A8_UNORM (ID - %d)", i);
					selectedSurfaceFormat = surfaceFormats[i];
					found = true;
					break;
				}
			}

			if(!found){
				selectedSurfaceFormat = surfaceFormats[0];
				LOGI("\tMK: Select First Available Surface Format");
			}


			//MK: (코드 1-10) Presentation Mode 선택 코드
			found = false;
			for(int i = 0; i < presentModeCount; i++){
				if(presentModes[i] == VK_PRESENT_MODE_MAILBOX_KHR){
					LOGI("\tMK: Found VK_PRESENT_MODE_MAILBOX_KHR (ID - %d)", i);
					selectedPresentMode = VK_PRESENT_MODE_MAILBOX_KHR;
					found = true;
					break;
				}
			}

			if(!found){
				LOGI("\tMK: Default Prsent Mode: VK_PRESENT_MODE_FIFO_KHR");
				selectedPresentMode = VK_PRESENT_MODE_FIFO_KHR;
			}

			//MK: (코드 1-11) Swap Extent 선택 코드
			int width, height;
			AndroidGetWindowSize(&width, &height);
			if(surfaceCapabilities.currentExtent.width == UINT32_MAX){
				selectedExtent.width = width;
				selectedExtent.height = height;
				//MK: Width Min/Max 안에 값이 존재하는지 확인
				if(selectedExtent.width < surfaceCapabilities.minImageExtent.width){
					selectedExtent.width = surfaceCapabilities.minImageExtent.width;
				}
				else if(selectedExtent.width > surfaceCapabilities.maxImageExtent.width){
					selectedExtent.width = surfaceCapabilities.maxImageExtent.width;
				}
				//MK: Height Min/Max 안에 값이 존재하는지 확인
				if(selectedExtent.height < surfaceCapabilities.minImageExtent.height){
					selectedExtent.height = surfaceCapabilities.minImageExtent.height;
				}
				else if(selectedExtent.height > surfaceCapabilities.maxImageExtent.height){
					selectedExtent.height = surfaceCapabilities.maxImageExtent.height;
				}
			}
			else{
				//MK: Swap Exnet가 미리 정의된 경우 해당 Setting을 사용함 
				selectedExtent = surfaceCapabilities.currentExtent;
			}
			LOGI("\tMK: Selected Swap Extent Size - (%d x %d)", selectedExtent.width, selectedExtent.height);

			//MK: (코드 1-12) Image 개수 선택 코드
			selectedImageCount = surfaceCapabilities.minImageCount + 1;
			if(surfaceCapabilities.maxImageCount > 0 && selectedImageCount > surfaceCapabilities.maxImageCount){
				selectedImageCount = surfaceCapabilities.maxImageCount;
			}
			LOGI("\tMK: Selected Image Count - %d", selectedImageCount);

			//MK: (코드 1-14) Swap Chain 생성 코드
			VkSwapchainCreateInfoKHR createInfo = {};
			createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
			createInfo.pNext = NULL;
			createInfo.flags = 0;
			createInfo.surface = surface;
			createInfo.minImageCount = selectedImageCount;
			createInfo.imageFormat = selectedSurfaceFormat.format;
			createInfo.imageColorSpace = selectedSurfaceFormat.colorSpace;
			createInfo.imageExtent = selectedExtent;
			createInfo.imageArrayLayers = 1;
			createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
			uint32_t queueFamilyIndices[] = {graphicQueueFamilyIndex, presentQueueFamilyIndex};
			if(graphicQueueFamilyIndex != presentQueueFamilyIndex){
				createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
				createInfo.queueFamilyIndexCount = 2;
				createInfo.pQueueFamilyIndices = queueFamilyIndices;
			}
			else{
				createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
				createInfo.queueFamilyIndexCount = 0;
				createInfo.pQueueFamilyIndices = NULL;
			}
			createInfo.preTransform = surfaceCapabilities.currentTransform;
			createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
			createInfo.presentMode = selectedPresentMode;
			createInfo.clipped = VK_TRUE;
			createInfo.oldSwapchain = VK_NULL_HANDLE;

			result = vkCreateSwapchainKHR(device, &createInfo, NULL, &swapChain);
			assert(result == VK_SUCCESS);
			LOGI("MK: Successfully Create Swap Chain (If not, should not see this print)");

			//MK: (코드 1-16) Swap Chain Image 저장 
			uint32_t imageCount;
			result = vkGetSwapchainImagesKHR(device, swapChain, &imageCount, NULL);
			assert(result == VK_SUCCESS);
			swapChainImages.resize(imageCount);
			result = vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());
			assert(result == VK_SUCCESS);
			LOGI("MK: Successfully Retrieveing %d Swap Chain Images (If not, should not see this print)", imageCount);

			LOGI("MK: (End) createSwapChain Function");
		}

		VkInstance instance;

		VkSurfaceKHR surface;
		PFN_vkCreateAndroidSurfaceKHR fpCreateAndroidSurfaceKHR;

		VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;

		uint32_t graphicQueueFamilyIndex = 0;
		uint32_t presentQueueFamilyIndex = 0;

		VkDevice device; 

		VkQueue graphicsQueue;
		VkQueue presentQueue;

		//MK: (코드 1-4) Basic Surface Capability, Surface Formats, Present Mode를 확인 하기 위해서 변수 추가
		VkSurfaceCapabilitiesKHR surfaceCapabilities;
		std::vector<VkSurfaceFormatKHR> surfaceFormats;
		std::vector<VkPresentModeKHR> presentModes;

		//MK: (코드 1-8) Surface Format, Presentation Mode, Swap Extend, Image 개수를 선택하기 위해서 변수 추가
		VkSurfaceFormatKHR selectedSurfaceFormat;
		VkPresentModeKHR selectedPresentMode;
		VkExtent2D selectedExtent;
		uint32_t selectedImageCount;

		//MK: (코드 1-13) Swap Chain 변수 추가
		VkSwapchainKHR swapChain;

		//MK: (코드 1-15) Swap Chain Image 변수 추가
		std::vector<VkImage> swapChainImages;
};

int sample_main(int argc, char *argv[]) {
	
	if(!InitVulkan()){
		LOGE("MK: Failed to Initialize Vulkan APIs");
		return EXIT_FAILURE;
	}
		
	mkTriangle mkApp;

	try{
		mkApp.run();
	} catch (const std::exception &e){
		std::cerr << e.what() << std::endl;
		LOGE("MK: Failed to Run Application");
		return EXIT_FAILURE;
	}
	return EXIT_SUCCESS;
}

먼저 코드 1-1과 같이 createSwapChain(…) 함수를 하나 생성한다. 이 함수는 코드 1-2 initVulkan(…) 함수에서 호출한다. Swap Chain 생성은 이전 글과 달리 상대적으로 아주 긴 설명이 필요하다. 그래서 출처 1과 동일하게 여러 Sub-Section으로 나누어서 코드에 대한 설명을 진행한다.


Creating Swap Chain: Enabling Device Extension

코드 설명을 진행하기 이전에 Swap Chain을 생성(사용)하기 위해서는 Device Extension이 필요하다. 해당 Extension은 VK_KHR_SWAPCHAIN_EXTENSION_NAME (VK_KHR_swapchain)이다. 해당 Extension은 우리가 앞장에서 작성한 Create Logical Device 코드에 미리 추가하였다. 앞장에서 추가한 Swap Chain Extension 코드는 코드 1-3에서 확인할 수 있다.


Creating Swap Chain: Querying Details of Swap Chain Support

단순히 Swap Chain Extension을 추가하는 것만으로 Swap Chain을 생성할 수 있는 게 아니라고 한다. 현재 사용하고 있는 Window Surface와 호환(Compatible) 가능한지 확인이 필요하다. Window Surface와 호환 가능한지 확인하기 위해서 아래 3가지 정보 값을 먼저 로딩할 필요가 있다.

  1. Surface Capability (Min/Max Image 개수, Min/Max Image Width/Height 크기 등)
  2. Surface Formats (Pixel Format, Color Space)
  3. Presentation Modes 

위 3가지 정보 값을 로딩하기 위해서 코드 1-4와 같이 3개의 변수를 추가한다.

가장 먼저 Surface Capability 확인하기 위한 코드를 작성한다. 먼저 코드 1-5와 같이 vkGetPhysicalDeviceSurfaceCapabilitiesKHR(…) 함수를 사용해서 Surface Capability 정보를 Query 한다. vkGetPhysicalDeviceSurfaceCapabilitiesKHR(…) 함수는 아래와 같이 3개의 인자(Parameter)를 가진다. 

vkGetPhysicalDeviceSurfaceCapabilitiesKHR(…) (출처 7)

  • physicalDevice (VkPhysicalDevice): Swap Chain과 연동할 예정인 Physical Device 변수
  • surface (VkSurfaceKHR): Swap Chain과 연동할 Window Surface
  • pSurfaceCapabilities (VkSurfaceCapabilitiesKHR*): VkSurfaceCapabilitiesKHR 변수 포인터 (Capability 확인을 위한 변수)

vkGetPhysicalDeviceSurfaceCapabilitiesKHR(…) 함수를 사용하면 VkSurfaceCapabilitiesKHR Struct 변수에 정보 값이 로딩된다. 정상적으로 로딩이 되었는지 확인을 하기 위해서 Struct에 저장된 여러 개의 값 중 Min/Max Image 개수를 확인하는 코드를 추가해보았다. VkSurfaceCapabilitiesKHR Struct은 아래와 같이 10개의 변수로 구성되어 있다. 

VkSurfaceCapabilitiesKHR Struct (출처 8)

  • minImageCount (uint32_t)
  • maxImageCount (uint32_t)
  • currentExtent (VkExtent2D)
  • minImageExtent (VkExtent2D)
  • maxImageExtent (VkExtent2D)
  • maxImageArrayLayers (uint32_t)
  • supportedTransforms (VkSurfaceTransformFlagsKHR)
  • currentTransform (VkSurfaceTransformFlagBitsKHR)
  • supportedCompositeAlpha (VkCompositeAlphaFlagsKHR)
  • supportedUsageFlags(VkImageUsageFlags)

MK: Surface Capability를 확인하기 위해서 로딩한 VkSurfaceCapabilitiesKHR Struct 는 다양한 변수를 가진다. 해당 변수의 설명을 찾아보면 Swap Chain이 가질 수 있는 Image 개수, 지원하는 Surface 크기(Width/Height), Image Layer 개수 등을 파악할 수 있다. 모든 값을 다 설명하기도 힘들 뿐만 아니라 다 알지도 못하기 때문에 대략적인 설명만 찾아보고 작성하였다.

Surface Capability 확인하기 위한 로딩이 완료되면 다음은 Surface Format을 확인하기 위한 코드를 작성한다. 코드 1-6은 Surface Format을 로딩하기 위한 코드이다. vkGetPhysicalDeviceSurfaceFormatsKHR(…) 함수를 사용해서 지원하는 Surface Format을 로딩한다. 해당 함수는 총 2차례 호출하게 된다. 처음 호출은 지원하는 Surface Format의 개수를 파악하기 위해서 사용되고, 2번째 호출은 Surface Format 값을 로딩하기 위해서 사용한다. vkGetPhysicalDeviceSurfaceFormatsKHR(…) 함수는 아래 4개의 인자를 가진다. 

vkGetPhysicalDeviceSurfaceFormatsKHR(…) (출처 9)

  • physicalDevice (VkPhysicalDevice): Swap Chain과 연동할 예정인 Physical Device 변수
  • surface (VkSurfaceKHR): Swap Chain과 연동할 Window Surface
  • pSurfaceFormatCount (uint32_t*): 지원하는 Surface Format 개수 값을 저장할 변수 
  • pSurfaceFormats (VkSurfaceFormatKHR*): VkSurfaceFormatKHR 변수 포인터 (Surface Format 확인을 위한 변수)

현재 사용하는 단말의 경우 Vulkan API를 지원하는 관계로 당연히 1개 이상의 Surface Format을 지원한다. 지원하는 모든 Surface Format 정보를 출력하는 코드 역시 추가로 작성하였다. VkSurfaceFormatKHR Struct는 2개의 변수로 구성되어 있다. 

VkSurfaceFormatKHR Struct (출처 10)

  • format (VkFormat)
  • colorSpace (VkColorSpaceKHR)

MK: VkSurfaceFormatKHR (Surface Format 확인) Struct은 Format, Color Space 변수로 구성되어 있다. Format의 경우 Color 색상을 저장하는 방법을 의미한다. Format의 하나의 예로 “VK_FORMAT_R8G8B8A8_UNORM”이 있다 (출처 11). 해당 Format은 하나의 Pixel 값을 각 8-Bit Unsigned R(Red), G(Green), B(Blue), A(Alpha)로 구성한다는 의미이다. Color Space는 위 Pixel 값을 표현하는 방법을 의미한다 (출처 12, 출처 13). 32-Bit로 구성된 색상을 어떤 방식으로 표현할지는 Display(모니터 포함) 제조사 마다 차이가 있을 수 있다. 해당 Pixel 값을 표현하는 방법을 정의한 것이 Color Space를 의미한다. 현재 가장 많이 사용하는 Color Space 방법은 sRGB 방식이다. sRGB 방식에 대한 설명은 출처 14에서 확인할 수 있다.

이제 이번 Sub-Section의 마지막인 현재 지원하는 Presentation Mode를 확인하는 코드를 작성할 차례이다. 해당 코드는 Surface Format 로딩하는 부분과 동일한 순서로 2차례 함수를 호출해서 지원하는 Presentation Mode를 확인한다. 코드 1-7은 지원하는 Presentation Mode를 로딩하는 코드이다. vkGetPhysicalDeviceSurfacePresentModesKHR(…) 함수를 사용해서 지원하는 Presentation Mode를 로딩한다. vkGetPhysicalDeviceSurfacePresentModesKHR(…) 함수는 아래 4가지 인자를 가진다. 

vkGetPhysicalDeviceSurfacePresentModesKHR(…) (출처 15)

  • physicalDevice (VkPhysicalDevice): Swap Chain과 연동할 예정인 Physical Device 변수
  • surface (VkSurfaceKHR): Swap Chain과 연동할 Window Surface
  • pPresentModeCount (uint32_t*): 지원하는 Presentatation Mode 개수 값을 저장할 변수 
  • pPresentModes (VkPresentModeKHR*): VkPresentModeKHR 변수 포인터 (Presentation Mode를 확인하기 위한 변수)

지원하는 모든 Presentation Mode를 출력하는 코드도 추가하였다. 

MK: Presentation Mode는 Image를 Display 하는 방법에 대한 설명이다. 예를 들어 GPU가 Rendering 한 결과를 바로 모니터에 보여주는 Immediate 방법이 있다. 해당 Display 방법을 사용하면 GPU에 Rendering 되는 결과를 모니터에 바로 출력한다. 그 결과로 Image 결과물에 Tearing(완성되지 않은 그림이 출력되는 현상)이 발생한다. 또 다른 Presentation Mode는 FIFO 방법으로 Queue를 사용해서 Queue의 제일 앞에 있는 이미지를 모니터에 출력하는 방법이다. 이러한 이미지를 출력하는 방법을 Presentation Mode 라고 한다.

지원하는 Surface Capability, Surface Formats, Presentation Modes 값을 모두 로딩하였다.


Creating Swap Chain: Choosing The Right Settings for The Swap Chain

Surface Capability, Surface Formats, Presentation Modes 확인을 위해서 몇 가지 필요한 값을 로딩했다. 로딩한 값을 기준으로 아래 4가지 Setting을 선택해야 한다. 

  • Surface Format (Color Depth)
  • Presentation Mode (Image를 Display(출력)하는 방법)
  • Swap Extent (Presentation 되는 Image의 크기) (Width/Height) (Resolution)
  • Image Count (Swap Chain을 구성하는 Image 개수)

먼저 Surface Format, Presentation Mode, Swap Extent, Image 개수를 저장할 변수를 추가한다. 코드 1-8은 4개의 변수를 추가한 코드이다. 

이제 Surface Format을 선택할 차례이다. 앞에서 설명한 것과 같이 Surface Format은 색상을 어떤 방식으로 저장할지를 의미한다. 우리는 VK_FORMAT_R8G8B8A8_UNORM Format을 사용할 예정이다. 만약 해당 Format을 지원하지 않는 경우 제일 처음 지원하는 Format을 사용할 예정이다. 코드 1-9는 지원하는 Surface Format을 하나씩 확인하면서 원하는 Format이 있는지 확인 하는 코드이다. 만약 원하는 Format이 없는 경우 제일 처음 지원하는 Surface Format을 선택한다.

다음은 Presentation Mode를 선택할 차례이다. Presentation은 Swap Chain 생성에 있어서 가장 중요한 부분이라고 한다. 앞에서 설명한 것과 같이 Presentation Mode는 Rendering된 이미지를 Display에 출력하는 방법을 의미한다. Vulkan은 아래와 같이 총 4개의 Presentation Mode를 지원한다. 

  • VK_PRESENT_MODE_IMMEDIATE_KHR: Rendering된(되고있는) Image를 바로 출력한다. Tearing 현상이 발생할 수 있다. 
  • VK_PRESENT_MODE_FIFO_KHR: Rendering된 Image를 Queue에 저장한다. Queue의 제일 앞에 있는 이미지를 Display에 출력하는 방식이다. 만약 Queue가 가득 차는 경우 Application은 Rendering 연산을 수행하지 않는다. 모든 이미지는 Vertical Blanking (수직동기화) 시점에 출력된다.
  • VK_PRESENT_MODE_FIFO_RELAXED_KHR: 앞 옵션과 거의 동일하다. 앞 옵션의 경우 Queue가 비어 있으면 새로운 이미지를 출력하지 않는다. 해당 옵션은 Queue가 비어 있으면 Rendering하고 있는 이미지를 출력한다. 제일 처음 옵션과 동일하게 Tearing 현상이 발생할 수 있다. 
  • VK_PRESENT_MODE_MAILBOX_KHR: 역시 2번째 옵션과 비슷하다. 해당 옵션은 Queue가 Full 일 때 차이가 발생한다. 2번째 옵션에서는 Queue가 꽉 차면 Rendering 연산을 수행하지 않았다. 반면 해당 옵션은 새로운 이미지를 Rendering하고 Queue에서 제일 오래된 Image와 교체한다. Latency 이슈가 줄어든다 (반면 파워 등은 더 소모할 것으로 예상된다).

위 4개의 옵션 중 VK_PRESENT_MODE_FIFO_KHR는 항상 지원한다고 한다. 먼저 VK_PRESENT_MODE_MAILBOX_KHR 지원 여부를 확인하고 만약 해당 옵션을 지원하면 선택하고 그렇지 않으면 VK_PRESENT_MODE_FIFO_KHR을 선택하도록 코드를 작성하였다. 코드 1-10은 Presentation Mode 선택하는 코드이다. 

다음은 Swap Extent를 결정할 차례이다. Swap Extent란 Swap Chain을 구성하고 있는 Image의 크기(Width/Height)를 의미한다. 거의 항상 Window 사이즈(Resolution)와 동일하게 선택하면 된다. 코드 1-11은 Swap Extent를 선택하는 코드이다. 먼저 AndroidGetWindowSize(…) 함수를 사용해서 Android Window 사이즈를 받아온다. AndroidGetWindowSize(…) 함수는 util.cpp에 구현되어 있다. 해당 함수는 현재 Window 사이 값을 Return 해준다. 가능하면 AndroidGetWindowSize(…) 함수에서 받은 사이즈를 사용하도록 코딩하였다. 하지만 가끔 Surface Capability가 Window 사이즈를 지원하지 못하는 경우가 있을 수도 있다고 한다. 그 경우 정해진 Swap Extent 사이즈를 사용해야 한다. 이러한 예외를 처리하기 위해서 코드를 추가로 작성하였다. 

마지막으로 Swap Chain을 구성할 Image 개수를 선택할 차례이다. 코드 1-12는 Image 개수를 선택하는 코드이다. 가장 간단하게 Surface Capability에서 지원하는 최소 Image 개수보다 1개를 더 만들도록 코드를 작성하였다. 혹시나 +1로 인해서 최대 지원 Image 개수를 초과 할 수도 있는 관계로 Max 값을 체크하는 코드도 추가하였다.

Surface Format, Presentation Mode, Swap Extent, Image 개수를 선택하면 드디어 Swap Chain을 생성할 차례이다. 


Creating Swap Chain: Finally~

위에서 선택한 옵션들은 Swap Chain을 생성하는 데 사용된다. 앞장에서 생성한 Instance와 비슷하게 Swap Chain을 생성하는 과정에서 Struct에 필요한 값을 저장해야 한다. Swap Chain을 생성하는 과정에 작성해야 하는 Struct는 아주 많은 변수를 가지고 있다. 먼저 Swap Chain 생성에 필요한 변수를 코드 1-13과 같이 추가한다. 다음은 VkSwapchainCreateInfoKHR Struct에 변수를 저장할 차례이다. VkSwapchainCreateInfoKHR Struct은 아래와 같이 18개의 변수를 가지고 있다.

VkSwapchainCreateInfoKHR Struct (출처 16)

  • sType (VkStructureType): 생성하고자 하는 Struct의 Type을 나타냄
  • pNext (const void*): Extension Struct Pointer
  • flags (VkSwapchainCreateFlagsKHR): 미래에 사용하기 위해서 미리 만들어 두었다고 함
  • surface (VkSurfaceKHR): Image를 보여줄 Window Surface
  • minImageCount (uint32_t): 최소 Presentable Images 개수 (보통 앞에서 선택한 Image 개수 이상)
  • imageFormat (VkFormat): Image Format (앞에서 선택한 Image Format 사용)
  • imageColorSpace (VkColorSpaceKHR): Color Space (앞에서 선택한 Image Color Space 사용)
  • imageExtent (VkExtent2D): Image 사이즈 (앞에서 선택한 Extent 사이즈 사용)
  • imageArrayLayers (uint32_t): 각 Image가 가지고 있는 Layer 개수 (보통 3D Application (아마 왼쪽/오른쪽으로 나뉘어서 나오는 화면을 의미하는 것으로 판단됨)이 아닌 경우 항상 1을 사용)
  • imageUsage (VkImageUsageFlags): Image가 사용되는 용도 (우리가 작성하는 Application의 경우 Rendering 한 결과 값을 Image에 작성하기 때문에 Color Attachment (VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) 값을 사용함)
  • imageSharingMode (VkSharingMode): Image를 Handling 하는 방법
  • queueFamilyIndexCount (uint32_t): Image에 접근할 수 있는 Queue Family의 개수 (imageSharingMode 값이 VK_SHARING_MODE_CONCURRENT로 선택되어 있을 때만 사용)
  • pQueueFamilyIndices (const uint32_t*): Image에 접근할 수 있는 Queue Family ID (imageSharingMode 값이 VK_SHARING_MODE_CONCURRENT로 선택되어 있을 때만 사용)
  • preTransform (VkSurfaceTransformFlagBitsKHR): Image를 표현하는 방향 (예를 들어서 90도 돌려서 표현 할 수 있음)
  • compositeAlpha (VkCompositeAlphaFlagBitsKHR): 다른 Window System과 이미지를 합치는 경우 사용 (대부분의 경우 VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR 옵션을 사용해서 Alpha 값을 무시함)
  • presentMode (VkPresentModeKHR): Present Mode (앞에서 선택한 Presentation Mode 값을 사용)
  • clipped (VkBool32): 보이지 않는 부분에 대한 이미지 부분을 제거 할지 판단함
  • oldSwapchain (VkSwapchainKHR): 새로운 Swap Chain을 생성할 때 예전에 사용하던 Swap Chain 변수를 추가함. 예를 들어서 Window 사이즈가 변경된 경우 Image 사이즈도 변경하면서 Swap Chain을 다시 생성해야 함. 이 경우 Old Swap Chain 변수를 추가함으로 Resource를 재활용 할 수 있음

VkSwapchainCreateInfoKHR Struct에 필요한 값을 모두 저장하였다. 아마 대부분의 변수 값은 코드를 보면 쉽게 이해할 수 있다. 조금 설명이 필요한 부분이 imageSharingMode 부분이다. imageSharingMode은 아래와 같이 2가지 모드 중 선택한다.

imageSharingMode (VkSharingMode)

  • VK_SHARING_MODE_EXCLUSIVE: 각 Queue는 하나의 Image에 대한 소유권(Ownership)을 가진다. 소유권은 Explicitly(명백히?)하게 이전되어야 한다. 가장 좋은 성능을 보인다고 한다. 이 옵션을 선택할 경우 queueFamilyIndexCount와 pQueueFamilyIndices 값을 설정하지 않아도 된다.
  • VK_SHARING_MODE_CONCURRENT: 각 Queue는 Image에 대한 소유권이 없다. 모든 Queue가 Image에 접근(Access)할 수 있다. 이 옵션을 선택할 경우 queueFamilyIndexCount와 pQueueFamilyIndices 값을 설정해야 한다.

이제 vkCreateSwapchainKHR(…) 함수를 사용해서 Swap Chain 생성을 진행한다. vkCreateSwapchainKHR(…) 함수는 아래와 같이 4개의 Parameter를 가진다.

vkCreateSwapchainKHR(…) (출처 17)

  • device (VkDevice): Logical Device 변수 (앞에서 생성한 Logical Device)
  • pCreateInfo (const VkSwapchainCreateInfoKHR*): 앞에서 작성한 Struct 변수
  • pAllocator (const VkAllocationCallbacks*): Pointer to custom allocator callbacks
  • pSwapchain (VkSwapchainKHR*): Swap Chain Pointer (생성된 Swap Chain을 저장할 변수)

성공적으로 Swap Chain이 생성되지 않으면 더 진행하지 않도록 Assert 함수를 추가하였다. Swap Chain의 경우 Destroy 함수를 호출해서 제거해야 한다. 코드 1-14는 vkDestroySwapchainKHR(…) 함수를 사용해서 Destroy 하는 부분이다. 드디어 Swap Chain 생성을 위한 코드를 모두 작성하였다. 하지만, 한가지 작업을 더 수행해야 한다.


Creating Swap Chain: Retrieving the Swap Chain Images (Really Final)

Swap Chain을 생성하면 Image(VkImage)가 같이 생성된다. 해당 Image는 Rendering하는 과정에서 사용된다. 그렇기 때문에 Swap Chain에 연결된 Image를 찾아서 변수에 저장해두는 작업을 수행해야 한다. 코드 1-15와 같이 Image를 저장하기 위한 변수를 하나 추가한다.

다음으로 vkGetSwapchainImagesKHR(…) 함수를 사용해서 생성된 Image를 찾아서 변수에 저장한다. 코드 1-16은 Image를 찾아서 저장하는 코드이다. vkGetSwapchainImagesKHR(…) 함수는 2번 호출된다. 처음에는 Swap Chain에서 생성된 이미지 개수를 파악하기 위해서 사용되고, 다음은 Image(VkImage) Object를 변수에 저장하기 위해서 사용된다.

Image는 Swap Chain이 제거(Destroy)되는 과정에서 같이 제거된다고 한다. 그래서 Destory하는 코드를 추가로 작성할 필요가 없다.


Creating Swap Chain: Result

그림 2: Logcat 결과

코드 1을 빌드해서 실행하면 여전히 어떠한 결과도 출력되지 않는다. 그림 2는 Logcat에 출력되는 결과이다 (Swap Chain 생성 단계에서 출력되는 결과만 보여준다). Swap Chain 생성 부분이 추가된 코드는 출처 18에서 확인 할 수 있다.


출처

  1. https://vulkan-tutorial.com/
  2. https://mkblog.co.kr/2018/07/30/gpus-swap-chain/
  3. https://vulkan.lunarg.com/doc/view/1.0.26.0/linux/tutorial/html/05-init_swapchain.html
  4. https://vulkan.lunarg.com/doc/view/1.0.26.0/linux/tutorial/html/12-init_frame_buffers.html
  5. https://source.android.com/devices/graphics/arch-sh
  6. https://mkblog.co.kr/2018/07/24/gpu-double-buffering-triple-buffering-and-vsync/
  7. https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkGetPhysicalDeviceSurfaceCapabilitiesKHR.html
  8. https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkSurfaceCapabilitiesKHR.html
  9. https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkGetPhysicalDeviceSurfaceFormatsKHR.html
  10. https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkSurfaceFormatKHR.html
  11. https://vulkan.lunarg.com/doc/view/1.0.30.0/linux/vkspec.chunked/ch31s03.html
  12. https://docs.rs/vulkano/0.5.2/vulkano/swapchain/enum.ColorSpace.html
  13. https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkColorSpaceKHR.html
  14. https://en.wikipedia.org/wiki/SRGB
  15. https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkGetPhysicalDeviceSurfacePresentModesKHR.html
  16. https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkSwapchainCreateInfoKHR.html
  17. https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkCreateSwapchainKHR.html
  18. https://github.com/mkblog-cokr/androidVulkanTutorial

Leave a Comment