[Vulkan Tutorial] 06-Create Window Surface

목차: 01-Overview (Link)
이전 글: 05-Create an Instance (Link)
다음 글: 07-Select Physical Device (Link)


Window Surface?

출처 1 Vulkan Tutorial에서 Window Surface 생성하는 부분이 Physical Device 선택 등의 설명 뒤에 나온다. 저의 경우 Window Surface 생성에 대한 설명을 먼저 작성하였다. 코드 작성 순서에서 Window Surface 생성이 Physical Device를 선택 전에 작성해야 하기 때문이다. 출처 1에서는 Window Surface 생성과 함께 Queue Family (Graphic Queue, Present Queue)에 대한 설명을 같이하기 위해서 뒤에 설명한다. 개인적으로 Queue Family 설명은 따로 작성할 계획이다.

앞에서 여러 번 설명한 것처럼 Vulkan은 Platform Agnostic API (여러 OS에서 사용 가능하다는 의미)이다. 그래서 Vulkan API가 Rendering 한 이미지를 Window System에 바로 출력할 수 없다 (직접 제어가 불가능하다는 의미인 것으로 판단됨). Vulkan API로 Rendering 한 이미지를 화면에 출력하기 위해서 Window System Integration (WSI) Extension (VK_KHR_surface)을 사용해야 한다.

MK: 사실 Window System과 API의 정확한 연결 관계를 이해하지는 못했다. 출처 2 위키피디아에 Window (Windowing) System에 대한 설명이 잘 작성되어 있다. Window System이란 우리가 사용하는 창 화면을 관리하는 프로그램이다. 우리가 크롬 등을 클릭하면 Window 창이 나타나는데 이러한 Window 창을 관리하는 프로그램 이란 의미이다. Graphic API는 이미지를 생성하고 Window System이 해당 이미지를 우리가 보는 화면으로 출력해주는 거라고 이해하면 될 것 같다. 위에서 설명한 것처럼 Vulkan은 다양한 OS에서 사용이 가능하지만, Window System과 직접 통신을 할 수 없기 때문에 Extension을 사용해서 이미지를 출력하는 것이다.

Window, Linux 등에서 Vulkan API로 프로그램을 작성하는 경우 GLFW (Graphics Library FrameWork) 등을 사용해서 Window System을 생성(Open)해야 한다. Android의 경우 NDK에서 자동(?)으로 Window System을 생성하는 것으로 판단된다. NDK를 사용하면 Window System이 언제 생성되는지 궁금해서 Android NDK 앱 실행 순서를 조금 찾아보았다.

그림 1: Native Android APP 실행 순서 (출처 3)

출처 3에 NDK로 작성된 Native Android APP (네이티브 앱)의 실행 순서에 대해서 설명되어 있다. Native Android APP의 동작 순서가 궁금한 경우 아래 출처 3의 내용을 확인하면 된다. 그림 1은 출처 3에 있는 그림을 저의 스타일로 조금(?) 수정한 그림이다. 그림 1과 같이 Native APP이 시작되면 아래의 순서대로 함수가 호출된다.

Native Android APP 함수 호출 순서 (출처 3)

  1. ANativeActivity_onCreate(…): android_native_app_glue.c 파일
  2. android_app_create(…): android_native_app_glue.c 파일
  3. android_app_entry(…): android_native_app_glue.c 파일
  4. android_main(…): util.cpp 파일
  5. Android_handle_cmd(…): util.cpp 파일
  6. sample_main(…): mkVulkanExample.cpp 파일
  7. 추가적인 부분은 출처 3 참조
그림 2: Android Window 생성 코드 (추측)

현재 우리가 작성하고 있는 코드는 android_main(…)에서 호출되는 함수에 코드를 작성하고 있다. 그림 2는 ANativeActivity_onCreate(…) 함수 코드이다. 빨간색으로 표시한 부분이 Window System을 처음으로 생성하는 부분으로 판단되는 코드 부분이다. 위 함수 코드는 android_native_app_glue.c 파일에 작성되어 있다. 우리는 android_native_app_glue.c 파일에서 제공하는 함수를 사용해서 코드를 작성하기 때문에 Window System이 자동(?)으로 생성된다. 결과적으로 Android에서 Vulkan 코드를 작성하는 경우 Window, Linux 등에서 GLFW를 사용해서 Window System을 생성(Open)하는 코드를 따고 작성할 필요가 없다. 출처 1의 Tutorial의 경우 Linux, Window 환경에서 작성하는 코드인 관계로 Instance 생성 과정에 GFLW 관련 코드가 포함되어 있다. 저의 경우 해당 코드 설명을 Instance 생성 부분에서 따로 작성하지 않았다.

