From 30dce1ad936f0204c3b4cd1029c010bad7c571bd Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Sun, 11 May 2025 11:58:37 -0700 Subject: [PATCH 1/6] CI: add `workflow_dispatch` --- .github/workflows/ci.yml | 1 + .github/workflows/deploy.yml | 1 + .github/workflows/guide.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f3da2f..a6eb13e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: branches-ignore: - staging - production + workflow_dispatch: jobs: format-check: runs-on: ubuntu-latest diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 23c2655..4b0afa6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -4,6 +4,7 @@ on: branches: - production - staging + workflow_dispatch: jobs: deploy: runs-on: ubuntu-latest diff --git a/.github/workflows/guide.yml b/.github/workflows/guide.yml index 0400187..c8c2033 100644 --- a/.github/workflows/guide.yml +++ b/.github/workflows/guide.yml @@ -3,6 +3,7 @@ on: pull_request: branches-ignore: - staging + workflow_dispatch: jobs: build-book: runs-on: ubuntu-latest From 381daf0ce7946917cbf7c607f95bd8511f2fb804 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Sun, 11 May 2025 14:00:36 -0700 Subject: [PATCH 2/6] Port present semaphore sync fix to main (#26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🐛 Fix present semaphore sync (#25) * Refactor build scripts and presets (#24) - Move compile flags to presets - Update CI script * :bug: Fix present semaphore sync Discovered on SDK 1.4.313 * Fixup guide --- .github/workflows/ci.yml | 24 ++-- CMakePresets.json | 156 +++++++++++++++++++++-- guide/src/rendering/dynamic_rendering.md | 2 +- guide/src/rendering/render_sync.md | 3 - guide/src/rendering/swapchain_loop.md | 4 +- guide/src/rendering/swapchain_update.md | 42 +++++- src/app.cpp | 6 +- src/app.hpp | 6 +- src/swapchain.cpp | 21 ++- src/swapchain.hpp | 6 +- 10 files changed, 227 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6eb13e..dbd67a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,11 +27,11 @@ jobs: - name: init run: uname -m; sudo apt update -yqq && sudo apt install -yqq ninja-build mesa-common-dev libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules - name: configure - run: export CXX=g++-14; cmake -S . --preset=default -B build -DGLFW_BUILD_X11=OFF + run: cmake -S . --preset=ninja-gcc -B build -DGLFW_BUILD_X11=OFF -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 - name: build debug - run: cmake --build build --config=Debug + run: cmake --build build --config=Debug -- -v - name: build release - run: cmake --build build --config=Release + run: cmake --build build --config=Release -- -v - name: test debug run: cd build && ctest -V -C Debug - name: test release @@ -45,9 +45,9 @@ jobs: - name: configure run: cmake -S . --preset=ninja-clang -B build -DGLFW_BUILD_X11=OFF - name: build debug - run: cmake --build build --config=Debug + run: cmake --build build --config=Debug -- -v - name: build release - run: cmake --build build --config=Release + run: cmake --build build --config=Release -- -v - name: test debug run: cd build && ctest -V -C Debug - name: test release @@ -59,11 +59,11 @@ jobs: - name: init run: uname -m; sudo apt update -yqq && sudo apt install -yqq ninja-build mesa-common-dev libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules - name: configure - run: export CXX=g++-14; cmake -S . --preset=default -B build -DGLFW_BUILD_X11=OFF + run: cmake -S . --preset=ninja-gcc -B build -DGLFW_BUILD_X11=OFF -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 - name: build debug - run: cmake --build build --config=Debug + run: cmake --build build --config=Debug -- -v - name: build release - run: cmake --build build --config=Release + run: cmake --build build --config=Release -- -v - name: test debug run: cd build && ctest -V -C Debug - name: test release @@ -77,9 +77,9 @@ jobs: - name: configure run: cmake -S . --preset=ninja-clang -B build -DGLFW_BUILD_X11=OFF - name: build debug - run: cmake --build build --config=Debug + run: cmake --build build --config=Debug -- -v - name: build release - run: cmake --build build --config=Release + run: cmake --build build --config=Release -- -v - name: test debug run: cd build && ctest -V -C Debug - name: test release @@ -107,9 +107,9 @@ jobs: - name: configure run: cmake -S . --preset=ninja-clang -B clang - name: build debug - run: cmake --build clang --config=Debug + run: cmake --build clang --config=Debug -- -v - name: build release - run: cmake --build clang --config=Release + run: cmake --build clang --config=Release -- -v - name: test debug run: cd clang && ctest -V -C Debug - name: test release diff --git a/CMakePresets.json b/CMakePresets.json index 8ddf102..a56321d 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,14 +1,15 @@ { - "version": 2, + "version": 5, "cmakeMinimumRequired": { "major": 3, - "minor": 20, + "minor": 24, "patch": 0 }, "configurePresets": [ { "name": "default", - "description": "Build configuration using Ninja Multi-config", + "displayName": "Default Config", + "description": "Base configuration using Ninja Multi-config", "generator": "Ninja Multi-Config", "binaryDir": "${sourceDir}/out/default", "cacheVariables": { @@ -16,50 +17,115 @@ } }, { - "name": "ninja-clang", - "description": "Build configuration using Ninja Multi-config / clang", + "name": "base-gcc", + "hidden": true, + "inherits": "default", + "cacheVariables": { + "CMAKE_C_COMPILER": "gcc", + "CMAKE_CXX_COMPILER": "g++" + } + }, + { + "name": "base-clang", + "hidden": true, "inherits": "default", - "binaryDir": "${sourceDir}/out/clang", "cacheVariables": { "CMAKE_C_COMPILER": "clang", "CMAKE_CXX_COMPILER": "clang++" } }, + { + "name": "base-msvc", + "hidden": true, + "inherits": "default", + "cacheVariables": { + "CMAKE_C_COMPILER": "cl", + "CMAKE_CXX_COMPILER": "cl" + } + }, + { + "name": "ninja-gcc", + "displayName": "Ninja GCC", + "description": "Build configuration using Ninja Multi-config / GCC", + "inherits": "base-gcc", + "binaryDir": "${sourceDir}/out/gcc", + "cacheVariables": { + "CMAKE_CXX_FLAGS_DEBUG_INIT": "-Wall -Wextra -Wpedantic -Werror=return-type", + "CMAKE_CXX_FLAGS_INIT": "-Wall -Wextra -Wpedantic -Werror" + } + }, + { + "name": "ninja-clang", + "displayName": "Ninja Clang", + "description": "Build configuration using Ninja Multi-config / Clang", + "inherits": "base-clang", + "binaryDir": "${sourceDir}/out/clang", + "cacheVariables": { + "CMAKE_CXX_FLAGS_DEBUG_INIT": "-Wall -Wextra -Wpedantic -Werror=return-type", + "CMAKE_CXX_FLAGS_INIT": "-Wall -Wextra -Wpedantic -Werror" + } + }, + { + "name": "ninja-msvc", + "displayName": "Ninja MSVC", + "description": "Build configuration using Ninja Multi-config / MSVC", + "inherits": "base-msvc", + "binaryDir": "${sourceDir}/out/msvc", + "cacheVariables": { + "CMAKE_CXX_FLAGS_INIT": "/WX" + } + }, { "name": "ninja-ubsan", + "displayName": "Ninja UBSan", "description": "UBSan build configuration using Ninja Multi-config", "inherits": "default", "binaryDir": "${sourceDir}/out/ubsan", "cacheVariables": { - "CMAKE_C_FLAGS": "-fsanitize=undefined", - "CMAKE_CXX_FLAGS": "-fsanitize=undefined" + "CMAKE_CXX_FLAGS_DEBUG_INIT": "-fsanitize=undefined -Wall -Wextra -Wpedantic -Werror=return-type", + "CMAKE_CXX_FLAGS_INIT": "-fsanitize=undefined -Wall -Wextra -Wpedantic -Werror" } }, { "name": "ninja-asan", + "displayName": "Ninja ASan", "description": "ASan build configuration using Ninja Multi-config", "inherits": "default", "binaryDir": "${sourceDir}/out/asan", "cacheVariables": { - "CMAKE_C_FLAGS": "-fsanitize=address", - "CMAKE_CXX_FLAGS": "-fsanitize=address" + "CMAKE_CXX_FLAGS_DEBUG_INIT": "-fsanitize=address -Wall -Wextra -Wpedantic -Werror=return-type", + "CMAKE_CXX_FLAGS_INIT": "-fsanitize=address -Wall -Wextra -Wpedantic -Werror" + } + }, + { + "name": "ninja-msvc-asan", + "displayName": "Ninja MSVC ASan", + "description": "ASan build configuration using Ninja Multi-config", + "inherits": "base-msvc", + "binaryDir": "${sourceDir}/out/asan", + "cacheVariables": { + "CMAKE_CXX_FLAGS_INIT": "-fsanitize=address" } }, { "name": "ninja-tsan", + "displayName": "Ninja TSan", "description": "TSan build configuration using Ninja Multi-config", "inherits": "default", "binaryDir": "${sourceDir}/out/tsan", "cacheVariables": { - "CMAKE_C_FLAGS": "-fsanitize=thread", - "CMAKE_CXX_FLAGS": "-fsanitize=thread" + "CMAKE_CXX_FLAGS_INIT": "-fsanitize=thread -Wall -Wextra -Wpedantic -Werror=return-type" } }, { "name": "vs22", + "displayName": "Visual Studio 2022", "description": "Build configuration using Visual Studio 17 (2022)", "generator": "Visual Studio 17 2022", "binaryDir": "${sourceDir}/out/vs", + "cacheVariables": { + "CMAKE_CXX_FLAGS_INIT": "/WX" + }, "architecture": { "value": "x64", "strategy": "external" @@ -82,6 +148,36 @@ "configurePreset": "default", "configuration": "RelWithDebInfo" }, + { + "name": "GCC Debug", + "configurePreset": "ninja-gcc", + "configuration": "Debug" + }, + { + "name": "GCC RelWithDebInfo", + "configurePreset": "ninja-gcc", + "configuration": "RelWithDebInfo" + }, + { + "name": "Clang Debug", + "configurePreset": "ninja-clang", + "configuration": "Debug" + }, + { + "name": "Clang RelWithDebInfo", + "configurePreset": "ninja-clang", + "configuration": "RelWithDebInfo" + }, + { + "name": "MSVC Debug", + "configurePreset": "ninja-msvc", + "configuration": "Debug" + }, + { + "name": "MSVC Release", + "configurePreset": "ninja-msvc", + "configuration": "Release" + }, { "name": "UBSan Debug", "configurePreset": "ninja-ubsan", @@ -107,6 +203,42 @@ "configuration": "RelWithDebInfo", "inheritConfigureEnvironment": true }, + { + "name": "GCC Debug", + "configurePreset": "ninja-gcc", + "configuration": "Debug", + "inheritConfigureEnvironment": true + }, + { + "name": "GCC RelWithDebInfo", + "configurePreset": "ninja-gcc", + "configuration": "RelWithDebInfo", + "inheritConfigureEnvironment": true + }, + { + "name": "Clang Debug", + "configurePreset": "ninja-clang", + "configuration": "Debug", + "inheritConfigureEnvironment": true + }, + { + "name": "Clang RelWithDebInfo", + "configurePreset": "ninja-clang", + "configuration": "RelWithDebInfo", + "inheritConfigureEnvironment": true + }, + { + "name": "MSVC Debug", + "configurePreset": "ninja-msvc", + "configuration": "Debug", + "inheritConfigureEnvironment": true + }, + { + "name": "MSVC Release", + "configurePreset": "ninja-msvc", + "configuration": "Release", + "inheritConfigureEnvironment": true + }, { "name": "UBSan Debug", "configurePreset": "ninja-ubsan", diff --git a/guide/src/rendering/dynamic_rendering.md b/guide/src/rendering/dynamic_rendering.md index b913b64..a0c7e0b 100644 --- a/guide/src/rendering/dynamic_rendering.md +++ b/guide/src/rendering/dynamic_rendering.md @@ -161,7 +161,7 @@ void App::submit_and_present() { wait_semaphore_info.setSemaphore(*render_sync.draw) .setStageMask(vk::PipelineStageFlagBits2::eColorAttachmentOutput); auto signal_semaphore_info = vk::SemaphoreSubmitInfo{}; - signal_semaphore_info.setSemaphore(*render_sync.present) + signal_semaphore_info.setSemaphore(m_swapchain->get_present_semaphore()) .setStageMask(vk::PipelineStageFlagBits2::eColorAttachmentOutput); submit_info.setCommandBufferInfos(command_buffer_info) .setWaitSemaphoreInfos(wait_semaphore_info) diff --git a/guide/src/rendering/render_sync.md b/guide/src/rendering/render_sync.md index f18023d..d713863 100644 --- a/guide/src/rendering/render_sync.md +++ b/guide/src/rendering/render_sync.md @@ -17,8 +17,6 @@ Add a private `struct RenderSync` to `App`: struct RenderSync { // signaled when Swapchain image has been acquired. vk::UniqueSemaphore draw{}; - // signaled when image is ready to be presented. - vk::UniqueSemaphore present{}; // signaled with present Semaphore, waited on before next render. vk::UniqueFence drawn{}; // used to record rendering commands. @@ -68,7 +66,6 @@ void App::create_render_sync() { std::views::zip(m_render_sync, command_buffers)) { sync.command_buffer = command_buffer; sync.draw = m_device->createSemaphoreUnique({}); - sync.present = m_device->createSemaphoreUnique({}); sync.drawn = m_device->createFenceUnique(fence_create_info_v); } } diff --git a/guide/src/rendering/swapchain_loop.md b/guide/src/rendering/swapchain_loop.md index 64f7043..b540525 100644 --- a/guide/src/rendering/swapchain_loop.md +++ b/guide/src/rendering/swapchain_loop.md @@ -19,7 +19,9 @@ Additionally, the number of swapchain images can vary, whereas the engine should ## Virtual Frames -All the dynamic resources used during the rendering of a frame comprise a virtual frame. The application has a fixed number of virtual frames which it cycles through on each render pass. For synchronization, each frame will be associated with a [`vk::Fence`](https://docs.vulkan.org/spec/latest/chapters/synchronization.html#synchronization-fences) which will be waited on before rendering to it again. It will also have a pair of [`vk::Semaphore`](https://docs.vulkan.org/spec/latest/chapters/synchronization.html#synchronization-semaphores)s to synchronize the acquire, render, and present calls on the GPU (we don't need to wait for them on the CPU side / in C++). For recording commands, there will be a [`vk::CommandBuffer`](https://docs.vulkan.org/spec/latest/chapters/cmdbuffers.html) per virtual frame, where all rendering commands for that frame (including layout transitions) will be recorded. +All the dynamic resources used during the rendering of a frame comprise a virtual frame. The application has a fixed number of virtual frames which it cycles through on each render pass. For synchronization, each frame will be associated with a [`vk::Fence`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkFence.html) which will be waited on before rendering to it again. It will also have a [`vk::Semaphore`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSemaphore.html) to synchronize the acquire and render calls on the GPU (we don't need to wait for them in the code). For recording commands, there will be a [`vk::CommandBuffer`](https://docs.vulkan.org/spec/latest/chapters/cmdbuffers.html) per virtual frame, where all rendering commands for that frame (including layout transitions) will be recorded. + +Presentation will require a semaphore for synchronization too, but since the swapchain loop waits on the _drawn_ fence, which will be pre-signaled for each virtual frame on first use, present semaphores cannot be part of the virtual frame. It is possible to acquire another image and submit commands with a present semaphore that has not been signaled yet - this is invalid. Thus, these semaphores will be tied to the swapchain images (associated with their indices), and recreated with it. ## Image Layouts diff --git a/guide/src/rendering/swapchain_update.md b/guide/src/rendering/swapchain_update.md index 20191d9..5ecf7cf 100644 --- a/guide/src/rendering/swapchain_update.md +++ b/guide/src/rendering/swapchain_update.md @@ -1,5 +1,41 @@ # Swapchain Update +Add a vector of semaphores and populate them in `recreate()`: + +```cpp +void create_present_semaphores(); + +// ... +// signaled when image is ready to be presented. +std::vector m_present_semaphores{}; + +// ... +auto Swapchain::recreate(glm::ivec2 size) -> bool { + // ... + populate_images(); + create_image_views(); + // recreate present semaphores as the image count might have changed. + create_present_semaphores(); + // ... +} + +void Swapchain::create_present_semaphores() { + m_present_semaphores.clear(); + m_present_semaphores.resize(m_images.size()); + for (auto& semaphore : m_present_semaphores) { + semaphore = m_device.createSemaphoreUnique({}); + } +} +``` + +Add a function to get the present semaphore corresponding to the acquired image, this will be signaled by render command submission: + +```cpp +auto Swapchain::get_present_semaphore() const -> vk::Semaphore { + return *m_present_semaphores.at(m_image_index.value()); +} +``` + Swapchain acquire/present operations can have various results. We constrain ourselves to the following: - `eSuccess`: all good @@ -59,13 +95,15 @@ auto Swapchain::acquire_next_image(vk::Semaphore const to_signal) Similarly, present: ```cpp -auto Swapchain::present(vk::Queue const queue, vk::Semaphore const to_wait) +auto Swapchain::present(vk::Queue const queue) -> bool { auto const image_index = static_cast(m_image_index.value()); + auto const wait_semaphore = + *m_present_semaphores.at(static_cast(image_index)); auto present_info = vk::PresentInfoKHR{}; present_info.setSwapchains(*m_swapchain) .setImageIndices(image_index) - .setWaitSemaphores(to_wait); + .setWaitSemaphores(wait_semaphore); // avoid VulkanHPP ErrorOutOfDateKHR exceptions by using alternate API. auto const result = queue.presentKHR(&present_info); m_image_index.reset(); diff --git a/src/app.cpp b/src/app.cpp index e68e275..cd407bb 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -229,7 +229,6 @@ void App::create_render_sync() { std::views::zip(m_render_sync, command_buffers)) { sync.command_buffer = command_buffer; sync.draw = m_device->createSemaphoreUnique({}); - sync.present = m_device->createSemaphoreUnique({}); sync.drawn = m_device->createFenceUnique(fence_create_info_v); } } @@ -535,7 +534,7 @@ void App::submit_and_present() { wait_semaphore_info.setSemaphore(*render_sync.draw) .setStageMask(vk::PipelineStageFlagBits2::eColorAttachmentOutput); auto signal_semaphore_info = vk::SemaphoreSubmitInfo{}; - signal_semaphore_info.setSemaphore(*render_sync.present) + signal_semaphore_info.setSemaphore(m_swapchain->get_present_semaphore()) .setStageMask(vk::PipelineStageFlagBits2::eColorAttachmentOutput); submit_info.setCommandBufferInfos(command_buffer_info) .setWaitSemaphoreInfos(wait_semaphore_info) @@ -549,8 +548,7 @@ void App::submit_and_present() { // framebuffer size does not match the Swapchain image size, check it // explicitly. auto const fb_size_changed = m_framebuffer_size != m_swapchain->get_size(); - auto const out_of_date = - !m_swapchain->present(m_queue, *render_sync.present); + auto const out_of_date = !m_swapchain->present(m_queue); if (fb_size_changed || out_of_date) { m_swapchain->recreate(m_framebuffer_size); } diff --git a/src/app.hpp b/src/app.hpp index aa07613..cd2aba8 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -22,11 +22,9 @@ class App { private: struct RenderSync { - // signalled when Swapchain image has been acquired. + // signaled when Swapchain image has been acquired. vk::UniqueSemaphore draw{}; - // signalled when image is ready to be presented. - vk::UniqueSemaphore present{}; - // signalled with present Semaphore, waited on before next render. + // signaled with present Semaphore, waited on before next render. vk::UniqueFence drawn{}; // used to record rendering commands. vk::CommandBuffer command_buffer{}; diff --git a/src/swapchain.cpp b/src/swapchain.cpp index fd08af0..aa32c82 100644 --- a/src/swapchain.cpp +++ b/src/swapchain.cpp @@ -120,6 +120,8 @@ auto Swapchain::recreate(glm::ivec2 size) -> bool { populate_images(); create_image_views(); + // recreate present semaphores as the image count might have changed. + create_present_semaphores(); size = get_size(); std::println("[lvk] Swapchain [{}x{}]", size.x, size.y); @@ -155,13 +157,18 @@ auto Swapchain::base_barrier() const -> vk::ImageMemoryBarrier2 { return ret; } -auto Swapchain::present(vk::Queue const queue, vk::Semaphore const to_wait) - -> bool { +auto Swapchain::get_present_semaphore() const -> vk::Semaphore { + return *m_present_semaphores.at(m_image_index.value()); +} + +auto Swapchain::present(vk::Queue const queue) -> bool { auto const image_index = static_cast(m_image_index.value()); + auto const wait_semaphore = + *m_present_semaphores.at(static_cast(image_index)); auto present_info = vk::PresentInfoKHR{}; present_info.setSwapchains(*m_swapchain) .setImageIndices(image_index) - .setWaitSemaphores(to_wait); + .setWaitSemaphores(wait_semaphore); // avoid VulkanHPP ErrorOutOfDateKHR exceptions by using alternate API. auto const result = queue.presentKHR(&present_info); m_image_index.reset(); @@ -195,4 +202,12 @@ void Swapchain::create_image_views() { m_image_views.push_back(m_device.createImageViewUnique(image_view_ci)); } } + +void Swapchain::create_present_semaphores() { + m_present_semaphores.clear(); + m_present_semaphores.resize(m_images.size()); + for (auto& semaphore : m_present_semaphores) { + semaphore = m_device.createSemaphoreUnique({}); + } +} } // namespace lvk diff --git a/src/swapchain.hpp b/src/swapchain.hpp index 3ad5d13..5892192 100644 --- a/src/swapchain.hpp +++ b/src/swapchain.hpp @@ -26,11 +26,13 @@ class Swapchain { [[nodiscard]] auto base_barrier() const -> vk::ImageMemoryBarrier2; - [[nodiscard]] auto present(vk::Queue queue, vk::Semaphore to_wait) -> bool; + [[nodiscard]] auto get_present_semaphore() const -> vk::Semaphore; + [[nodiscard]] auto present(vk::Queue queue) -> bool; private: void populate_images(); void create_image_views(); + void create_present_semaphores(); vk::Device m_device{}; Gpu m_gpu{}; @@ -39,6 +41,8 @@ class Swapchain { vk::UniqueSwapchainKHR m_swapchain{}; std::vector m_images{}; std::vector m_image_views{}; + // signaled when image is ready to be presented. + std::vector m_present_semaphores{}; std::optional m_image_index{}; }; } // namespace lvk From 9705e628f2784c24c064f9674e98d2f0fc5105e8 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Sun, 11 May 2025 14:05:13 -0700 Subject: [PATCH 3/6] Fixup guide --- guide/src/rendering/dynamic_rendering.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/guide/src/rendering/dynamic_rendering.md b/guide/src/rendering/dynamic_rendering.md index a0c7e0b..1627e92 100644 --- a/guide/src/rendering/dynamic_rendering.md +++ b/guide/src/rendering/dynamic_rendering.md @@ -175,8 +175,7 @@ void App::submit_and_present() { // framebuffer size does not match the Swapchain image size, check it // explicitly. auto const fb_size_changed = m_framebuffer_size != m_swapchain->get_size(); - auto const out_of_date = - !m_swapchain->present(m_queue, *render_sync.present); + auto const out_of_date = !m_swapchain->present(m_queue); if (fb_size_changed || out_of_date) { m_swapchain->recreate(m_framebuffer_size); } From ef5debfe0f5499ebff5ddb635cebb9dfb71e891a Mon Sep 17 00:00:00 2001 From: MyungKun Chang <58208787+CDDing@users.noreply.github.com> Date: Mon, 12 May 2025 11:56:06 +0900 Subject: [PATCH 4/6] ko-translation revised (#27) --- .../ko-KR/src/rendering/dynamic_rendering.md | 5 +-- .../ko-KR/src/rendering/render_sync.md | 3 -- .../ko-KR/src/rendering/swapchain_loop.md | 5 ++- .../ko-KR/src/rendering/swapchain_update.md | 38 ++++++++++++++++++- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/guide/translations/ko-KR/src/rendering/dynamic_rendering.md b/guide/translations/ko-KR/src/rendering/dynamic_rendering.md index e7e5be3..8ccc18f 100644 --- a/guide/translations/ko-KR/src/rendering/dynamic_rendering.md +++ b/guide/translations/ko-KR/src/rendering/dynamic_rendering.md @@ -161,7 +161,7 @@ void App::submit_and_present() { wait_semaphore_info.setSemaphore(*render_sync.draw) .setStageMask(vk::PipelineStageFlagBits2::eColorAttachmentOutput); auto signal_semaphore_info = vk::SemaphoreSubmitInfo{}; - signal_semaphore_info.setSemaphore(*render_sync.present) + signal_semaphore_info.setSemaphore(m_swapchain->get_present_semaphore()) .setStageMask(vk::PipelineStageFlagBits2::eColorAttachmentOutput); submit_info.setCommandBufferInfos(command_buffer_info) .setWaitSemaphoreInfos(wait_semaphore_info) @@ -175,8 +175,7 @@ void App::submit_and_present() { // framebuffer size does not match the Swapchain image size, check it // explicitly. auto const fb_size_changed = m_framebuffer_size != m_swapchain->get_size(); - auto const out_of_date = - !m_swapchain->present(m_queue, *render_sync.present); + auto const out_of_date = !m_swapchain->present(m_queue); if (fb_size_changed || out_of_date) { m_swapchain->recreate(m_framebuffer_size); } diff --git a/guide/translations/ko-KR/src/rendering/render_sync.md b/guide/translations/ko-KR/src/rendering/render_sync.md index 46520ef..ff92e9e 100644 --- a/guide/translations/ko-KR/src/rendering/render_sync.md +++ b/guide/translations/ko-KR/src/rendering/render_sync.md @@ -17,8 +17,6 @@ using Buffered = std::array; struct RenderSync { // signaled when Swapchain image has been acquired. vk::UniqueSemaphore draw{}; - // signaled when image is ready to be presented. - vk::UniqueSemaphore present{}; // signaled with present Semaphore, waited on before next render. vk::UniqueFence drawn{}; // used to record rendering commands. @@ -68,7 +66,6 @@ void App::create_render_sync() { std::views::zip(m_render_sync, command_buffers)) { sync.command_buffer = command_buffer; sync.draw = m_device->createSemaphoreUnique({}); - sync.present = m_device->createSemaphoreUnique({}); sync.drawn = m_device->createFenceUnique(fence_create_info_v); } } diff --git a/guide/translations/ko-KR/src/rendering/swapchain_loop.md b/guide/translations/ko-KR/src/rendering/swapchain_loop.md index c506322..3a35fae 100644 --- a/guide/translations/ko-KR/src/rendering/swapchain_loop.md +++ b/guide/translations/ko-KR/src/rendering/swapchain_loop.md @@ -19,7 +19,10 @@ ## 가상 프레임 -프레임마다 사용되는 모든 동적 자원들은 가상 프레임에 포함됩니다. 애플리케이션은 고정된 개수의 가상 프레임을 가지고 있으며, 매 렌더 패스마다 이를 순환하며 사용합니다. 동기화를 위해 각 프레임은 이전 프레임의 렌더링이 끝날 때 까지 대기하게 만드는 [`vk::Fence`](https://docs.vulkan.org/spec/latest/chapters/synchronization.html#synchronization-fences)가 있어야 합니다. 또한 GPU에서의 이미지를 받아오고, 렌더링, 화면에 나타내는 작업을 동기화하기 위한 2개의[`vk::Semaphore`](https://docs.vulkan.org/spec/latest/chapters/synchronization.html#synchronization-semaphores)가 필요합니다(이 작업들은 CPU측에서 대기할 필요는 없습니다). 명령을 기록하기 위해 가상 프레임마다 [`vk::CommandBuffer`](https://docs.vulkan.org/spec/latest/chapters/cmdbuffers.html)를 두어 해당 프레임의 (레이아웃 전환을 포함한) 모든 렌더링 명령을 기록할 것입니다. +프레임마다 사용되는 모든 동적 자원들은 가상 프레임에 포함됩니다. 애플리케이션은 고정된 개수의 가상 프레임을 가지고 있으며, 매 렌더 패스마다 이를 순환하며 사용합니다. 동기화를 위해 각 프레임은 이전 프레임의 렌더링이 끝날 때 까지 대기하게 만드는 [`vk::Fence`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkFence.html)가 있어야 합니다. 또한 GPU에서의 이미지를 받아오는 것과 렌더링하는 작업을 동기화하기 위한 [`vk::Semaphore`](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSemaphore.html)가 필요합니다(이 작업들은 코드에서 대기할 필요는 없습니다). 명령을 기록하기 위해 가상 프레임마다 [`vk::CommandBuffer`](https://docs.vulkan.org/spec/latest/chapters/cmdbuffers.html)를 두어 해당 프레임의 (레이아웃 전환을 포함한) 모든 렌더링 명령을 기록할 것입니다. + + +화면 표시 작업에도 동기화를 위한 세마포어가 필요하지만, 스왑체인 루프는 각 가상 프레임이 처음 사용될 때 미리 시그널되는 drawn 펜스를 기준으로 대기하기 때문에, 표시용 세마포어는 가상 프레임의 일부가 될 수 없습니다. 아직 시그널되지 않은 표시용 세마포어를 사용하여 이미지를 가져오고 커맨드를 제출하는 것도 가능하지만, 이는 유효하지 않은 동작입니다. 따라서 이러한 세마포어는 스왑체인 이미지(인덱스)와 연결되며, 스왑체인이 재생성될 때 함께 재생성됩니다. ## 이미지 레이아웃 diff --git a/guide/translations/ko-KR/src/rendering/swapchain_update.md b/guide/translations/ko-KR/src/rendering/swapchain_update.md index 8455293..9c6d2ec 100644 --- a/guide/translations/ko-KR/src/rendering/swapchain_update.md +++ b/guide/translations/ko-KR/src/rendering/swapchain_update.md @@ -1,6 +1,42 @@ # 스왑체인 업데이트 -스왑체인에서 이미지를 받아오고 표시하는 작업은 다양한 결과를 반환할 수 있습니다. 우리는 다음과 같은 경우에 한정하여 처리합니다. +세마포어 vector를 추가하고 `recreate()`함수를 통해 이를 할당합니다. + +```cpp +void create_present_semaphores(); + +// ... +// signaled when image is ready to be presented. +std::vector m_present_semaphores{}; + +// ... +auto Swapchain::recreate(glm::ivec2 size) -> bool { + // ... + populate_images(); + create_image_views(); + // recreate present semaphores as the image count might have changed. + create_present_semaphores(); + // ... +} + +void Swapchain::create_present_semaphores() { + m_present_semaphores.clear(); + m_present_semaphores.resize(m_images.size()); + for (auto& semaphore : m_present_semaphores) { + semaphore = m_device.createSemaphoreUnique({}); + } +} +``` + +가져온 이미지에 대응되는 표시용 세마포어를 가져오는 함수를 추가합니다. 이는 렌더링 커맨드 버퍼가 제출될 때 시그널 됩니다. + +```cpp +auto Swapchain::get_present_semaphore() const -> vk::Semaphore { + return *m_present_semaphores.at(m_image_index.value()); +} +``` + +스왑체인에서 이미지를 받아오고 표시하는 작업은 다양한 결과를 반환할 수 있습니다. 우리는 다음과 같은 경우로 한정하여 처리하겠습니다. - `eSuccess` : 문제가 없습니다. - `eSuboptimalKHR` : 역시 문제가 없습니다(에러는 아니며, 데스크탑 환경에서는 드물게 발생합니다). From 7c0f122caad8751d2d12275305aed79ef0958abc Mon Sep 17 00:00:00 2001 From: Mes Date: Sat, 9 Aug 2025 23:00:38 +0800 Subject: [PATCH 5/6] Fix typo and remove redundant blank space (#29) * Fix typo and remove redundant blank space * Fix typo --- guide/src/descriptor_sets/descriptor_buffer.md | 2 +- guide/src/initialization/device.md | 2 +- guide/src/initialization/instance.md | 2 +- guide/src/initialization/scoped_waiter.md | 2 +- guide/translations/ko-KR/src/initialization/device.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) 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{}; From 5ea8db8c5fb2f78a2fe5a13f9108595465a48f49 Mon Sep 17 00:00:00 2001 From: Mes Date: Sat, 9 Aug 2025 19:17:45 +0800 Subject: [PATCH 6/6] Translate chapter 2 into Traditional Chinese --- guide/translations/zh-TW/src/SUMMARY.md | 8 + .../zh-TW/src/initialization/README.md | 12 + .../zh-TW/src/initialization/device.md | 66 +++++ .../zh-TW/src/initialization/glfw_window.md | 90 +++++++ .../zh-TW/src/initialization/gpu.md | 94 ++++++++ .../zh-TW/src/initialization/instance.md | 87 +++++++ .../zh-TW/src/initialization/scoped_waiter.md | 64 +++++ .../zh-TW/src/initialization/surface.md | 26 ++ .../zh-TW/src/initialization/swapchain.md | 226 ++++++++++++++++++ 9 files changed, 673 insertions(+) create mode 100644 guide/translations/zh-TW/src/initialization/README.md create mode 100644 guide/translations/zh-TW/src/initialization/device.md create mode 100644 guide/translations/zh-TW/src/initialization/glfw_window.md create mode 100644 guide/translations/zh-TW/src/initialization/gpu.md create mode 100644 guide/translations/zh-TW/src/initialization/instance.md create mode 100644 guide/translations/zh-TW/src/initialization/scoped_waiter.md create mode 100644 guide/translations/zh-TW/src/initialization/surface.md create mode 100644 guide/translations/zh-TW/src/initialization/swapchain.md 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); +} +```