Vulkan 튜토리얼: 디바이스와 큐 생성하기
목표 설정
앞선 튜토리얼을 끝냈다면 이제 본격적으로 이 튜토리얼, 그 중에서도 소주제의 목표를 설정하겠습니다. 우리는 처음부터 그래픽스 렌더링을 위해 창을 띄우고, 삼각형을 그리는 Hello Triangle을 만들지 않을 것입니다. 그 이유는 다음과 같습니다:
- Vulkan 창을 띄우는 것은 굉장히 복잡합니다. 우리는 크로스플랫폼에서 창을 띄우기 위한 GLFW 라이브러리를 사용할 것이지만, 이 라이브러리는 OpenGL을 사용하는 전제 하에 만들어졌기에 추가적인 Window System Integration (WSI)라는 과정을 거쳐야 합니다.
- 이 과정은 서피스(surface)·스왑체인(swapchain) 등의 다소 난해한 개념을 이해해야 하고, 무엇보다 아주 복잡한 그래픽스 파이프라인을 설정해야 합니다. 이는 자칫 초심자들의 흥미를 저해할 수 있습니다. 우리는 이것들 말고도 충분히 복잡한 객체들을 생성해야 합니다.
- 또한, Vulkan의 의의는 GPGPU 프레임워크라는 사실에 있습니다. 무언가를 ‘그린다’라는 개념은, 명백히 그릴 영역의 픽셀의 색을 결정하는 과정입니다. 예를 들어 1920x1080 해상도의 화면을 그린다는 것은, 총 1920×1080=2,073,600개의 픽셀의 빨강/초록/파랑 채널의 0부터 255까지의 값을 결정하는 과정입니다. 우리가 화면을 그릴 때 필요한 그래픽카드는 본질적으로 병렬 연산기와 이를 이용해 구현된 래스터라이저(rasterizer) 및 샘플러(sampler)의 모음입니다. 따라서, 우리는 이러한 본질적인 병렬 연산을 수행하는 과정부터 알아볼 것입니다.
그 대신, 다음과 같은 순서를 따르겠습니다.
- 먼저 컴퓨트 파이프라인(compute pipeline)을 이용해 버퍼를 조작하고 입출력하겠습니다. 이 과정을 통해 어떻게 GPU 파이프라인이 객체를 보고, 파이프라인을 실행하며, 명령을 작성하고 제출하는 방법을 다룸으로써, Vulkan을 관통하는 개념인 디스크립터 셋(descriptor set)과 커맨드 버퍼(command buffer)에 대해 이해할 것입니다.
- 이후 똑같이 컴퓨트 파이프라인을 이용해 이미지를 조작하고 입출력하겠습니다. 이 과정을 통해 이미지 레이아웃과 파이프라인 장벽을 이해할 것입니다.
- 컴퓨트 파이프라인을 이용한 두 과정이 끝났다면, 본격적으로 그래픽스 파이프라인(graphics pipeline)을 다루며 두 스테이지로 나뉜 버텍스(vertex)/프래그멘트(fragment) 셰이더에서의 데이터 흐름을 파악하며, 렌더링에 등장하는 어태치먼트(attachment)의 개념에 대해 이해할 것입니다.
세 과정이 모두 끝났다면, 비로소 WSI를 다룸으로써 튜토리얼을 따라하는 과정에서 중간중간 결과를 확인할 수 있게 됩니다. (그렇기에 우리는 아무것도 보지 못한 채 약 1,200줄에 달하는 코드를 작성하며 이해할 수 없는 과정을 반복하고, 코드를 실행했더니 검정 화면만 나타나는 불상사를 방지할 수 있겠죠!)
본 소주제에서는 위 1번 순서에 해당하는 버퍼 조작 및 입출력을 목표로 진행하겠습니다. 이를 위해서는 다음과 같은 개념을 알아야 합니다:
- 인스턴스(instance)
- 물리 디바이스(physical device)와 큐 패밀리(queue family)
- 디바이스(device)와 큐(queue)
- 버퍼(buffer)와 디바이스 메모리(device memory)
- 셰이더(shader)와 컴퓨트 파이프라인
- 디스크립터 풀(descriptor pool)과 디스크립터 셋(descriptor set)
- 커맨드 풀(command buffer)과 커맨드 버퍼
그리고 다음 질문에 답할 수 있게 될 것입니다.
- 어떻게 Vulkan 디바이스와 연산에 필요한 큐를 생성할 수 있는가?
- 파이프라인이란 무엇인가?
- 어떻게 GPU 메모리에서 데이터를 저장하고, 이를 ‘파이프라인이 볼 수 있게’ 하는가?
- 어떻게 명령을 GPU에 전달할 수 있는가?
이 모든 것들은 충분히 어려운 주제입니다. 차근차근 튜토리얼을 따라가며, 위 질문을 상기하세요.
첫 번째 관문: 디바이스와 큐 생성하기
위 그림은 Vulkan 객체들에 대한 계층 관계를 나타냅니다. 당장은 이 도표가 이해가 가지 않을 것이며, 아직은 이해할 필요가 없습니다. 단지 일단은 화살표로 연결된 개념끼리는 서로 생성할 수 있거나, 소유하거나, 그 참조를 가지고 있다 정도로 인지하면 됩니다. 예를 들어, 그림의 PhysicalDevice는 Instance로부터 생성될 수 있습니다.
우리의 첫번째 목표는 위 도표의 Device와 Queue를 생성하는 것입니다. 디바이스는 우리가 사용할 GPU의 논리적 표현이며, Vulkan의 대부분의 기능은 디바이스를 이용해 조작될 수 있습니다.
그러나, 어떤 기능은 직접적인 조작을 통해 수행하기에는 부적절할 수 있습니다. 우리가 GPU에 내리는 모든 명령은 GPU 드라이버를 통해 전달되고, 이 과정은 오버헤드를 일으킵니다. 따라서 어떤 명령들은 그 개개의 명령을 디바이스로 조작하는 것이 아닌, 이들을 순차적으로 기록한 후 전달하는 방식으로 오버헤드를 줄일 수 있습니다. 이들을 순차적으로 기록한 것이 커맨드 버퍼이며, 이를 전달받는 수신자가 큐입니다. 큐는 실제 GPU 하드웨어의 실행기에 대응하며, 여러 개 존재할 수 있습니다. 또한 각 큐의 능력(가능한 연산)은 다를 수 있습니다 (예를 들어, 어떤 큐는 그래픽 연산만 가능하고, 어떤 큐는 데이터 전송에 특화되어 있을 수 있습니다).
모든 Vulkan 프로그램은 디바이스와 큐를 필요로 하며, (위 도표에 따라) 이들을 생성할 수 있는 인스턴스/물리 디바이스를 마찬가지로 요구합니다. 인스턴스는 사용할 Vulkan 명세를 설정하고, 물리 디바이스는 기기에 존재하는 물리적 GPU에 대응하며 우리는 물리 디바이스를 통해 해당 GPU의 능력과 성질에 대해 알 수 있고, 실제 GPU의 큐를 능력에 따라 구분한 집합인 큐 패밀리를 얻어낼 수 있습니다. 물리 디바이스로부터 디바이스 (GPU의 논리적 표현)을 얻어내고, 큐 패밀리를 바탕으로 실제 큐를 얻어낼 수 있습니다.
이 모든 것들을 처음부터 이해하려 하지 마세요. 우리는 가장 단순한 상황을 다루고 있으며, 아직은 큐 패밀리와 큐를 구분하지 못해도 상관없습니다. 어쨌든 Vulkan 애플리케이션에서 가장 중요한 것은 GPU를 조작할 수 있는 디바이스와 명령을 전달할 수 있는 큐를 생성하는 것입니다. 지금부터 이 과정을 시작해보겠습니다.
컨텍스트와 인스턴스 생성하기
먼저, 위 도표에 나와있지 않은 vk::raii::Context
객체를 생성하겠습니다. 이는 공식 Vulkan 명세에는 존재하지 않는 개념으로, Vulkan-Hpp RAII 객체를 사용하기 위한 것으로 생각하면 됩니다. 이 객체는 프로그램 전체 수명에 오직 하나만 필요하며, 이를 이용해 인스턴스를 생성할 수 있습니다.
Subject: [PATCH] Create context.
---
Index: src/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/main.cpp b/src/main.cpp
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -2,5 +2,5 @@
import vulkan_hpp;
int main(){
- std::println("Hello world!");
+ const vk::raii::Context context{};
}
\ No newline at end of file
그 다음 인스턴스를 생성합니다. 앞서 설명했듯, 인스턴스는 사용할 Vulkan API 명세를 나타냅니다. 인스턴스를 생성하기 위해서는 Vulkan 애플리케이션의 정보와 사용할 버전이 필요합니다. 부가적으로, 우리는 VK_LAYER_KHRONOS_validation
라는 검증 레이어(validation layer)를 사용하여 API를 올바르게 사용하고 있는지를 점검하겠습니다. 이 레이어는 우리가 조작하는 GPU의 많은 부분에 관여하고 동작을 검증하기 때문에 높은 오버헤드를 불러일으킵니다. 따라서 레이어를 NDEBUG
매크로로 감싸 오직 디버그 빌드에서만 이를 활성화하겠습니다.
Subject: [PATCH] Create instance.
---
Index: src/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/main.cpp b/src/main.cpp
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -3,4 +3,22 @@
int main(){
const vk::raii::Context context{};
+
+ const vk::raii::Instance instance = [&] {
+ constexpr vk::ApplicationInfo appInfo {
+ "Vulkan tutorial", {},
+ {}, {},
+ vk::makeApiVersion(0, 1, 0, 0), // Vulkan 1.0.
+ };
+ const std::vector<const char*> instanceLayers {
+#ifndef NDEBUG
+ "VK_LAYER_KHRONOS_validation",
+#endif
+ };
+ return vk::raii::Instance { context, vk::InstanceCreateInfo {
+ {},
+ &appInfo,
+ instanceLayers,
+ } };
+ }();
}
\ No newline at end of file
위 코드에서, 우리는 다음과 같은 몇 가지 인사이트를 얻을 수 있습니다.
- 우리가 사용하는 모든 Vulkan 객체는
vk::raii
네임스페이스 내에 있습니다. 이는 정확히 맞는 말은 아닙니다 (예:vk::Queue
,vk::CommandBuffer
,vk::DescriptorSet
등). 정확히 말하면 GPU 상에서 독자적으로 존재해야 하는 객체들은 해당 네임스페이스 내에 있으며, RAII 패턴에 따라 이들은 스코프가 종료됨과 동시에 자동으로 소멸합니다. 즉,main
함수가 끝나는 시점에서 차례로instance
,context
가 소멸합니다. 다르게 말하면, 적어도context
의 수명은instance
의 수명보다 길어야 합니다. - 컨텍스트를 이용해 인스턴스를 생성하는 과정에서 인스턴스 생성자의 첫 번째 인수로 컨텍스트가 들어갑니다. 즉, 위 도표에 따라 물리 디바이스 생성자의 첫 번째 인수로 인스턴스가, 디바이스 생성자의 첫 번째 인수로 물리 디바이스가 들어갈 것이라 예측할 수 있습니다.
- 인스턴스 생성자의 두 번째 인수로는
vk::InstanceCreateInfo
구조체가 들어갑니다. 어떤 Vulkan 객체를 생성하기 위해서는 그 객체의-CreateInfo
접미사가 붙은 생성 정보 구조체가 필요합니다. - 인스턴스 생성자의 세 번째 인수는 (그리고 지금은 다루지 않는 네 번째 인수 또한)
vk::ArrayProxyNoTemporaries<const char *const>>
타입을 받습니다.vk::ArrayProxyNoTemporaries<T>
는 말 그대로 임시적이지 않은 연속된 T 객체들로부터 생성될 수 있습니다. 다음과 같은 코드를 예로 들어봅시다.1 2 3
std::array nums { 1, 2, 3 }; vk::ArrayProxyNoTemporaries numsProxy1 { nums }; // OK vk::ArrayProxyNoTemporaries numsProxy2 { std::array { 4, 5, 6 } }; // Error: { 4, 5, 6 } will destroyed after numsProxy2 declaration!
nums
은 적어도numsProxy1
이 생성되는 시점까지 소멸하지 않는 스택 변수이므로numsProxy1
은 올바른 정의지만,numsProxy2
는std::array { 4, 5, 6 }
인수가 전달 이후 바로 소멸하기 때문에 오류를 야기할 것입니다. 즉,vk::InstanceCreateInfo
생성자의 non-primitive한 인수는 해당 구조체에 소유되는 것이 아닌, 독자적으로 존재해야 합니다. 그리고 이는 인스턴스의 생성자가 실행되는 시점까지 살아있어야 합니다. 인스턴스가 생성된 이후에는 이들이 필요하지 않기에, 람다의 스코프가 종료되는 시점에서 소멸됩니다.
애플리케이션을 실행하고, 0을 반환하며 성공적으로 종료되는지 확인하세요.
vk::IncompatibleDriverError
오류가 발생하는 경우
macOS나 iOS를 비롯한 MoltenVK 환경에서 위 코드를 실행할 시 오류가 발생할 것입니다. GPU 제조사가 직접 Vulkan 드라이버를 제공하는 Windows나 Linux와 달리 이들 환경은 Metal API를 Vulkan 명세에 맞게 래핑한 것으로, portability extension이 필요합니다. 이들은 인스턴스 extension으로 전달될 수 있습니다. 부가적으로, 인스턴스의 첫 번째 인수인 flags
에 portability 플래그를 추가해야 하며, 추후 디바이스의 extension에도 portability extension (VK_KHR_portability_subset
)을 추가할 것입니다.
Subject: [PATCH] Fix incompatible driver error in MoltenVK environment.
---
Index: src/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/main.cpp b/src/main.cpp
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -15,10 +15,21 @@
"VK_LAYER_KHRONOS_validation",
#endif
};
+ const std::vector<const char*> instanceExtensions {
+#if __APPLE__
+ "VK_KHR_portability_enumeration",
+ "VK_KHR_get_physical_device_properties2",
+#endif
+ };
return vk::raii::Instance { context, vk::InstanceCreateInfo {
+#if __APPLE__
+ vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR,
+#else
{},
+#endif
&appInfo,
instanceLayers,
+ instanceExtensions,
} };
}();
}
\ No newline at end of file
위 -CreateInfo
접미사 명명 규칙과 마찬가지로 열거형 타입의 경우 -FlagBits
접미사가 붙으며, 이들은 -Flags
구조체로 암시적 변환될 수 있습니다. 다시 코드를 실행하여 정상 종료되는지 확인하세요.
2024년 4월 4일 기준, 현재 MoltenVK에서
VK_KHR_portability_enumeration
을 deprecated하고 새로운VK_KHR_portability_subset_metal
extension으로 대체할 예정입니다. 위 코드는 향후 작동하지 않을 가능성이 있으며, 해당 내용에 대해서는 위 링크의 PR을 참조하세요.
물리 디바이스와 가용 큐 패밀리 파악하기
한 시스템에서 여러 개의 GPU가 존재할 수 있습니다. 예를 들어 어떤 Windows 게이밍 랩톱에는 Intel 내장 그래픽과 NVIDIA의 고성능 외장 그래픽 카드가 있습니다. Vulkan SDK를 설치할 때 같이 설치된 VulkanCapsViewer를 통해 시스템에 어떤 물리 디바이스가 있는지 확인할 수 있습니다.
Apple Macbook Pro (2021)의 물리 디바이스. [GPU0] Apple M1 Pro 하나만 표시됩니다.
TODO: Windows 랩톱에서 VulkanCapsViewer 스크린샷 윈도우 랩톱의 물리 디바이스. 두 개의 물리 디바이스가 표시됩니다.
여러 개의 물리 디바이스 중 우리가 사용할 물리 디바이스를 골라야 합니다. 제일 최적의 물리 디바이스를 고르기 위해서 해당 GPU의 특성을 기반으로 택할 수 있겠으나, 지금 우리의 목표는 매우 단순한 연산이기 때문에 사실 어떤 것을 택하던 간에 큰 의미는 없을 것입니다. 코드의 단순화를 위해 인스턴스로부터 열거한 물리 디바이스 중 가장 첫 번째 것을 택하겠습니다.
Subject: [PATCH] Get first physical device from instance.
---
Index: src/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/main.cpp b/src/main.cpp
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -32,4 +32,6 @@
instanceExtensions,
} };
}();
+
+ const vk::raii::PhysicalDevice physicalDevice = instance.enumeratePhysicalDevices().front();
}
\ No newline at end of file
이 물리 디바이스는 여러개의 큐 패밀리를 가질 것이며, 그중 우리는 컴퓨트 파이프라인을 실행할 수 있는 컴퓨트 큐 패밀리를 택해야 합니다. 물리 디바이스로부터 큐 패밀리 프로퍼티를 열거하며 컴퓨트 기능을 지원하는 첫 번째 큐 패밀리의 인덱스를 가져오겠습니다.
Subject: [PATCH] Get compute queue family index from physical device.
---
Index: src/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/main.cpp b/src/main.cpp
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,6 +1,33 @@
import std;
import vulkan_hpp;
+#define FWD(...) static_cast<decltype(__VA_ARGS__)&&>(__VA_ARGS__)
+
+namespace std::ranges {
+ template <typename Derived>
+ struct range_adaptor_closure {
+ template <std::ranges::range R>
+ friend constexpr auto operator|(
+ R &&r,
+ const Derived &self
+ ) noexcept(std::is_nothrow_invocable_v<const Derived&, R>) -> auto {
+ return self(FWD(r));
+ }
+ };
+
+namespace views {
+ struct enumerate_fn : range_adaptor_closure<enumerate_fn> {
+ static constexpr auto operator()(
+ std::ranges::input_range auto &&r
+ ) -> auto {
+ return zip(iota(std::int64_t { 0 }), FWD(r));
+ }
+ };
+
+ constexpr enumerate_fn enumerate;
+}
+}
+
int main(){
const vk::raii::Context context{};
@@ -34,4 +61,15 @@
}();
const vk::raii::PhysicalDevice physicalDevice = instance.enumeratePhysicalDevices().front();
+
+ const std::uint32_t computeQueueFamilyIndex = [&] {
+ for (auto [idx, properties] : physicalDevice.getQueueFamilyProperties() | std::views::enumerate) {
+ if (properties.queueFlags & vk::QueueFlagBits::eCompute) {
+ return idx;
+ }
+ }
+
+ // Since Vulkan specifies that a compute queue must be present, this should never happen.
+ throw std::runtime_error { "No compute queue family in the physical device." };
+ }();
}
\ No newline at end of file
본 코드를 실행하는 컴파일러의 STL인 libc++17 기준
std::views::enumerate
가 구현되지 않았기 때문에 동일한 기능을 하는 코드를 추가했으며, GCC 13과 MSVC 17.8 기준 해당 기능은 구현되어 있습니다. Libc++에서 구현이 삭제될 예정입니다.std
네임스페이스를 오버라이딩하는 것은 충돌을 야기할 수 있기 때문에 지양하는 것이 좋습니다.
큐 패밀리 프로퍼티를 인덱스와 동시에 열거하기 위해 std::views::enumerate
를 사용했습니다. Vulkan 명세는 반드시 한 물리 디바이스에 컴퓨트 기능을 지원하는 큐가 존재하게끔 보장하므로, 밑 throw
구문에는 도달하지 않을 것입니다.
디바이스 생성 및 큐 불러오기
위 섹션에서 vk::raii
네임스페이스에 해당하지 않는 것으로 vk::Queue
가 있다는 사실을 기억하시나요?
vk::raii
네임스페이스에는 독자적으로 존재하는 객체들이 있다고 했으니, 그 말인즉슨 큐는 독자적으로 존재하지 않는다는 뜻입니다. 정확히 말하면 큐는 디바이스가 생성되는 동시에 같이 생성되며, 우리는 큐를 디바이스의 getQueue
메서드를 이용해 불러올 수 있습니다 (create라는 접두사가 아닌 get이라는 접두사가 사용되었음을 유의하세요. 대개 API에서 create는 객체를 생성하는 뜻을, get은 존재하는 객체에 대한 포인터를 불러오는 의미로 사용됩니다).
그렇기에 우리는 디바이스의 생성 정보 (vk::DeviceCreateInfo
)에 큐에 대한 정보를 추가적으로 넣어야 합니다. 큐를 생성하기 위해서는 해당 큐의 본래 큐 패밀리 인덱스와 해당 패밀리로부터 생성할 큐들의 우선도(priority)를 설정해야 합니다 (우선도가 높을수록 더 GPU에서 제출된 명령을 많이 수행하게끔 스케줄링됩니다). 우리는 오직 한 개의 컴퓨트 큐만을 생성하므로, 1.0 (최대 우선도) 하나만을 인수로 넘기겠습니다. 한 개의 원소를 가지는 우선도 배열을 넘김으로써, 우리는 암시적으로 큐를 한 개 생성한다는 정보도 넘기는 것입니다.
Subject: [PATCH] Create device and get compute queue.
---
Index: src/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/main.cpp b/src/main.cpp
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -72,4 +72,26 @@
// Since Vulkan specifies that a compute queue must be present, this should never happen.
throw std::runtime_error { "No compute queue family in the physical device." };
}();
+
+ const vk::raii::Device device = [&] {
+ constexpr std::array queuePriorities { 1.f };
+ const vk::DeviceQueueCreateInfo queueCreateInfo {
+ {},
+ computeQueueFamilyIndex,
+ queuePriorities,
+ };
+ const std::vector<const char*> deviceExtensions {
+#if __APPLE__
+ "VK_KHR_portability_subset",
+#endif
+ };
+ return vk::raii::Device { physicalDevice, vk::DeviceCreateInfo {
+ {},
+ queueCreateInfo,
+ {},
+ deviceExtensions,
+ } };
+ }();
+
+ const vk::Queue computeQueue = (*device).getQueue(computeQueueFamilyIndex, 0);
}
\ No newline at end of file
디바이스 extension에 대해서는 위
vk::IncompatibleDriverError
오류가 발생하는 경우 섹션을 참조하세요.
위 인스턴스를 생성하는 것과 마찬가지로, 디바이스 생성자의 첫 번째 인수는 물리 디바이스이고, 두 번째 인수로 -CreateInfo
접미사를 갖는 생성 정보 구조체를 넘기고 있습니다. 이 둘은 Vulkan 객체를 생성하는 관례입니다. 또, vk::InstanceCreateInfo
와 vk::DeviceCreateInfo
둘 다 생성자의 첫 번째 인수로 플래그를 넘기는 것을 확인하세요. 애플리케이션을 실행하고 성공적으로 종료되는지 확인하세요.
이렇게 디바이스와 컴퓨트 큐를 생성하였습니다. 앞으로의 모든 코드는 디바이스와 큐의 메서드로 진행될 것입니다. 다만, 둘이 정상 동작하기 위해서는 그 상위 객체 (컨텍스트, 인스턴스, 물리 디바이스)가 살아있어야 하므로 이들의 수명이 둘의 수명보다 길게 유지하여야 합니다.
이번 튜토리얼의 전체 코드 변경 사항은 다음과 같습니다.
Subject: [PATCH] Create device and get compute queue.
Get compute queue family index from physical device.
Get first physical device from instance.
Fix incompatible driver error in MoltenVK environment.
Create instance.
Create context.
---
Index: src/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/main.cpp b/src/main.cpp
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,6 +1,97 @@
import std;
import vulkan_hpp;
+#define FWD(...) static_cast<decltype(__VA_ARGS__)&&>(__VA_ARGS__)
+
+namespace std::ranges {
+ template <typename Derived>
+ struct range_adaptor_closure {
+ template <std::ranges::range R>
+ friend constexpr auto operator|(
+ R &&r,
+ const Derived &self
+ ) noexcept(std::is_nothrow_invocable_v<const Derived&, R>) -> auto {
+ return self(FWD(r));
+ }
+ };
+
+namespace views {
+ struct enumerate_fn : range_adaptor_closure<enumerate_fn> {
+ static constexpr auto operator()(
+ std::ranges::input_range auto &&r
+ ) -> auto {
+ return zip(iota(std::int64_t { 0 }), FWD(r));
+ }
+ };
+
+ constexpr enumerate_fn enumerate;
+}
+}
+
int main(){
- std::println("Hello world!");
+ const vk::raii::Context context{};
+
+ const vk::raii::Instance instance = [&] {
+ constexpr vk::ApplicationInfo appInfo {
+ "Vulkan tutorial", {},
+ {}, {},
+ vk::makeApiVersion(0, 1, 0, 0), // Vulkan 1.0.
+ };
+ const std::vector<const char*> instanceLayers {
+#ifndef NDEBUG
+ "VK_LAYER_KHRONOS_validation",
+#endif
+ };
+ const std::vector<const char*> instanceExtensions {
+#if __APPLE__
+ "VK_KHR_portability_enumeration",
+ "VK_KHR_get_physical_device_properties2",
+#endif
+ };
+ return vk::raii::Instance { context, vk::InstanceCreateInfo {
+#if __APPLE__
+ vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR,
+#else
+ {},
+#endif
+ &appInfo,
+ instanceLayers,
+ instanceExtensions,
+ } };
+ }();
+
+ const vk::raii::PhysicalDevice physicalDevice = instance.enumeratePhysicalDevices().front();
+
+ const std::uint32_t computeQueueFamilyIndex = [&] {
+ for (auto [idx, properties] : physicalDevice.getQueueFamilyProperties() | std::views::enumerate) {
+ if (properties.queueFlags & vk::QueueFlagBits::eCompute) {
+ return idx;
+ }
+ }
+
+ // Since Vulkan specifies that a compute queue must be present, this should never happen.
+ throw std::runtime_error { "No compute queue family in the physical device." };
+ }();
+
+ const vk::raii::Device device = [&] {
+ constexpr std::array queuePriorities { 1.f };
+ const vk::DeviceQueueCreateInfo queueCreateInfo {
+ {},
+ computeQueueFamilyIndex,
+ queuePriorities,
+ };
+ const std::vector<const char*> deviceExtensions {
+#if __APPLE__
+ "VK_KHR_portability_subset",
+#endif
+ };
+ return vk::raii::Device { physicalDevice, vk::DeviceCreateInfo {
+ {},
+ queueCreateInfo,
+ {},
+ deviceExtensions,
+ } };
+ }();
+
+ const vk::Queue computeQueue = (*device).getQueue(computeQueueFamilyIndex, 0);
}
\ No newline at end of file