VkSurfaceKHR은 Abstract Type of Surface(번역을 못 하겠음)로 Rendering 한 이미지를 출력(Present)하기 위해서 사용한다. 아마도 Window Surface는 Window System에 출력되는 이미지를 생성하는 공간이란 의미인 것 같다. 정확한 해석을 못하여서 아래 Surface에 대한 영어 원문을 출처 1에서 가져왔다.

  • “The surface in our program will be backed by the window that we’ve already opened with GLFW. (출처 1)”

만약 Off-Screen Rendering을 하고 싶은 경우 Window Surface 생성을 하지 않아도 된다. 아마 Window System 역시 생성(Open)하지 않아도 될 것 같다. OpenGL의 경우 Vulkan과 달리 Off-Screen Rendering을 하기 위해서는 Invisible Window를 꼭 생성해야 한다고 한다. 

MK: Off-Screen Rendering은 보통 GPU의 최대 성능을 측정하기 위해서 사용되는 것으로 알고 있다. Anandtech 등에서는 새로운 모바일 기기가 출시되면 Benchmark Off-Screen 점수를 측정하여 이전 세대 단말 또는 타사 단말들과의 성능 비교를 한다. 


Creating Window Surface

드디어 Window Surface를 생성할 차례이다. 아래 코드 1은 Window Surface를 생성하는 코드이다.

코드 1 : Surface 생성 코드 추가

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

#define APP_SHORT_NAME "mkVulkanExample"

//MK: (코드 1-4) vk 함수 Function Pointer (FP)를 찾는 코드
#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();
			//MK: Surface 생성 함수 호출
			createSurface();
		}

		void mainLoop(){
		}

		void cleanup(){
			//MK: (코드 1-5) Surface Destroy 코드 
			vkDestroySurfaceKHR(instance, surface, NULL);                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      
			vkDestroyInstance(instance, nullptr);
		}

		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(nullptr, &extensionCount, nullptr);
			std::vector<VkExtensionProperties> extensions(extensionCount);
			vkEnumerateInstanceExtensionProperties(nullptr, &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, nullptr);
			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, nullptr, &instance);
			
			assert(result == VK_SUCCESS);
			LOGI("MK: Successfully Create Instance (If not, should not see this print)");

		}

		//MK: (코드 1-2) createSurface 함수 추가
		void createSurface(){
			
			//MK: VkAndroidSurfaceCreateInfoKHR Struct 작성
			VkAndroidSurfaceCreateInfoKHR createInfo = {};
			createInfo.sType = VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR;
			createInfo.pNext = nullptr;
			createInfo.flags = 0;
			createInfo.window = AndroidGetApplicationWindow();
			
			//MK: vkCreateAndroidSurfaceKHR 함수 포인터 검색
			MK_GET_INSTANCE_PROC_ADDR(instance, CreateAndroidSurfaceKHR);
			
			//MK: (코드 1-3) Surface 생성
			VkResult result = fpCreateAndroidSurfaceKHR(instance, &createInfo, nullptr, &surface);

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

		VkInstance instance;

		//MK: (코드 1-1) VkSurfaceKHR 변수 추가
		VkSurfaceKHR surface;
   		PFN_vkCreateAndroidSurfaceKHR fpCreateAndroidSurfaceKHR;
};

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처럼 VkSurfaceKHR 변수를 하나 추가한다. 다음은 createSurface(…) 함수를 생성하여서 Surface를 생성하는 코드를 작성한다. 코드 1-2는 createSurface(…) 함수 코드이다. Instance 생성과 비슷하게 먼저 VkAndroidSurfaceCreateInfoKHR Struct에 필요한 정보를 추가한다. VkAndroidSurfaceCreateInfoKHR Struct은 아래와 같이 총 4개 변수를 가지고 있다. 

VkAndroidSurfaceCreateInfoKHR (출처 4)

  • sType (VkStructureType): 생성하고자 하는 Struct의 Type을 나타냄
  • pNext (const void *): Extension Struct Pointer
  • flags (VkAndroidSurfaceCreateFlagsKHR): 미래에 사용하기 위해서 미리 만들어 두었다고 함
  • window (struct ANativeWindow *): ANativeWindow 포인터 (현재 작성하고 있는 코드의 경우 AndroidGetApplicationWindow(…) 함수를 사용해서 ANativeWindow 포인터를 전달 받음. util_init.cpp 파일 참조)

