diff --git a/guide/src/descriptor_sets/descriptor_buffer.md b/guide/src/descriptor_sets/descriptor_buffer.md index a581a0f..64f76de 100644 --- a/guide/src/descriptor_sets/descriptor_buffer.md +++ b/guide/src/descriptor_sets/descriptor_buffer.md @@ -1,6 +1,6 @@ # Descriptor Buffer -Uniform and Storage buffers need to be N-buffered unless they are "GPU const", ie contents do not change after creation. Encapsulate a `vma::Buffer` per virtual frame in a `DescriptorBuffer`: +Uniform and Storage buffers need to be N-buffered unless they are "GPU const", ie contents do not change after creation. Encapsulate a `vma::Buffer` per virtual frame in a `DescriptorBuffer`: ```cpp class DescriptorBuffer { diff --git a/guide/src/initialization/device.md b/guide/src/initialization/device.md index 5cb2530..f84867f 100644 --- a/guide/src/initialization/device.md +++ b/guide/src/initialization/device.md @@ -2,7 +2,7 @@ A [Vulkan Device](https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#devsandqueues-devices) is a logical instance of a Physical Device, and will the primary interface for everything Vulkan now onwards. [Vulkan Queues](https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#devsandqueues-queues) are owned by the Device, we will need one from the queue family stored in the `Gpu` to submit recorded command buffers. We also need to explicitly declare all features we want to use, eg [Dynamic Rendering](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_dynamic_rendering.html) and [Synchronization2](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_synchronization2.html). -Setup a `vk::QueueCreateInfo` object: +Setup a `vk::DeviceQueueCreateInfo` object: ```cpp auto queue_ci = vk::DeviceQueueCreateInfo{}; diff --git a/guide/src/initialization/instance.md b/guide/src/initialization/instance.md index da6cef2..0cc132f 100644 --- a/guide/src/initialization/instance.md +++ b/guide/src/initialization/instance.md @@ -16,7 +16,7 @@ constexpr auto vk_version_v = VK_MAKE_VERSION(1, 3, 0); } // namespace ``` -In `App`, create a new member function `create_instance()` and call it after `create_window()` in `run()`. After initializing the dispatcher, check that the loader meets the version requirement: +In `App`, create a new member function `create_instance()` and call it after `create_window()` in `run()`. After initializing the dispatcher, check that the loader meets the version requirement: ```cpp void App::create_instance() { diff --git a/guide/src/initialization/scoped_waiter.md b/guide/src/initialization/scoped_waiter.md index 7982c83..7e9fe11 100644 --- a/guide/src/initialization/scoped_waiter.md +++ b/guide/src/initialization/scoped_waiter.md @@ -2,7 +2,7 @@ A useful abstraction to have is an object that in its destructor waits/blocks until the Device is idle. It is incorrect usage to destroy Vulkan objects while they are in use by the GPU, such an object helps with making sure the device is idle before some dependent resource gets destroyed. -Being able to do arbitary things on scope exit will be useful in other spots too, so we encapsulate that in a basic class template `Scoped`. It's somewhat like a `unique_ptr` that stores the value (`Type`) instead of a pointer (`Type*`), with some constraints: +Being able to do arbitrary things on scope exit will be useful in other spots too, so we encapsulate that in a basic class template `Scoped`. It's somewhat like a `unique_ptr` that stores the value (`Type`) instead of a pointer (`Type*`), with some constraints: 1. `Type` must be default constructible 1. Assumes a default constructed `Type` is equivalent to null (does not call `Deleter`) diff --git a/guide/translations/ko-KR/src/initialization/device.md b/guide/translations/ko-KR/src/initialization/device.md index 1165677..1b4666d 100644 --- a/guide/translations/ko-KR/src/initialization/device.md +++ b/guide/translations/ko-KR/src/initialization/device.md @@ -2,7 +2,7 @@ [디바이스](https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#devsandqueues-devices)는 Physical Device의 논리적 인스턴스이며, 이후의 모든 Vulkan 작업에서 주요 인터페이스 역할을 하게 됩니다. [큐](https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#devsandqueues-queues)는 디바이스가 소유하는 것으로, `Gpu` 구조체에 저장된 큐 패밀리에서 하나를 가져와 기록된 커맨드 버퍼를 제출하는 데 사용할 것입니다. 또한 사용하기를 원하는 [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)같은 기능들을 명시적으로 선언해야 합니다. -`vk::QueueCreateInfo`객체를 설정합시다. +`vk::DeviceQueueCreateInfo`객체를 설정합시다. ```cpp auto queue_ci = vk::DeviceQueueCreateInfo{}; diff --git a/guide/translations/zh-TW/src/SUMMARY.md b/guide/translations/zh-TW/src/SUMMARY.md index 9bb6ddf..d56b3d2 100644 --- a/guide/translations/zh-TW/src/SUMMARY.md +++ b/guide/translations/zh-TW/src/SUMMARY.md @@ -8,3 +8,11 @@ - [Project Layout](getting_started/project_layout.md) - [Validation Layers](getting_started/validation_layers.md) - [class App](getting_started/class_app.md) +- [Initialization](initialization/README.md) + - [GLFW Window](initialization/glfw_window.md) + - [Vulkan Instance](initialization/instance.md) + - [Vulkan Surface](initialization/surface.md) + - [Vulkan Physical Device](initialization/gpu.md) + - [Vulkan Device](initialization/device.md) + - [Scoped Waiter](initialization/scoped_waiter.md) + - [Swapchain](initialization/swapchain.md) \ No newline at end of file diff --git a/guide/translations/zh-TW/src/initialization/README.md b/guide/translations/zh-TW/src/initialization/README.md new file mode 100644 index 0000000..4726963 --- /dev/null +++ b/guide/translations/zh-TW/src/initialization/README.md @@ -0,0 +1,12 @@ +# Initialization + +本節將處理所有必要系統的初始化工作,包括: + +- 初始化 GLFW 並建立視窗 +- 建立 Vulkan Instance +- 建立 Vulkan Surface +- 選擇 Vulkan Physical Device +- 建立 Vulkan logical Device +- 建立 Vulkan Swapchain + +如果其中任何一步失敗,都會是致命錯誤,之後的操作都會變得沒有意義 diff --git a/guide/translations/zh-TW/src/initialization/device.md b/guide/translations/zh-TW/src/initialization/device.md new file mode 100644 index 0000000..2aaee3c --- /dev/null +++ b/guide/translations/zh-TW/src/initialization/device.md @@ -0,0 +1,66 @@ +# Vulkan Device + +[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) + +接下來讓我們來設定各項功能。 首先要設定 `vk::DeviceQueueCreateInfo` 物件: + +```cpp +auto queue_ci = vk::DeviceQueueCreateInfo{}; +// since we use only one queue, it has the entire priority range, ie, 1.0 +static constexpr auto queue_priorities_v = std::array{1.0f}; +queue_ci.setQueueFamilyIndex(m_gpu.queue_family) + .setQueueCount(1) + .setQueuePriorities(queue_priorities_v); +``` + +接著設定核心功能: + +```cpp +// nice-to-have optional core features, enable if GPU supports them. +auto enabled_features = vk::PhysicalDeviceFeatures{}; +enabled_features.fillModeNonSolid = m_gpu.features.fillModeNonSolid; +enabled_features.wideLines = m_gpu.features.wideLines; +enabled_features.samplerAnisotropy = m_gpu.features.samplerAnisotropy; +enabled_features.sampleRateShading = m_gpu.features.sampleRateShading; +``` + +設定額外功能,並用 `setPNext()` 把它們串接起來: + +```cpp +// extra features that need to be explicitly enabled. +auto sync_feature = vk::PhysicalDeviceSynchronization2Features{vk::True}; +auto dynamic_rendering_feature = + vk::PhysicalDeviceDynamicRenderingFeatures{vk::True}; +// sync_feature.pNext => dynamic_rendering_feature, +// and later device_ci.pNext => sync_feature. +// this is 'pNext chaining'. +sync_feature.setPNext(&dynamic_rendering_feature); +``` + +建立並設定一個 `vk::DeviceCreateInfo` 物件: + +```cpp +auto device_ci = vk::DeviceCreateInfo{}; +// we only need one device extension: Swapchain. +static constexpr auto extensions_v = + std::array{VK_KHR_SWAPCHAIN_EXTENSION_NAME}; +device_ci.setPEnabledExtensionNames(extensions_v) + .setQueueCreateInfos(queue_ci) + .setPEnabledFeatures(&enabled_features) + .setPNext(&sync_feature); +``` + +在 `m_gpu` 之後宣告一個 `vk::UniqueDevice` 成員,建立它,並針對它初始化 dispatcher: + +```cpp +m_device = m_gpu.device.createDeviceUnique(device_ci); +// initialize the dispatcher against the created Device. +VULKAN_HPP_DEFAULT_DISPATCHER.init(*m_device); +``` + +宣告一個 `vk::Queue` 成員(順序無所謂,因為它只是個 handle,實際的 Queue 由 Device 擁有),並完成初始化: + +```cpp +static constexpr std::uint32_t queue_index_v{0}; +m_queue = m_device->getQueue(m_gpu.queue_family, queue_index_v); +``` diff --git a/guide/translations/zh-TW/src/initialization/glfw_window.md b/guide/translations/zh-TW/src/initialization/glfw_window.md new file mode 100644 index 0000000..778bb1e --- /dev/null +++ b/guide/translations/zh-TW/src/initialization/glfw_window.md @@ -0,0 +1,90 @@ +# GLFW Window + +本教學使用 GLFW(3.4)來處理視窗與相關事件。 這個函式庫和其他外部相依套件一樣,會在 `ext/CMakeLists.txt` 中設定並加入建置樹(build tree)。 對使用者而言,我們會定義 `GLFW_INCLUDE_VULKAN`,以啟用 GLFW 的 Vulkan 相關功能,這些功能被稱為「視窗系統整合(Window System Integration, WSI)」 + +GLFW 3.4 在 Linux 上支援 Wayland,並且預設會同時建置 X11 與 Wayland 兩種後端。 因此,你必須同時安裝[兩個平台的開發套件](https://www.glfw.org/docs/latest/compile_guide.html#compile_deps_wayland)(以及一些其他 Wayland/CMake 相依套件),才能順利完成設定與建置。 如果需要,你也可以在執行階段透過 `GLFW_PLATFORM` 來指定要使用的特定後端 + +雖然 Vulkan-GLFW 的應用程式可以同時擁有多個視窗,但這不在本書探討範圍內。 對我們而言,GLFW(函式庫)與單一視窗被視為一個整體單元,它們會一起初始化並一起銷毀。 由於 GLFW 會回傳一個不透明指標(opaque pointer) `GLFWwindow*`,因此你可以將它封裝在帶有自訂刪除器的 `std::unique_ptr` 中來管理 + +```cpp +// window.hpp +namespace lvk::glfw { +struct Deleter { + void operator()(GLFWwindow* window) const noexcept; +}; + +using Window = std::unique_ptr; + +// Returns a valid Window if successful, else throws. +[[nodiscard]] auto create_window(glm::ivec2 size, char const* title) -> Window; +} // namespace lvk::glfw + +// window.cpp +void Deleter::operator()(GLFWwindow* window) const noexcept { + glfwDestroyWindow(window); + glfwTerminate(); +} +``` + +GLFW 可以建立全螢幕或無邊框的視窗,本教學會使用帶有邊框與標題列的標準視窗。 由於如果無法建立視窗,就無法再進行任何有意義的操作,因此其他所有分支都會丟出致命例外(會在 main 中被捕捉) + +```cpp +auto glfw::create_window(glm::ivec2 const size, char const* title) -> Window { + static auto const on_error = [](int const code, char const* description) { + std::println(stderr, "[GLFW] Error {}: {}", code, description); + }; + glfwSetErrorCallback(on_error); + if (glfwInit() != GLFW_TRUE) { + throw std::runtime_error{"Failed to initialize GLFW"}; + } + // check for Vulkan support. + if (glfwVulkanSupported() != GLFW_TRUE) { + throw std::runtime_error{"Vulkan not supported"}; + } + auto ret = Window{}; + // tell GLFW that we don't want an OpenGL context. + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + ret.reset(glfwCreateWindow(size.x, size.y, title, nullptr, nullptr)); + if (!ret) { throw std::runtime_error{"Failed to create GLFW Window"}; } + return ret; +} +``` + +接著 `App` 會儲存一個 `glfw::Window`,並在 `run()` 中持續輪詢它,直到使用者關閉視窗為止。 雖然暫時還無法在視窗上繪製任何內容,但這是邁向該目標的第一步 + +首先將它宣告為私有成員: + +```cpp +private: + glfw::Window m_window{}; +``` + +接著加上一些私有的成員函式來將相關的操作封裝起來: + +```cpp +void create_window(); + +void main_loop(); +``` + +最後補上函式定義,並在 `run()` 中呼叫它們: + +```cpp +void App::run() { + create_window(); + + main_loop(); +} + +void App::create_window() { + m_window = glfw::create_window({1280, 720}, "Learn Vulkan"); +} + +void App::main_loop() { + while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) { + glfwPollEvents(); + } +} +``` + +> 如果是在 Wayland 上,此時還看不到視窗。 Wayland 需要應用程式將 framebuffer 給它後,視窗才會顯示出來 diff --git a/guide/translations/zh-TW/src/initialization/gpu.md b/guide/translations/zh-TW/src/initialization/gpu.md new file mode 100644 index 0000000..52246e9 --- /dev/null +++ b/guide/translations/zh-TW/src/initialization/gpu.md @@ -0,0 +1,94 @@ +# Vulkan Physical Device + +[Physical Device](https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#devsandqueues-physical-device-enumeration) 代表一個完整的 Vulkan 實作; 就我們的用途而言,可以把它視為一張 GPU(它也可能是像 Mesa/lavapipe 這樣的軟體算繪器)。 有些機器上可能同時存在多個 Physical Device,例如配備雙顯卡的筆電。 我們需要在下列限制條件下,挑選要使用的是哪一個: + +1. 必須支援 Vulkan 1.3 +2. 必須支援 Vulkan Swapchain +3. 必須有支援 Graphics 與 Transfer 作業的 Vulkan Queue +4. 必須能呈現(present)先前建立的 Vulkan Surface +5. (Optional)優先選擇獨立顯示卡(discrete GPU) + +我們會把實際的 Physical Device 以及其他幾個實用物件包裝成 `struct Gpu`。 由於它會搭配一個相當龐大的工具函式,我們會把它放到獨立的 hpp/cpp 檔案中,並把 `vk_version_v` 這個常數移到新的標頭檔裡: + +```cpp +constexpr auto vk_version_v = VK_MAKE_VERSION(1, 3, 0); + +struct Gpu { + vk::PhysicalDevice device{}; + vk::PhysicalDeviceProperties properties{}; + vk::PhysicalDeviceFeatures features{}; + std::uint32_t queue_family{}; +}; + +[[nodiscard]] auto get_suitable_gpu(vk::Instance instance, + vk::SurfaceKHR surface) -> Gpu; +``` + +函式定義: + +```cpp +auto lvk::get_suitable_gpu(vk::Instance const instance, + vk::SurfaceKHR const surface) -> Gpu { + auto const supports_swapchain = [](Gpu const& gpu) { + static constexpr std::string_view name_v = + VK_KHR_SWAPCHAIN_EXTENSION_NAME; + static constexpr auto is_swapchain = + [](vk::ExtensionProperties const& properties) { + return properties.extensionName.data() == name_v; + }; + auto const properties = gpu.device.enumerateDeviceExtensionProperties(); + auto const it = std::ranges::find_if(properties, is_swapchain); + return it != properties.end(); + }; + + auto const set_queue_family = [](Gpu& out_gpu) { + static constexpr auto queue_flags_v = + vk::QueueFlagBits::eGraphics | vk::QueueFlagBits::eTransfer; + for (auto const [index, family] : + std::views::enumerate(out_gpu.device.getQueueFamilyProperties())) { + if ((family.queueFlags & queue_flags_v) == queue_flags_v) { + out_gpu.queue_family = static_cast(index); + return true; + } + } + return false; + }; + + auto const can_present = [surface](Gpu const& gpu) { + return gpu.device.getSurfaceSupportKHR(gpu.queue_family, surface) == + vk::True; + }; + + auto fallback = Gpu{}; + for (auto const& device : instance.enumeratePhysicalDevices()) { + auto gpu = Gpu{.device = device, .properties = device.getProperties()}; + if (gpu.properties.apiVersion < vk_version_v) { continue; } + if (!supports_swapchain(gpu)) { continue; } + if (!set_queue_family(gpu)) { continue; } + if (!can_present(gpu)) { continue; } + gpu.features = gpu.device.getFeatures(); + if (gpu.properties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) { + return gpu; + } + // keep iterating in case we find a Discrete Gpu later. + fallback = gpu; + } + if (fallback.device) { return fallback; } + + throw std::runtime_error{"No suitable Vulkan Physical Devices"}; +} +``` + +最後,在 `App` 中加入 `Gpu` 成員,並在 `create_surface()` 之後初始化它: + +```cpp +create_surface(); +select_gpu(); + +// ... +void App::select_gpu() { + m_gpu = get_suitable_gpu(*m_instance, *m_surface); + std::println("Using GPU: {}", + std::string_view{m_gpu.properties.deviceName}); +} +``` diff --git a/guide/translations/zh-TW/src/initialization/instance.md b/guide/translations/zh-TW/src/initialization/instance.md new file mode 100644 index 0000000..677bc11 --- /dev/null +++ b/guide/translations/zh-TW/src/initialization/instance.md @@ -0,0 +1,87 @@ +# Vulkan Instance + +本教學不會在建置階段透過 SDK 連結 Vulkan,而是在執行期載入 Vulkan,這需要做一些調整: + +1. 在 CMake ext target 中定義 `VK_NO_PROTOTYPES`,讓 API 的函式宣告變成函示指標 +2. 在 `app.cpp` 的全域範圍新增這行:`VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE` +3. 在初始化前與初始化過程中呼叫 `VULKAN_HPP_DEFAULT_DISPATCHER.init()` + +在 Vulkan 中,第一步是建立一個 [Instance](https://docs.vulkan.org/spec/latest/chapters/initialization.html#initialization-instances),這讓我們能列舉可用的實體裝置(GPU),並建立邏輯裝置 + +由於我們需要 Vulkan 1.3,因此會將這個需求存為常數,以方便後續引用: + +```cpp +namespace { +constexpr auto vk_version_v = VK_MAKE_VERSION(1, 3, 0); +} // namespace +``` + +現在讓我們於 `App` 中建立一個新的成員函式 `create_instance()`,並在 `run()` 中於 `create_window()` 之後呼叫它。 這會在初始化 dispatcher 後,檢查載入器是否符合版本需求: + +```cpp +void App::create_instance() { + // initialize the dispatcher without any arguments. + VULKAN_HPP_DEFAULT_DISPATCHER.init(); + auto const loader_version = vk::enumerateInstanceVersion(); + if (loader_version < vk_version_v) { + throw std::runtime_error{"Loader does not support Vulkan 1.3"}; + } +} +``` + +我們之後會需要 WSI 的 instance 擴充功能,對此 GLFW 為我們提供了方便的介面。 讓我們在 `window.hpp/cpp` 中加入一個輔助函式: + +```cpp +auto glfw::instance_extensions() -> std::span { + auto count = std::uint32_t{}; + auto const* extensions = glfwGetRequiredInstanceExtensions(&count); + return {extensions, static_cast(count)}; +} +``` + +接著繼續建立 Instance,先建立一個 `vk::ApplicationInfo` 物件並填入其內容: + +```cpp +auto app_info = vk::ApplicationInfo{}; +app_info.setPApplicationName("Learn Vulkan").setApiVersion(vk_version_v); +``` + +並建立一個 `vk::InstanceCreateInfo` 並填入其內容: + +```cpp +auto instance_ci = vk::InstanceCreateInfo{}; +// need WSI instance extensions here (platform-specific Swapchains). +auto const extensions = glfw::instance_extensions(); +instance_ci.setPApplicationInfo(&app_info).setPEnabledExtensionNames( + extensions); +``` + +在 `m_window`「之後」新增一個 `vk::UniqueInstance` 成員,因為它必須在 GLFW 結束前銷毀。 建立它並針對它初始化 dispatcher: + +```cpp +glfw::Window m_window{}; +vk::UniqueInstance m_instance{}; + +// ... +// initialize the dispatcher against the created Instance. +m_instance = vk::createInstanceUnique(instance_ci); +VULKAN_HPP_DEFAULT_DISPATCHER.init(*m_instance); +``` + +請確保 VkConfig 是在啟用了 validation layer 的情況下執行的,接著再去偵錯/執行你的應用程式。 若啟用了「Information」等級的載入器訊息,此時你應該會在主控台上看到不少的輸出,像是已載入的 validation layer 的資訊、實體裝置(physical device)及其 ICD 的列舉結果等 + +如果在日誌中看不到以下這行或類似的內容,請重新檢查你 Vulkan Configurator 的設定與 `PATH`: + +``` +INFO | LAYER: Insert instance layer "VK_LAYER_KHRONOS_validation" +``` + +另外例如,如果應用程式或載入器無法找到 `libVkLayer_khronos_validation.so` / `VkLayer_khronos_validation.dll`,你會看到類似以下的訊息: + +``` +INFO | LAYER: Requested layer "VK_LAYER_KHRONOS_validation" failed to load. +``` + +至此,你已經成功初始化了 Vulkan Instance,恭喜你! + +> Wayland 使用者注意:要看到視窗還有很長一段路要走,目前 VkConfig 與驗證層的日誌就是你唯一的回饋來源 diff --git a/guide/translations/zh-TW/src/initialization/scoped_waiter.md b/guide/translations/zh-TW/src/initialization/scoped_waiter.md new file mode 100644 index 0000000..5fdeeff --- /dev/null +++ b/guide/translations/zh-TW/src/initialization/scoped_waiter.md @@ -0,0 +1,64 @@ +# Scoped Waiter + +有個實用的抽象做法是讓物件在解構子中等待/阻塞,直到 Device 進入閒置狀態。 因為你不能在 GPU 仍在使用某些 Vulkan 物件時就銷毀它們,這麼做的話,就能在銷毀相依資源之前,確保裝置已進入閒置狀態 + +能在離開作用域時執行任意動作,在其他情境也很有用,因此我們會把這個概念封裝成一個基礎的類別模板 `Scoped`。 它有點像 `unique_ptr`,但儲存的是值(`Type`),而不是指標(`Type*`),並有以下限制: + +1. `Type` 必須可預設建構(default constructible) +2. 視預設建構得到的 `Type` 等同於 `null`(因此不會呼叫 `Deleter`) + +```cpp +template +concept Scopeable = + std::equality_comparable && std::is_default_constructible_v; + +template +class Scoped { + public: + Scoped(Scoped const&) = delete; + auto operator=(Scoped const&) = delete; + + Scoped() = default; + + constexpr Scoped(Scoped&& rhs) noexcept + : m_t(std::exchange(rhs.m_t, Type{})) {} + + constexpr auto operator=(Scoped&& rhs) noexcept -> Scoped& { + if (&rhs != this) { std::swap(m_t, rhs.m_t); } + return *this; + } + + explicit(false) constexpr Scoped(Type t) : m_t(std::move(t)) {} + + constexpr ~Scoped() { + if (m_t == Type{}) { return; } + Deleter{}(m_t); + } + + [[nodiscard]] constexpr auto get() const -> Type const& { return m_t; } + [[nodiscard]] constexpr auto get() -> Type& { return m_t; } + + private: + Type m_t{}; +}; +``` + +這看起來不太直覺,但不用擔心,實作細節並不重要,重要的是它的作用以及如何使用它 + +現在我們可以很容易地實作一個 `ScopedWaiter` 了: + +```cpp +struct ScopedWaiterDeleter { + void operator()(vk::Device const device) const noexcept { + device.waitIdle(); + } +}; + +using ScopedWaiter = Scoped; +``` + +接著在 `App` 成員列表的「最後面」新增一個 `ScopedWaiter` 成員:它必須放在最後,這樣它會成為第一個被解構的成員,從而確保在解構其他成員之前,Device 已經進入閒置狀態了。 建立 Device 後初始化它: + +```cpp +m_waiter = *m_device; +``` diff --git a/guide/translations/zh-TW/src/initialization/surface.md b/guide/translations/zh-TW/src/initialization/surface.md new file mode 100644 index 0000000..5a7d8c2 --- /dev/null +++ b/guide/translations/zh-TW/src/initialization/surface.md @@ -0,0 +1,26 @@ +# Vulkan Surface + +Vulkan 與平台無關,它會透過 [`VK_KHR_surface` 擴充功能](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_surface.html)與 WSI 介接。 [Surface](https://docs.vulkan.org/guide/latest/wsi.html#_surface) 讓我們能透過呈現引擎(presentation engine)把影像顯示在視窗上 + +在 `window.hpp/cpp` 中再加入一個輔助函式: + +```cpp +auto glfw::create_surface(GLFWwindow* window, vk::Instance const instance) + -> vk::UniqueSurfaceKHR { + VkSurfaceKHR ret{}; + auto const result = + glfwCreateWindowSurface(instance, window, nullptr, &ret); + if (result != VK_SUCCESS || ret == VkSurfaceKHR{}) { + throw std::runtime_error{"Failed to create Vulkan Surface"}; + } + return vk::UniqueSurfaceKHR{ret, instance}; +} +``` + +在 `m_instance` 之後替 `App` 新增一個 `vk::UniqueSurfaceKHR` 的成員,並建立該 Surface: + +```cpp +void App::create_surface() { + m_surface = glfw::create_surface(m_window.get(), *m_instance); +} +``` diff --git a/guide/translations/zh-TW/src/initialization/swapchain.md b/guide/translations/zh-TW/src/initialization/swapchain.md new file mode 100644 index 0000000..ded3b91 --- /dev/null +++ b/guide/translations/zh-TW/src/initialization/swapchain.md @@ -0,0 +1,226 @@ +# Swapchain + +[Vulkan Swapchain](https://docs.vulkan.org/guide/latest/wsi.html#_swapchain) 是與某個 Surface 關聯的一組可呈現影像(presentable images),它充當應用程式與平台呈現引擎(compositor / display engine)之間的橋樑。 Swapchain 會在主迴圈中持續用來取得與呈現影像。 若 Swapchain 的建立失敗了將會是致命錯誤,因此它的建立屬於初始化流程的一部分。 + +我們會將 Vulkan Swapchain 封裝成自訂的 `class Swapchain`。 它還會儲存 Vulkan Swapchain 擁有的影像副本,並為每個影像建立(並擁有)對應的 Image View。 有時你會需要在主迴圈中重新建立 Vulkan Swapchain,例如當 framebuffer 大小改變,或 acquire/present 操作回傳 `vk::ErrorOutOfDateKHR` 時。 這會被封裝在一個 `recreate()` 函式中,在初始化時也可以直接呼叫該函式 + +```cpp +// swapchain.hpp +class Swapchain { + public: + explicit Swapchain(vk::Device device, Gpu const& gpu, + vk::SurfaceKHR surface, glm::ivec2 size); + + auto recreate(glm::ivec2 size) -> bool; + + [[nodiscard]] auto get_size() const -> glm::ivec2 { + return {m_ci.imageExtent.width, m_ci.imageExtent.height}; + } + + private: + void populate_images(); + void create_image_views(); + + vk::Device m_device{}; + Gpu m_gpu{}; + + vk::SwapchainCreateInfoKHR m_ci{}; + vk::UniqueSwapchainKHR m_swapchain{}; + std::vector m_images{}; + std::vector m_image_views{}; +}; + +// swapchain.cpp +Swapchain::Swapchain(vk::Device const device, Gpu const& gpu, + vk::SurfaceKHR const surface, glm::ivec2 const size) + : m_device(device), m_gpu(gpu) {} +``` + +## Static Swapchain Properties + +有些 Swapchain 會根據 Surface 的特性(capabilities)來建立如影像範圍大小與張數之類的參數,而這些特性在執行期間可能會變化。 我們可以在建構子中設定其他固定參數,並用一個輔助函式來取得所需的 [Surface Format](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSurfaceFormatKHR.html): + +```cpp +constexpr auto srgb_formats_v = std::array{ + vk::Format::eR8G8B8A8Srgb, + vk::Format::eB8G8R8A8Srgb, +}; + +// returns a SurfaceFormat with SrgbNonLinear color space and an sRGB format. +[[nodiscard]] constexpr auto +get_surface_format(std::span supported) + -> vk::SurfaceFormatKHR { + for (auto const desired : srgb_formats_v) { + auto const is_match = [desired](vk::SurfaceFormatKHR const& in) { + return in.format == desired && + in.colorSpace == + vk::ColorSpaceKHR::eVkColorspaceSrgbNonlinear; + }; + auto const it = std::ranges::find_if(supported, is_match); + if (it == supported.end()) { continue; } + return *it; + } + return supported.front(); +} +``` + +之所以偏好 sRGB 格式,是因為 sRGB 是顯示器的標準色彩空間。 這一點可由唯一的核心[色彩空間](https://registry.khronos.org/vulkan/specs/latest/man/html/VkColorSpaceKHR.html) `vk::ColorSpaceKHR::eVkColorspaceSrgbNonlinear` 得知(該設定表示支援 sRGB 色彩空間影像) + +現在可以實作 `Swapchain` 的建構子了: + +```cpp +auto const surface_format = + get_surface_format(m_gpu.device.getSurfaceFormatsKHR(surface)); +m_ci.setSurface(surface) + .setImageFormat(surface_format.format) + .setImageColorSpace(surface_format.colorSpace) + .setImageArrayLayers(1) + // Swapchain images will be used as color attachments (render targets). + .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment) + // eFifo is guaranteed to be supported. + .setPresentMode(vk::PresentModeKHR::eFifo); +if (!recreate(size)) { + throw std::runtime_error{"Failed to create Vulkan Swapchain"}; +} +``` + +## Swapchain Recreation + +[Surface Capabilities](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSurfaceCapabilitiesKHR.html) 規範了建立 Swapchain 時的參數限制。 根據規範,我們會新增兩個輔助函式與一個常數: + +```cpp +constexpr std::uint32_t min_images_v{3}; + +// returns currentExtent if specified, else clamped size. +[[nodiscard]] constexpr auto +get_image_extent(vk::SurfaceCapabilitiesKHR const& capabilities, + glm::uvec2 const size) -> vk::Extent2D { + constexpr auto limitless_v = 0xffffffff; + if (capabilities.currentExtent.width < limitless_v && + capabilities.currentExtent.height < limitless_v) { + return capabilities.currentExtent; + } + auto const x = std::clamp(size.x, capabilities.minImageExtent.width, + capabilities.maxImageExtent.width); + auto const y = std::clamp(size.y, capabilities.minImageExtent.height, + capabilities.maxImageExtent.height); + return vk::Extent2D{x, y}; +} + +[[nodiscard]] constexpr auto +get_image_count(vk::SurfaceCapabilitiesKHR const& capabilities) + -> std::uint32_t { + if (capabilities.maxImageCount < capabilities.minImageCount) { + return std::max(min_images_v, capabilities.minImageCount); + } + return std::clamp(min_images_v, capabilities.minImageCount, + capabilities.maxImageCount); +} +``` + +我們希望至少有三張影像,這樣才能夠啟用三重緩衝(triple buffering)。 雖然理論上某些 Surface 可能會出現 `maxImageCount < 3` 的情況,但這種狀況相當罕見,大多數的情況都是 `minImageCount > 3` + +Vulkan Image 的大小必須為正數,因此如果輸入的 framebuffer 大小不是正數,就會略過重建的嘗試。 這種情況在 Windows 上視窗被最小化時可能發生(在視窗恢復前,算繪基本上會暫停) + +```cpp +auto Swapchain::recreate(glm::ivec2 size) -> bool { + // Image sizes must be positive. + if (size.x <= 0 || size.y <= 0) { return false; } + + auto const capabilities = + m_gpu.device.getSurfaceCapabilitiesKHR(m_ci.surface); + m_ci.setImageExtent(get_image_extent(capabilities, size)) + .setMinImageCount(get_image_count(capabilities)) + .setOldSwapchain(m_swapchain ? *m_swapchain : vk::SwapchainKHR{}) + .setQueueFamilyIndices(m_gpu.queue_family); + assert(m_ci.imageExtent.width > 0 && m_ci.imageExtent.height > 0 && + m_ci.minImageCount >= min_images_v); + + // wait for the device to be idle before destroying the current swapchain. + m_device.waitIdle(); + m_swapchain = m_device.createSwapchainKHRUnique(m_ci); + + return true; +} +``` + +在成功重建之後,我們要把影像與 View 的那些 vector 填好。 針對影像部分,我們使用較穩妥的作法,避免每次都要指派一個新的 vector 給它: + +```cpp +void require_success(vk::Result const result, char const* error_msg) { + if (result != vk::Result::eSuccess) { throw std::runtime_error{error_msg}; } +} + +// ... +void Swapchain::populate_images() { + // we use the more verbose two-call API to avoid assigning m_images to a new + // vector on every call. + auto image_count = std::uint32_t{}; + auto result = + m_device.getSwapchainImagesKHR(*m_swapchain, &image_count, nullptr); + require_success(result, "Failed to get Swapchain Images"); + + m_images.resize(image_count); + result = m_device.getSwapchainImagesKHR(*m_swapchain, &image_count, + m_images.data()); + require_success(result, "Failed to get Swapchain Images"); +} +``` + +建立 Image View 的流程相對直接: + +```cpp +void Swapchain::create_image_views() { + auto subresource_range = vk::ImageSubresourceRange{}; + // this is a color image with 1 layer and 1 mip-level (the default). + subresource_range.setAspectMask(vk::ImageAspectFlagBits::eColor) + .setLayerCount(1) + .setLevelCount(1); + auto image_view_ci = vk::ImageViewCreateInfo{}; + // set common parameters here (everything except the Image). + image_view_ci.setViewType(vk::ImageViewType::e2D) + .setFormat(m_ci.imageFormat) + .setSubresourceRange(subresource_range); + m_image_views.clear(); + m_image_views.reserve(m_images.size()); + for (auto const image : m_images) { + image_view_ci.setImage(image); + m_image_views.push_back(m_device.createImageViewUnique(image_view_ci)); + } +} +``` + +現在我們可以在 `recreate()` 中於 `return true` 之前呼叫這些函式,並加上一行日誌作為回饋: + +```cpp +populate_images(); +create_image_views(); + +size = get_size(); +std::println("[lvk] Swapchain [{}x{}]", size.x, size.y); +return true; +``` + +> 在不斷調整視窗大小時(特別是在 Linux 上),這行日誌可能會過於頻繁地出現 + +為了取得 framebuffer 大小,在 `window.hpp/cpp` 中新增一個輔助函式: + +```cpp +auto glfw::framebuffer_size(GLFWwindow* window) -> glm::ivec2 { + auto ret = glm::ivec2{}; + glfwGetFramebufferSize(window, &ret.x, &ret.y); + return ret; +} +``` + +最後,在 `App` 中的 `m_device` 後面新增一個 `std::optional` 成員,新增建立函式,並在 `create_device()` 之後呼叫它: + +```cpp +std::optional m_swapchain{}; + +// ... +void App::create_swapchain() { + auto const size = glfw::framebuffer_size(m_window.get()); + m_swapchain.emplace(*m_device, m_gpu, *m_surface, size); +} +```