Skip to content

Commit 5ea8db8

Browse files
committed
Translate chapter 2 into Traditional Chinese
1 parent 33f5364 commit 5ea8db8

File tree

9 files changed

+673
-0
lines changed

9 files changed

+673
-0
lines changed

guide/translations/zh-TW/src/SUMMARY.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,11 @@
88
- [Project Layout](getting_started/project_layout.md)
99
- [Validation Layers](getting_started/validation_layers.md)
1010
- [class App](getting_started/class_app.md)
11+
- [Initialization](initialization/README.md)
12+
- [GLFW Window](initialization/glfw_window.md)
13+
- [Vulkan Instance](initialization/instance.md)
14+
- [Vulkan Surface](initialization/surface.md)
15+
- [Vulkan Physical Device](initialization/gpu.md)
16+
- [Vulkan Device](initialization/device.md)
17+
- [Scoped Waiter](initialization/scoped_waiter.md)
18+
- [Swapchain](initialization/swapchain.md)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Initialization
2+
3+
本節將處理所有必要系統的初始化工作,包括:
4+
5+
- 初始化 GLFW 並建立視窗
6+
- 建立 Vulkan Instance
7+
- 建立 Vulkan Surface
8+
- 選擇 Vulkan Physical Device
9+
- 建立 Vulkan logical Device
10+
- 建立 Vulkan Swapchain
11+
12+
如果其中任何一步失敗,都會是致命錯誤,之後的操作都會變得沒有意義
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Vulkan Device
2+
3+
[Vulkan Device](https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#devsandqueues-devices) 是實體裝置(Physical Device)的邏輯實例,它是我們之後操作 Vulkan 的主要介面。 [Vulkan Queues](https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#devsandqueues-queues) 由 Device 所擁有,我們需要從儲存在 `Gpu` 中的佇列家族(queue family)中取得一個 Device,來提交已記錄的指令緩衝區(command buffer)。 我們還需要顯式地宣告所有想要使用的功能,例如 [Dynamic Rendering](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_dynamic_rendering.html)[Synchronization2](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_synchronization2.html)
4+
5+
接下來讓我們來設定各項功能。 首先要設定 `vk::DeviceQueueCreateInfo` 物件:
6+
7+
```cpp
8+
auto queue_ci = vk::DeviceQueueCreateInfo{};
9+
// since we use only one queue, it has the entire priority range, ie, 1.0
10+
static constexpr auto queue_priorities_v = std::array{1.0f};
11+
queue_ci.setQueueFamilyIndex(m_gpu.queue_family)
12+
.setQueueCount(1)
13+
.setQueuePriorities(queue_priorities_v);
14+
```
15+
16+
接著設定核心功能:
17+
18+
```cpp
19+
// nice-to-have optional core features, enable if GPU supports them.
20+
auto enabled_features = vk::PhysicalDeviceFeatures{};
21+
enabled_features.fillModeNonSolid = m_gpu.features.fillModeNonSolid;
22+
enabled_features.wideLines = m_gpu.features.wideLines;
23+
enabled_features.samplerAnisotropy = m_gpu.features.samplerAnisotropy;
24+
enabled_features.sampleRateShading = m_gpu.features.sampleRateShading;
25+
```
26+
27+
設定額外功能,並用 `setPNext()` 把它們串接起來:
28+
29+
```cpp
30+
// extra features that need to be explicitly enabled.
31+
auto sync_feature = vk::PhysicalDeviceSynchronization2Features{vk::True};
32+
auto dynamic_rendering_feature =
33+
vk::PhysicalDeviceDynamicRenderingFeatures{vk::True};
34+
// sync_feature.pNext => dynamic_rendering_feature,
35+
// and later device_ci.pNext => sync_feature.
36+
// this is 'pNext chaining'.
37+
sync_feature.setPNext(&dynamic_rendering_feature);
38+
```
39+
40+
建立並設定一個 `vk::DeviceCreateInfo` 物件:
41+
42+
```cpp
43+
auto device_ci = vk::DeviceCreateInfo{};
44+
// we only need one device extension: Swapchain.
45+
static constexpr auto extensions_v =
46+
std::array{VK_KHR_SWAPCHAIN_EXTENSION_NAME};
47+
device_ci.setPEnabledExtensionNames(extensions_v)
48+
.setQueueCreateInfos(queue_ci)
49+
.setPEnabledFeatures(&enabled_features)
50+
.setPNext(&sync_feature);
51+
```
52+
53+
`m_gpu` 之後宣告一個 `vk::UniqueDevice` 成員,建立它,並針對它初始化 dispatcher:
54+
55+
```cpp
56+
m_device = m_gpu.device.createDeviceUnique(device_ci);
57+
// initialize the dispatcher against the created Device.
58+
VULKAN_HPP_DEFAULT_DISPATCHER.init(*m_device);
59+
```
60+
61+
宣告一個 `vk::Queue` 成員(順序無所謂,因為它只是個 handle,實際的 Queue 由 Device 擁有),並完成初始化:
62+
63+
```cpp
64+
static constexpr std::uint32_t queue_index_v{0};
65+
m_queue = m_device->getQueue(m_gpu.queue_family, queue_index_v);
66+
```
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# GLFW Window
2+
3+
本教學使用 GLFW(3.4)來處理視窗與相關事件。 這個函式庫和其他外部相依套件一樣,會在 `ext/CMakeLists.txt` 中設定並加入建置樹(build tree)。 對使用者而言,我們會定義 `GLFW_INCLUDE_VULKAN`,以啟用 GLFW 的 Vulkan 相關功能,這些功能被稱為「視窗系統整合(Window System Integration, WSI)」
4+
5+
GLFW 3.4 在 Linux 上支援 Wayland,並且預設會同時建置 X11 與 Wayland 兩種後端。 因此,你必須同時安裝[兩個平台的開發套件](https://www.glfw.org/docs/latest/compile_guide.html#compile_deps_wayland)(以及一些其他 Wayland/CMake 相依套件),才能順利完成設定與建置。 如果需要,你也可以在執行階段透過 `GLFW_PLATFORM` 來指定要使用的特定後端
6+
7+
雖然 Vulkan-GLFW 的應用程式可以同時擁有多個視窗,但這不在本書探討範圍內。 對我們而言,GLFW(函式庫)與單一視窗被視為一個整體單元,它們會一起初始化並一起銷毀。 由於 GLFW 會回傳一個不透明指標(opaque pointer) `GLFWwindow*`,因此你可以將它封裝在帶有自訂刪除器的 `std::unique_ptr` 中來管理
8+
9+
```cpp
10+
// window.hpp
11+
namespace lvk::glfw {
12+
struct Deleter {
13+
void operator()(GLFWwindow* window) const noexcept;
14+
};
15+
16+
using Window = std::unique_ptr<GLFWwindow, Deleter>;
17+
18+
// Returns a valid Window if successful, else throws.
19+
[[nodiscard]] auto create_window(glm::ivec2 size, char const* title) -> Window;
20+
} // namespace lvk::glfw
21+
22+
// window.cpp
23+
void Deleter::operator()(GLFWwindow* window) const noexcept {
24+
glfwDestroyWindow(window);
25+
glfwTerminate();
26+
}
27+
```
28+
29+
GLFW 可以建立全螢幕或無邊框的視窗,本教學會使用帶有邊框與標題列的標準視窗。 由於如果無法建立視窗,就無法再進行任何有意義的操作,因此其他所有分支都會丟出致命例外(會在 main 中被捕捉)
30+
31+
```cpp
32+
auto glfw::create_window(glm::ivec2 const size, char const* title) -> Window {
33+
static auto const on_error = [](int const code, char const* description) {
34+
std::println(stderr, "[GLFW] Error {}: {}", code, description);
35+
};
36+
glfwSetErrorCallback(on_error);
37+
if (glfwInit() != GLFW_TRUE) {
38+
throw std::runtime_error{"Failed to initialize GLFW"};
39+
}
40+
// check for Vulkan support.
41+
if (glfwVulkanSupported() != GLFW_TRUE) {
42+
throw std::runtime_error{"Vulkan not supported"};
43+
}
44+
auto ret = Window{};
45+
// tell GLFW that we don't want an OpenGL context.
46+
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
47+
ret.reset(glfwCreateWindow(size.x, size.y, title, nullptr, nullptr));
48+
if (!ret) { throw std::runtime_error{"Failed to create GLFW Window"}; }
49+
return ret;
50+
}
51+
```
52+
53+
接著 `App` 會儲存一個 `glfw::Window`,並在 `run()` 中持續輪詢它,直到使用者關閉視窗為止。 雖然暫時還無法在視窗上繪製任何內容,但這是邁向該目標的第一步
54+
55+
首先將它宣告為私有成員:
56+
57+
```cpp
58+
private:
59+
glfw::Window m_window{};
60+
```
61+
62+
接著加上一些私有的成員函式來將相關的操作封裝起來:
63+
64+
```cpp
65+
void create_window();
66+
67+
void main_loop();
68+
```
69+
70+
最後補上函式定義,並在 `run()` 中呼叫它們:
71+
72+
```cpp
73+
void App::run() {
74+
create_window();
75+
76+
main_loop();
77+
}
78+
79+
void App::create_window() {
80+
m_window = glfw::create_window({1280, 720}, "Learn Vulkan");
81+
}
82+
83+
void App::main_loop() {
84+
while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) {
85+
glfwPollEvents();
86+
}
87+
}
88+
```
89+
90+
> 如果是在 Wayland 上,此時還看不到視窗。 Wayland 需要應用程式將 framebuffer 給它後,視窗才會顯示出來
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Vulkan Physical Device
2+
3+
[Physical Device](https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#devsandqueues-physical-device-enumeration) 代表一個完整的 Vulkan 實作; 就我們的用途而言,可以把它視為一張 GPU(它也可能是像 Mesa/lavapipe 這樣的軟體算繪器)。 有些機器上可能同時存在多個 Physical Device,例如配備雙顯卡的筆電。 我們需要在下列限制條件下,挑選要使用的是哪一個:
4+
5+
1. 必須支援 Vulkan 1.3
6+
2. 必須支援 Vulkan Swapchain
7+
3. 必須有支援 Graphics 與 Transfer 作業的 Vulkan Queue
8+
4. 必須能呈現(present)先前建立的 Vulkan Surface
9+
5. (Optional)優先選擇獨立顯示卡(discrete GPU)
10+
11+
我們會把實際的 Physical Device 以及其他幾個實用物件包裝成 `struct Gpu`。 由於它會搭配一個相當龐大的工具函式,我們會把它放到獨立的 hpp/cpp 檔案中,並把 `vk_version_v` 這個常數移到新的標頭檔裡:
12+
13+
```cpp
14+
constexpr auto vk_version_v = VK_MAKE_VERSION(1, 3, 0);
15+
16+
struct Gpu {
17+
vk::PhysicalDevice device{};
18+
vk::PhysicalDeviceProperties properties{};
19+
vk::PhysicalDeviceFeatures features{};
20+
std::uint32_t queue_family{};
21+
};
22+
23+
[[nodiscard]] auto get_suitable_gpu(vk::Instance instance,
24+
vk::SurfaceKHR surface) -> Gpu;
25+
```
26+
27+
函式定義:
28+
29+
```cpp
30+
auto lvk::get_suitable_gpu(vk::Instance const instance,
31+
vk::SurfaceKHR const surface) -> Gpu {
32+
auto const supports_swapchain = [](Gpu const& gpu) {
33+
static constexpr std::string_view name_v =
34+
VK_KHR_SWAPCHAIN_EXTENSION_NAME;
35+
static constexpr auto is_swapchain =
36+
[](vk::ExtensionProperties const& properties) {
37+
return properties.extensionName.data() == name_v;
38+
};
39+
auto const properties = gpu.device.enumerateDeviceExtensionProperties();
40+
auto const it = std::ranges::find_if(properties, is_swapchain);
41+
return it != properties.end();
42+
};
43+
44+
auto const set_queue_family = [](Gpu& out_gpu) {
45+
static constexpr auto queue_flags_v =
46+
vk::QueueFlagBits::eGraphics | vk::QueueFlagBits::eTransfer;
47+
for (auto const [index, family] :
48+
std::views::enumerate(out_gpu.device.getQueueFamilyProperties())) {
49+
if ((family.queueFlags & queue_flags_v) == queue_flags_v) {
50+
out_gpu.queue_family = static_cast<std::uint32_t>(index);
51+
return true;
52+
}
53+
}
54+
return false;
55+
};
56+
57+
auto const can_present = [surface](Gpu const& gpu) {
58+
return gpu.device.getSurfaceSupportKHR(gpu.queue_family, surface) ==
59+
vk::True;
60+
};
61+
62+
auto fallback = Gpu{};
63+
for (auto const& device : instance.enumeratePhysicalDevices()) {
64+
auto gpu = Gpu{.device = device, .properties = device.getProperties()};
65+
if (gpu.properties.apiVersion < vk_version_v) { continue; }
66+
if (!supports_swapchain(gpu)) { continue; }
67+
if (!set_queue_family(gpu)) { continue; }
68+
if (!can_present(gpu)) { continue; }
69+
gpu.features = gpu.device.getFeatures();
70+
if (gpu.properties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) {
71+
return gpu;
72+
}
73+
// keep iterating in case we find a Discrete Gpu later.
74+
fallback = gpu;
75+
}
76+
if (fallback.device) { return fallback; }
77+
78+
throw std::runtime_error{"No suitable Vulkan Physical Devices"};
79+
}
80+
```
81+
82+
最後,在 `App` 中加入 `Gpu` 成員,並在 `create_surface()` 之後初始化它:
83+
84+
```cpp
85+
create_surface();
86+
select_gpu();
87+
88+
// ...
89+
void App::select_gpu() {
90+
m_gpu = get_suitable_gpu(*m_instance, *m_surface);
91+
std::println("Using GPU: {}",
92+
std::string_view{m_gpu.properties.deviceName});
93+
}
94+
```
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Vulkan Instance
2+
3+
本教學不會在建置階段透過 SDK 連結 Vulkan,而是在執行期載入 Vulkan,這需要做一些調整:
4+
5+
1. 在 CMake ext target 中定義 `VK_NO_PROTOTYPES`,讓 API 的函式宣告變成函示指標
6+
2.`app.cpp` 的全域範圍新增這行:`VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE`
7+
3. 在初始化前與初始化過程中呼叫 `VULKAN_HPP_DEFAULT_DISPATCHER.init()`
8+
9+
在 Vulkan 中,第一步是建立一個 [Instance](https://docs.vulkan.org/spec/latest/chapters/initialization.html#initialization-instances),這讓我們能列舉可用的實體裝置(GPU),並建立邏輯裝置
10+
11+
由於我們需要 Vulkan 1.3,因此會將這個需求存為常數,以方便後續引用:
12+
13+
```cpp
14+
namespace {
15+
constexpr auto vk_version_v = VK_MAKE_VERSION(1, 3, 0);
16+
} // namespace
17+
```
18+
19+
現在讓我們於 `App` 中建立一個新的成員函式 `create_instance()`,並在 `run()` 中於 `create_window()` 之後呼叫它。 這會在初始化 dispatcher 後,檢查載入器是否符合版本需求:
20+
21+
```cpp
22+
void App::create_instance() {
23+
// initialize the dispatcher without any arguments.
24+
VULKAN_HPP_DEFAULT_DISPATCHER.init();
25+
auto const loader_version = vk::enumerateInstanceVersion();
26+
if (loader_version < vk_version_v) {
27+
throw std::runtime_error{"Loader does not support Vulkan 1.3"};
28+
}
29+
}
30+
```
31+
32+
我們之後會需要 WSI 的 instance 擴充功能,對此 GLFW 為我們提供了方便的介面。 讓我們在 `window.hpp/cpp` 中加入一個輔助函式:
33+
34+
```cpp
35+
auto glfw::instance_extensions() -> std::span<char const* const> {
36+
auto count = std::uint32_t{};
37+
auto const* extensions = glfwGetRequiredInstanceExtensions(&count);
38+
return {extensions, static_cast<std::size_t>(count)};
39+
}
40+
```
41+
42+
接著繼續建立 Instance,先建立一個 `vk::ApplicationInfo` 物件並填入其內容:
43+
44+
```cpp
45+
auto app_info = vk::ApplicationInfo{};
46+
app_info.setPApplicationName("Learn Vulkan").setApiVersion(vk_version_v);
47+
```
48+
49+
並建立一個 `vk::InstanceCreateInfo` 並填入其內容:
50+
51+
```cpp
52+
auto instance_ci = vk::InstanceCreateInfo{};
53+
// need WSI instance extensions here (platform-specific Swapchains).
54+
auto const extensions = glfw::instance_extensions();
55+
instance_ci.setPApplicationInfo(&app_info).setPEnabledExtensionNames(
56+
extensions);
57+
```
58+
59+
`m_window`「之後」新增一個 `vk::UniqueInstance` 成員,因為它必須在 GLFW 結束前銷毀。 建立它並針對它初始化 dispatcher:
60+
61+
```cpp
62+
glfw::Window m_window{};
63+
vk::UniqueInstance m_instance{};
64+
65+
// ...
66+
// initialize the dispatcher against the created Instance.
67+
m_instance = vk::createInstanceUnique(instance_ci);
68+
VULKAN_HPP_DEFAULT_DISPATCHER.init(*m_instance);
69+
```
70+
71+
請確保 VkConfig 是在啟用了 validation layer 的情況下執行的,接著再去偵錯/執行你的應用程式。 若啟用了「Information」等級的載入器訊息,此時你應該會在主控台上看到不少的輸出,像是已載入的 validation layer 的資訊、實體裝置(physical device)及其 ICD 的列舉結果等
72+
73+
如果在日誌中看不到以下這行或類似的內容,請重新檢查你 Vulkan Configurator 的設定與 `PATH`
74+
75+
```
76+
INFO | LAYER: Insert instance layer "VK_LAYER_KHRONOS_validation"
77+
```
78+
79+
另外例如,如果應用程式或載入器無法找到 `libVkLayer_khronos_validation.so` / `VkLayer_khronos_validation.dll`,你會看到類似以下的訊息:
80+
81+
```
82+
INFO | LAYER: Requested layer "VK_LAYER_KHRONOS_validation" failed to load.
83+
```
84+
85+
至此,你已經成功初始化了 Vulkan Instance,恭喜你!
86+
87+
> Wayland 使用者注意:要看到視窗還有很長一段路要走,目前 VkConfig 與驗證層的日誌就是你唯一的回饋來源

0 commit comments

Comments
 (0)