Struct에 필요한 정보를 모두 추가한 후에 vkCreateAndroidSurfaceKHR(…) 함수를 사용해서 Surface를 생성하면 된다. vkCreateAndroidSurfaceKHR(…)는 총 4개의 인자(Parameter)를 가진다. 

vkCreateAndroidSurfaceKHR(…) (출처 5)

  • instance (VkInstance): Instance 변수 (앞 장에서 생성함)
  • pCreateInfo (const VkAndroidSurfaceCreateInfoKHR *): 앞에서 작성한 VkAndroidSurfaceCreateInfoKHR Struct 변수
  • pAllocator(const VkAllocationCallbacks*): Pointer to custom allocator callbacks
  • pSurface (VkSurfaceKHR*): VkSurfaceKHR 변수 포인터

코드 1-3에 vkCreateAndroidSurfaceKHR(…) 함수를 사용해서 Surface를 생성하는 부분이다. 해당 코드에서 vkCreateAndroidSurfaceKHR(…) 함수를 아닌 fpCreateAndroidSurfaceKHR(…) 함수를 사용하고 있다. fpCreateAndroidSurfaceKHR(…) 함수는 vkCreateAndroidSurfaceKHR(…)의 Function Pointer (함수 포인터)를 의미한다. Function Pointer을 가져오기 위해서 코드 1-4를 추가하였다.

MK: vkGetInstanceProcAddr(…) 함수는 Vulkan Loader(출처 7)와 관련이 있는 코드이다. 정확히 Vulkan Loader의 동작원리를 이해하지 못해서 상세 설명은 작성하지 못하였다. 해당 부분은 출처 6의 예제 코드를 보고 따라서 작성한 부분이다. 출처 8 설명에 따르면 vkGetInstanceProcAddr(…) 함수는 Platform-Specific API를 찾을 때 사용한다고 한다. 아마도 vkCreateAndroidSurfaceKHR(…) 함수는 Android Specific API인 관계로 해당 vkGetInstanceProcAddr(…) 함수를 사용해서 Function Pointer을 찾아야 하는 것 같다. 

vkGetInstanceProcAddr(…) (출처 8) 

  • instance (VkInstance): 앞에서 생성한 Instance 변수. Instance와 Dependency가 없는 경우 Null 값을 사용
  • pName (const char* ): Function Name (the name of the command to obtain)

코드 1-3과 같이 Surface를 생성하고 문제가 없으면 VK_SUCCESS 값을 Ruturn한다. 마지막으로 코드 1-5는 Surface를 제거(Destroy)하는 코드이다. Instance 제거 이전에 Surface를 먼저 제거해야 한다. 

그림 3: Logcat 결과 화면

위 코드를 빌드해서 안드로이드 단말에서 실행하면 여전히 검은색 화면만 출력된다. 위 그림 3은 해당 빌드를 실행하면 Logcat에 출력되는 결과 화면이다. 정상적으로 Surface가 생성되었다는 Log만 출력한다. 출처 9에서 Surface 생성 코드를 확인 할 수 있다.

앞에서 언급했던 것과 같이 출처 1의 Surface Create 관련 부분은 훨씬 많은 내용을 포함하고 있다. Surface 생성하는 과정에 Present Queue에 대한 설명을 같이하고 있다. Present Queue에 대해서 같이 작성하려고 고민했는데 Queue Family는 상대적으로 중요한 내용인 듯하여서 따로 정리할 계획이다.


출처

  1. https://vulkan-tutorial.com/
  2. https://en.wikipedia.org/wiki/Windowing_system
  3. http://www.tipssoft.com/bulletin/board.php?bo_table=FAQ&wr_id=1876
  4. https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/VkAndroidSurfaceCreateInfoKHR.html
  5. https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkCreateAndroidSurfaceKHR.html
  6. https://developer.android.com/ndk/guides/graphics/getting-started
  7. https://vulkan.lunarg.com/doc/view/1.0.69.0/mac/loader_and_layer_interface.html
  8. https://www.khronos.org/registry/vulkan/specs/1.2-extensions/man/html/vkGetInstanceProcAddr.html
  9. https://github.com/mkblog-cokr/androidVulkanTutorial

Leave a Comment