초기화 과정.
1) Vulkan Loader는 드라이버를 찾아서 로딩한다.
2) Vulkan Loader는 플랫폼의 일관성을 갖기 위해 다른 윈도우 시스템 API를 확장한다.
3) Vulkan Loader는 드라이버의 API 유효성을 검증하기 위한 레이어를 주입하거나 해제한다.
4) Vulkan Instance 생성한다.
5) 물리적 장치에 쿼리를 하여 원하는 큐가 있는지 확인 후 유효한 물리장치를 얻는다.
6) 물리장치에 해당하는 논리 장치를 생성한다.
7) 논리장치에 해당하는 큐를 가져온다.
Vulkan Loader 란
Vulkan loader의 주요한 역할은 Vulkan Driver를 초기화 하고 Vulkan API를 동적으로 로딩을 한다.
Vulkan Loader는 라이브러리이며 안드로이드의 경우 NDK에 포함이 되어있다.
linux, mac의 경우 sdk를 따로 설치해야 한다.
https://vulkan.lunarg.com/doc/sdk/1.3.283.0/windows/LoaderInterfaceArchitecture.html
초기화 과정
1) 번 vulkan loader는 SDK혹은 Android NDK에 설치가 되어 있음.
Vulkan Loader는 사용자 개입없이 Vulkan Application을 만들면 자동으로 동작하여
개발자가 할것이 없습니다.
아래 그림은 Vulkan Loader가 레이어들을 주입하고 Vulkan Driver를 초기화 하고 Vulkan API를
로딩하는 일련의 과정을 보여준다.
2) 다른 WindowSystem API 확장
Window, Linux, Android 각 플랫폼은 각기 다른 WindowSystem을 보유하고 있다.
렌더링을 하기 위해서 각 플랫폼의 Window or Surface 얻기 위한 API를 확장을 해야 한다.
책에서는 확장판이라는 용어를 사용한다.
확장판은 두가지 유형으로 분리가 되는데
첫 번째로 전역 인스턴스 기반 확장판이 있으며,
이 확장판은 어떤 장치와도 독립적인 전역기능으로 VkDevice 없이도 액세스 할수 있다.
두 번째 장치기반 확장판은 장치에 특별히 한정돼 있어
특수 기능을 조작하고 응용 프로그램에 제공하려면 장치에 유효한 핸들이 필요하다.
두 번째 장치기반 확장판은 6)논리장치 생성에서 활성화할 예정입니다.
지금은 WindowSystem API를 얻기 위해 인스턴스 확장판을 활성해 봅시다.
안드로이드의 경우, 렌더링을 위한 Surface를 얻기 위한 2개의 인스턴스 기반 확장판이 필요 합니다.
KHR의 의미는 크로노스를 의미 하며, KHR이 들어간 API의 경우 확장을 해야 사용할수 있습니다.
위 API들은 libvulkan.so에 구현이 되어있으며, ndk 에서 사용할수 있게 구현이 되어있습니다.
- VK_KHR_surface
- VK_KHR_android_surface
두 개의 확장판 api를 얻는 코드는 다음과 같습니다.
코드에서 보면 vkEnumerateInstanceExtensionProperties 함수를 호출을 두번 하는데,
첫번째 호출시에는 properteis의 사이즈를 얻고,
두번 째 호출시에 생성한 vector에 값 복사를 합니다.
이는 자주 쓰이는 방식이니 잘 알아두도록 합니다.
extensionsNames는 vulkan instance 생성할때
VkInstanceCreateInfo 구조체의 ppEnabledExtensionNames로 들어가게 됩니다.
std::vector<const char*> extenstionsNames;
void VkRenderer::CheckInstanceExtension() {
uint32_t extensionCount;
VK_CHECK_ERROR(vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr));
std::vector<VkExtensionProperties> extensions(extensionCount);
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());
aout << "Instance Extensions Properties " << extensions.size() << endl;
for (auto extension : extensions) {
if (extension.extensionName == string("VK_KHR_surface") ||
extension.extensionName == string("VK_KHR_android_surface")) {
extenstionsNames.push_back(extension.extensionName);
}
}
for (auto name : extenstionsNames) aout << name << endl;
}
3) API 유효성을 검증하기 위한 레이어 주입의 대해서 생각해보자
벌칸은 여러개의 Layer 주입 또는 해제를 통하여 개발을 용이하게 할수 있습니다.
유효성 검증 레이어를 통해 API를 검증 할수있는데요.
추가해야할 유효성 검증 레이어의 이름은 "VK_Layer_KHRONOS_Validation" 입니다.
Android의 경우
위에서 소개한 확장판 API는 libvulkan.so 내부 포함이 되어있어서 선택해서 적용할수 있지만,
유효성 검증 레이어는 다운로드를 받아야 합니다.
유효성 검증 레이어를 인스턴스에 주입을 하게되면 에러 발생시 아래와 같은 로그를 볼 수 있습니다.
아래 주소에서 Android 용 validation layer 바이너리를 다운 받을수 있다.
- https://github.com/KhronosGroup/Vulkan-ValidationLayers/releases
안드로이드 프로젝트 뷰로 바꾼 뒤 jniLibs 폴더에 압축을 풀어준다.
전역 기반 인스턴스 레이어 프로퍼티 얻어오기
정상적으로 jniLibs에 등록이 되었다면, 프로퍼티의 레이어 네임이 " VK_Layer_KHRONOS_Validation " 라고 출력 됨.
VkResult vkEnumerateInstanceLayerProperties(
uint32_t* pPropertyCount,
VkLayerProperties* pProperties);
- pPropertyCount : 이 변수는 인스턴스 계층에 있는 레이어의 개수를 나타낸다. 이 변수는 pProperties로 전달되는 값에 따라 입력/출력 변수로 작동한다.
- pProperties : 이 변수는 두 개의 값을 가질 수 있다. 값이 NULL로 지정되면 nPropertyCount에 레이어의 총 개수와 함께 레이어 개수를 반환한다. 배열로 사용되면 같은 배열의 레이어 속성 정보를 반환한다.
전역 기반 인스턴스 확장판 프로퍼티 얻어오기
확장판 프로퍼티 출력해보면 위에서 언급한 서피스 관련 2개의 레이어가 출력된다.
VK_KHR_surface, VK_KHR_android_surface
// Provided by VK_VERSION_1_0
VkResult vkEnumerateInstanceExtensionProperties(
const char* pLayerName,
uint32_t* pPropertyCount,
VkExtensionProperties* pProperties);
- pLayerName : 확장 검색할 레이어 이름.
- pPropertyCount : 쿼리되는 확장 속성 count
- pProperties : VkExtensionProperties 배열의 포인터
구현 코드>>
std::vector<const char*> validationLayers;
bool VkRenderer::CheckValidationLayer() {
uint32_t validationLayerCount;
VK_CHECK_ERROR(vkEnumerateInstanceLayerProperties(&validationLayerCount, nullptr));
std::vector<VkLayerProperties> layers(validationLayerCount);
aout << "CheckValidationLayer count : " << validationLayerCount << endl;
VK_CHECK_ERROR(vkEnumerateInstanceLayerProperties(&validationLayerCount, layers.data()));
aout << "CheckValidationLayer List" << endl;
for (auto& layer : layers) {
aout << layer.layerName << std::endl;
validationLayers.push_back(layer.layerName);
}
uint32_t extensionCount;
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr);
std::vector<VkExtensionProperties> extensions(extensionCount);
vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data());
aout << " Extensions " << endl;
for (auto extension : extensions) {
aout << extension.extensionName << endl;
}
return true;
}
4) Vulkan Instance를 생성한다.
Vulkan Instance를 생성하기 위해서는 vkCreateInstance 함수를 실행한다.
Instance를 생성할때 VkApplicationInfo, VkInstanceCreateInfo 두개의 구조체가 필요하다.
마지막 인자에 생성된 인스턴스를 전달 받는다.
vkInstanceCreateInfo 구조체에 2,3) 단계에서 열거한 레이어와 확장판을 설정해 주자.
2) 인스턴스 레이어 설정.
uint32_t enabledLayerCount;
const char* const* ppEnabledLayerNames;
3) 인스턴스 확장판 설정
uint32_t enabledExtensionCount;
const char* const* ppEnabledExtensionNames;
typedef struct VkApplicationInfo {
VkStructureType sType;
const void* pNext;
const char* pApplicationName;
uint32_t applicationVersion;
const char* pEngineName;
uint32_t engineVersion;
uint32_t apiVersion;
} VkApplicationInfo;
- sType : VK_STRUCTURE_TYPE_APPLICATION_INFO
- pNext : 이 필드는 확장판 지정 구조체의 정확한 포인터 지정 or NULL
- pApplicationName : 이 필드는 "Hello World"처럼 사용자가 지정한 응용 프로그램의 이름이다.
- applicationVersion : 개발자의 응용 프로그램의 버전을 나타내는 데 사용한다. 이 정보는 응용 프로그램 자체가 자신의 버전을 확인할때 유용하게 활용할 수 있다.
- engineName : 응용 프로그램에서 사용되는 백엔드 엔진의 이름을 지정한다.
- engineVersion : 이 필드가 사용되면 응용 프로그램에서 사용하는 백엔드 엔진의 버전을 지정한다. 이 필드가 사용되지 않는다면 응용 프로그램 버전으로 충분하다.
- apiVersion : 이 필드는 Vulkan API의 버전 번호를 지정한다. 이를 프로그램 실행에 사용한다. 드라이버는 이 값을 읽어 사용 가능한지 아닌지를 검증한다. 0으로 지정되면 이 값을 무시하고, 0이 아닌 값으로 지정된 경우에만 사용한다.
지정한 버전이 지원되지 않는다면 오류를 보고하고, 오류가 있는 경우 VK_ERROR_INCOMPATIBLE_DRIVER를 반환한다.
typedef struct VkInstanceCreateInfo {
VkStructureType sType;
const void* pNext;
VkInstanceCreateFlags flags;
const VkApplicationInfo* pApplicationInfo;
uint32_t enabledLayerCount;
const char* const* ppEnabledLayerNames;
uint32_t enabledExtensionCount;
const char* const* ppEnabledExtensionNames;
} VkInstanceCreateInfo;
- sType : VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO
- pNext : 이 필드는 확장판 지정 구조체에 대한 유효한 포인터를 지정하거나 NULL
- flags : 현재 사용 안함
- pApplicationInfo : VkApplicationInfo를 가리키는 개체 포인터로, 응용 프로그램 지정 정보를 갖고있다.
여기에는 Vulkan API 버전, 이름, 엔진 버전 등이 포함된다. 상세한 정보는 추후 설명할 VkApplicationInfo를 참조하기 바란다. 이 필드는 NULL이 될 수 있다.
- enabledLayerCount : 이 필드는 인스턴스 레벨에서 활성화할 레이어수를 지정한다.
- ppEnabledLayerNames : 이 필드는 배열 형태로 레이어 이름의 목록을 갖고 있으며, 인스턴스 계층에서 활성화되야 한다.
- enabledExtensionCount : 인스턴스 계층에서 활성화된 확장판 수를 지정한다.
- ppEnabledExtensionNames : 이 필드는 인스턴스 계층에서 활성화 된 확장판 이름들의 목록을 배열 형태로 갖고 있다.
Vulkan Instance를 생성하는 코드는 다음과 같다.
구현 코드>>
void VkRenderer::CreateInstance() {
aout << "CreateInstance" << endl;
VkApplicationInfo appInfo = {};
appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO;
appInfo.pApplicationName = "Vulkan App";
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_3;
VkInstanceCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
createInfo.pApplicationInfo = &appInfo;
createInfo.enabledLayerCount = validationLayers.size();
createInfo.ppEnabledLayerNames = validationLayers.data();
createInfo.enabledExtensionCount = extenstionsNames.size();
createInfo.ppEnabledExtensionNames = extenstionsNames.data();
vkCreateInstance(&createInfo, nullptr, &instance);
}
5) 물리적 장치들에게 쿼리를 하여 원하는 큐 유형이 있는지 확인 후 유효한 물리장치를 얻는다.
큐는 응용프로그램과 물리적 장치가 통신하는 수단입니다.
응용 프로그램은 커맨드 버퍼를 큐에 제출하는 형식으로 원하는 작업을 시키고,
이 작업을 물리적으로 읽어 들여 비동기적으로 처리가 됩니다.
큐의 속성은 4가지가 있습니다. (VkQueueFlagBits 참고)
- 그래픽스 큐, 컴퓨팅 큐, 전송 큐, 희소 큐
물리 장치에 쿼리를 하면 큐 패밀리의 속성값을 알 수 있습니다.
큐 패밀리는 물리장치가 지원하는 큐의 속성의 종류에 따라 그룹화 시켜놓은 집합이다.
큐를 만들기 위해서는 물리 장치가 지원하는 큐 패밀리에서
원하는 큐 유형 (Graphics Vk queue) 의 큐 패밀리 인덱스를 저장을 합니다.
- graphic 속성이 있어서 그래픽 큐 핸들이라고도 표현한다.
그래픽 큐 핸들을 input으로 논리장치를 생성하면 묵시적으로 큐가 생성이 됩니다.
큐 패밀리의 속성은 vkGetPhysicalDeviceQueueFamilyProperties()를 통해서 알 수 있습니다.
큐 패밀리는 해당하는 큐의 개수(queueCount)만큼 큐를 지원 합니다.
모바일의 경우 대부분 1개의 물리적장치와 1개의 큐 패밀리, 1개의 큐를 가지고 있습니다.
정리하면 다음과 같습니다.
1) 물리장치(gpu)를 열거합니다.
2) 해당 물리장치에 대해서 큐 패밀리 배열을 가져옵니다.
// Provided by VK_VERSION_1_0
void vkGetPhysicalDeviceQueueFamilyProperties(
VkPhysicalDevice physicalDevice,
uint32_t* pQueueFamilyPropertyCount,
VkQueueFamilyProperties* pQueueFamilyProperties);
- physicalDevice : 큐 속성들이 검색될 물리적 장치의 핸들이다.
- pQueueFamilyPropertyCount : 이 장치에 의해 공개된 큐 패밀리의 개수를 참조한다.
- pQueueFamilyProperties : 큐 패밀리의 개수 같은 크기의 배열로 된 이 필드에 큐 패밀리 속성을 저장한다.
3) 큐 패밀리 배열을 순회하면서 큐 패밀리의 queueFlag 값이 VK_QUEUE_GRAPHIC_BIT이면 물리장치를 메인으로 선택합니다.
큐 패밀리 인덱스는 큐 패밀리 배열의 index 입니다.
4) 선택된 물리장치의 속성값을 출력해 봅니다.
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceProperties(
VkPhysicalDevice physicalDevice,
VkPhysicalDeviceProperties* pProperties);
typedef struct VkPhysicalDeviceProperties {
uint32_t apiVersion;
uint32_t driverVersion;
uint32_t vendorID;
uint32_t deviceID;
VkPhysicalDeviceType deviceType;
char deviceName[VK_MAX_PHYSICAL_DEVICE_NAME_SIZE];
uint8_t pipelineCacheUUID[VK_UUID_SIZE];
VkPhysicalDeviceLimits limits;
VkPhysicalDeviceSparseProperties sparseProperties;
} VkPhysicalDeviceProperties;
VKAPI_ATTR void VKAPI_CALL vkGetPhysicalDeviceMemoryProperties(
VkPhysicalDevice physicalDevice,
VkPhysicalDeviceMemoryProperties* pMemoryProperties);
구현 코드 >>
void VkRenderer::GetPhysicalDevice() {
aout << "GetPhysicalDevice Enter " << endl;
physicalDevice = nullptr;
uint32_t deviceCount = 0;
vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr);
std::vector<VkPhysicalDevice> deviceList(deviceCount);
vkEnumeratePhysicalDevices(instance, &deviceCount, deviceList.data());
// rendering 가능한 physical device selection
for (const auto& device : deviceList) {
QueueFamilyIndices indices = GetQueueFamilies(device);
if (indices.isValid()) {
aout << "physicalDevice selected" << endl;
physicalDevice = device;
break;
}
}
if (physicalDevice != nullptr) {
VkPhysicalDeviceProperties deviceProperties;
vkGetPhysicalDeviceProperties(physicalDevice, &deviceProperties);
aout << "device ID : " << deviceProperties.deviceID << endl;
aout << "device Name : " << deviceProperties.deviceName << endl;
aout << "device Type : " << deviceProperties.deviceType << endl;
aout << "device Version : " << deviceProperties.driverVersion << endl;
}
else {
aout << "device is nothing..." << endl;
}
}
QueueFamilyIndices VkRenderer::GetQueueFamilies(VkPhysicalDevice device)
{
QueueFamilyIndices indices;
// Get all Queue Family Property info for the given device
uint32_t queueFamilyCount = 0;
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr);
std::vector<VkQueueFamilyProperties> queueFamilyList(queueFamilyCount);
vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilyList.data());
int i = 0;
for (const auto& queueFamily : queueFamilyList)
{
aout << "queue family count : " << queueFamilyCount << " queueCount : " << queueFamily.queueCount << endl;
if (queueFamily.queueCount > 0 && queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT)
{
indices.graphicsFamily = i;
}
if (indices.isValid()) break;
i++;
}
aout << " queue family : " << i << endl;
return indices;
}
6) 물리장치에 맵핑되는 논리 장치를 생성한다.
논리장치는 VkDevice로 표현하고, vkDeviceCreateInfo 구조체를 사용한다.
vkDeviceCreateInfo 내부에 큐를 생성하는 정보 vkDeviceQueueCreateInfo가 추가로 필요하다.
vkDeviceQueueCreateInfo 내부에서 유효한 큐 패밀리 인덱스가 필요하다.
논리장치 생성하기 위해 지원되는 큐 패밀리 인덱스가 필요하다는 의미이다.
위 5) 과정에서 쿼리된 모든 패밀리의 속성을 반복해서 queueFlags 비트 정보를 확인해서 큐 패밀리 인덱스를 구했다.
큐 패밀리 인덱스를 큐 VkDeviceQueueCreateInfo
멤버 변수인 queueFamilyIndex에 넣어주어야 한다.
논리적 장치 개체가 생성 될때, 큐 패밀리 인덱스에 해당하는 큐가 묵시적으로 생성이 된다.
물리 디바이스에서 확장 properties를 구한다.
논리 장치 큐 생성 메타 데이터
암묵적으로 큐를 생성하기 위한 메타 데이터
논리장치 생성 메타 데이터
스왑 체인 API도 플랫폼에 의존적이기 때문에,
벌칸에서 제공되지 않고 확장판을 이용하여 라이브러리를 등록해야한다.
위에서 벌칸 인스턴스 생성시, 전역 기반 확장판의 경우는 서피스 관련 2개를 등록했었다.
장치 인스턴스 생성시, 장치 기반 확장판을 확장해야 하는데,
이는 스왑체인을 생성할때 필요하다.
다음 챕터에서 물리 장치에서 확장할수 있는 API을 나열하고, 중 "VK_KHR_swapchain" 장치를 확장한다.
지금은 pp
논리장치를 생성 함수는 vkCreateDevice() 이다.
Usage >>
void VkRenderer::CreateLogicalDevice()
{
aout << "CreateLogicalDevice " << endl;
QueueFamilyIndices indices = GetQueueFamilies(physicalDevice);
std::vector<VkDeviceQueueCreateInfo> queueCreateInfos;
std::set<int> queueFamilyIndices = { indices.graphicsFamily };
for (int queueFamilyIndex : queueFamilyIndices)
{
VkDeviceQueueCreateInfo queueCreateInfo = {};
queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO;
queueCreateInfo.queueFamilyIndex = queueFamilyIndex;
queueCreateInfo.queueCount = 1;
float priority = 1.0f;
queueCreateInfo.pQueuePriorities = &priority;
queueCreateInfos.push_back(queueCreateInfo);
}
VkDeviceCreateInfo deviceCreateInfo = {};
deviceCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
deviceCreateInfo.queueCreateInfoCount = static_cast<uint32_t>(queueCreateInfos.size());
deviceCreateInfo.pQueueCreateInfos = queueCreateInfos.data();
VkPhysicalDeviceFeatures deviceFeatures = {};
deviceCreateInfo.pEnabledFeatures = &deviceFeatures;
deviceCreateInfo.enabledExtensionCount = 0;
deviceCreateInfo.ppEnabledExtensionNames = nullptr;
VkResult result = vkCreateDevice(physicalDevice, &deviceCreateInfo, nullptr, &logicalDevice);
if (result != VK_SUCCESS)
{
throw std::runtime_error("Failed to create a Logical Device!");
}
vkGetDeviceQueue(logicalDevice, indices.graphicsFamily, 0, &graphicQueue);
}
7) 논리장치에 해당하는 큐를 가져온다.
6)에서 생성된 논리장치를 파라메터로 생성된 큐를 가져온다.
queueFamilyIndex : 큐 패밀리에 다수의 큐가 있을 수 있다. 이때 각각의 큐는 유일한 인덱스 값으로 구분된다.
이 필드는 큐 패밀리 내에 있는 큐의 인덱스를 가리킨다.
vkGetDeviceQueue(logicalDevice, indices.graphicsFamily, 0, &graphicQueue);
Reference
https://vulkan.lunarg.com/doc/sdk/1.3.283.0/windows/khronos_validation_layer.html
'Vulkan' 카테고리의 다른 글
5. Presentation And Synchronize ( Fence, Semaphore, Event ) (0) | 2024.06.27 |
---|---|
4. Pipeline Barrier And Image Layout (0) | 2024.06.24 |
3. Command buffer (0) | 2024.06.21 |
2. Surface And SwapChain (1) | 2024.06.11 |
0. Vulkan을 시작하면서..... (0) | 2024.06.07 |