[Vulkan Tutorial] 08-Search and Select Queue Families

목차: 01-Overview (Link)
이전 글: 07-Select Physical Device (Link)
다음 글: 09-Create Logical Device (Link)


Queue Family?

MK: 다른 글도 문제가 많을 수 있지만, 특히 Queue Family에 대한 글은 문제가 더 많을 수 있다. 정확히 Queue Family에 대한 설명만을 작성한 곳을 찾지 못하여서 여러 곳에서 작성한 내용을 조합해서 정리해보았다.

그림 1: Command Buffer, Queue, Queue Family 그림

Queue Family를 설명하려면 Command, Command Buffer, Queue, Queue Family 순서대로 설명을 해야 할 것 같다. 그림 1은 Vulkan에서 사용하는 Command, Command Buffer, Queue, Queue Family에 대해서 정리한 그림이다.

먼저 Command에 대해서 알아보자. Vulkan에서는 크게 아래와 같이 4가지 종료의 Command가 존재한다 (출처 3).

  1. Graphics Command: vkCmdDraw* Function과 같이 이미지를 Rendering 하는데 필요한 연산을 수행
  2. Compute Command: vkCmdDispatch* Function과 같이 Compute 연산을 수행
  3. Transfer Command: vkCmdCopy* Function과 같이 메모리 Transfer에 관련된 연산을 수행
  4. Sparse Binding Command: (vkQueueBindSparse) binding of sparse resources to memory

MK: vkQueueBindSparse는 정확히 어떤 용도인지 모르겠다. 현재 진행하고 있는 Tutorial에 큰 관계가 없는 것 같아서 대충 작성하고 넘어갈 예정이다.

Vulkan에서는 위 Command를 실행하기 위해서 먼저 Command Buffer에 Command를 저장한다. 나중에 Command Buffer를 생성하는 코드 역시 작성해야 한다. Command가 저장된 Command Buffer는 다시 Queue를 통해서 GPU로 전달된다. Graphics, Compute, Transfer Command의 경우 Command Buffer에 저장한 후에 Queue로 전달된다. 하지만, Sparse Binding Command의 경우 Command Buffer에 저장하지 않고 바로 Queue로 전달된다고 한다 (출처 3).

모든 Queue가 Graphics, Compute, Transfer, Sparse Binding Command를 모두 전달받을 수 있는 것은 아니다. 예를 들어서 특정 Queue의 경우 Transfer Command만 저장해서 GPU로 보낼 수 있다. 반대로 모든 Queue가 위 4가지 Command를 모두 받아서 GPU로 보낼 수도 있다.

Queue Family는 동일한 Property(특성)를 가진 Queue의 집합을 의미한다. 그림 1의 경우 5개의 Queue가 Graphics, Compute, Transfer 관련 Command를 받을 수 있다. 이렇게 동일한 Property를 가진 5개를 하나의 Queue Family로 정의한다. 추가로 그림 1에서 Sparse Binding Command를 받을 수 있는 Queue가 1개 존재하고, 해당 Queue는 단독으로 Queue Family라고 정의할 수 있다.

그림 2: Android 단말에서 지원하는 Queue

그림 2는 현재 실험을 진행하고 있는 Android 단말의 Queue Family 정보를 보여준다. 해당 Android 단말은 총 1개의 Queue Family가 존재하고, 해당 Queue Family는 2개의 Queue로 구성되어 있다. 2개의 Queue는 Graphics, Compute, Transfer Command를 전달받을 수 있다. 추가로 해당 Queue Family는 Presentation을 지원한다고 되어있다.

MK: Presentation은 아마도 Rendering 된 이미지를 Display 할 때 사용되는 것으로 추측된다. Vulkan API를 사용해서 이미지를 Rendering 하는 경우 Presentation Queue도 생성해야 한다. 출처 1 Surface Create 과정에서 Presentation Queue 생성에 대한 설명이 같이 나온다. Compute, Transfer, Graphics, Sparse Binding과 달리 Presentation Queue에 대한 설명은 별로 존재하지 않는 것 같다. 관련된 정보를 찾지 못하였다. 아마 다른 Queue와 비슷한 의미일 것으로 생각한다. 결과적으로 Presentation Queue의 경우 Rendering이 완료된 이미지를 Display 하는 과정에서 필요한 Command를 전달하는 Queue가 아닐까 추측해본다.


Search and Select Queue Families

대략 Queue, Queue Family에 대해서 알게 되었으니 Queue Family를 검색하는 코드를 작성할 차례이다. 아래 코드 1은 Queue Family를 탐색하여서 적합한 Queue Family를 선택하는 코드이다.

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

#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()");
			initVulkan();
			mainLoop();
			cleanup();
		}

	private:
		void initVulkan(){
			createInstance();
			createSurface();
			pickPhysicalDevice();
			//MK: (코드 1-2) Graphic, Present Queue Index를 찾기 위한 함수 호출
			searchQueue();
		}

		void mainLoop(){
		}

		void cleanup(){
			vkDestroySurfaceKHR(instance, surface, NULL);
			vkDestroyInstance(instance, NULL);
		}

		void createInstance(){

			LOGI("MK: Create VkApplicationInfo");
			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, nullptr);
			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);
		}

		//MK: (코드 1-1) Graphic, Present Queue를 찾기 위한 함수
		void searchQueue(){
			LOGI("MK: searchQueue Function");

			//MK: (코드 1-4) GPU가 가지고 있는 모든 Framily Queue 정보를 파악하는 코드
			uint32_t queueCount = 0;
			vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueCount, nullptr);
			assert(queueCount > 0);
			std::vector<VkQueueFamilyProperties> queueFamilies(queueCount);
			vkGetPhysicalDeviceQueueFamilyProperties(physicalDevice, &queueCount, queueFamilies.data());

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

			//MK: (코드 1-5) Graphics Queue와 Present를 Support하는 Queue Family를 찾는 코드
			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)");
		}

		VkInstance instance;

		VkSurfaceKHR surface;
		PFN_vkCreateAndroidSurfaceKHR fpCreateAndroidSurfaceKHR;

		VkPhysicalDevice physicalDevice = VK_NULL_HANDLE;

		//MK: (코드 1-3) graphic, present queue index를 저장하기 위한 변수
		uint32_t graphicQueueFamilyIndex = 0;
		uint32_t presentQueueFamilyIndex = 0;
};

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과 같이 searchQueue(…) 함수를 추가한다. 해당 함수는 코드 1-2 initVulkan(…) 함수에서 호출한다. 추가로 Graphics Queue Family, Presentation Queue Family index를 저장하기 위한 변수를 코드 1-3과 같이 추가한다.

코드 1-4는 vkGetPhysicalDeviceQueueFamilyProperties(…) 함수를 사용해서 GPU가 가지고 있는 모든 Family Queue 정보를 Loading 한다. 코드 1-4에서 vkGetPhysicalDeviceQueueFamilyProperties(…) 함수를 2번 호출한다. 처음 호출은 Queue Family 개수를 파악하기 위해서 사용하고, 2번째 호출은 Queue Family 정보를 값을 Loading 하기 위해서 사용한다. vkGetPhysicalDeviceQueueFamilyProperties(…) 함수는 아래와 같이 총 3개 Parameter (인자)를 가진다.

vkGetPhysicalDeviceQueueFamilyProperties(…) (출처 4)

  • physicalDevice (VkPhysicalDevice): Physical Device 변수
  • pQueueFamilyPropertyCount (uint32_t): 총 Queue Family 개수를 저장하기 위한 변수
  • pQueueFamilyProperties (VkQueueFamilyProperties): Queue Family 정보를 저장하기 위한 변수

다음은 Graphics를 지원하는 Queue Family와 Presentation을 지원하는 Queue Family를 찾을 차례이다. 코드 1-5는 For Loop을 사용해서 적당한 Queue Family를 찾는 코드이다. 가장 먼저 Graphics, Presentation을 지원하는 Family Queue를 선택한다. 만약 적당한 Queue Family가 없으면 assert(…) 함수를 사용해서 코드를 종료한다.

그림 3: Queue Family 탐색하는 코드 결과

코드 1을 컴파일 해서 실행하면 역시나 검은색 화면만 나온다. 그림 3은 해당 빌드를 실행하면 출력되는 Logcat 결과이다. 그림 2와 동일하게 총 1개의 Queue Family가 존재하고, 1개의 Queue Family는 Graphics Command를 받을 수 있는 Queue를 2개 가지고 있으며, Presentation 기능을 지원한다. 코드는 출처 5에서 확인할 수 있다.


출처

  1. https://vulkan-tutorial.com/
  2. https://lifeisforu.tistory.com/404
  3. https://stackoverflow.com/questions/55272626/what-is-actually-a-queue-family-in-vulkan
  4. https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkGetPhysicalDeviceQueueFamilyProperties.html
  5. https://github.com/mkblog-cokr/androidVulkanTutorial

Leave a Comment