├── .clang-format ├── .clang-tidy ├── .editorconfig ├── .github ├── format_check_diff.sh └── workflows │ ├── ci.yml │ ├── deploy.yml │ └── guide.yml ├── .gitignore ├── CMakeLists.txt ├── CMakePresets.json ├── LICENSE ├── README.md ├── assets ├── shader.frag └── shader.vert ├── ext ├── .gitignore ├── CMakeLists.txt ├── src.zip └── vk_mem_alloc.cpp ├── guide ├── .gitignore ├── book.toml ├── build.cmake ├── src │ ├── README.md │ ├── SUMMARY.md │ ├── dear_imgui │ │ ├── README.md │ │ ├── dear_imgui.md │ │ ├── imgui_demo.png │ │ └── imgui_integration.md │ ├── descriptor_sets │ │ ├── README.md │ │ ├── descriptor_buffer.md │ │ ├── instanced_rendering.md │ │ ├── instanced_rendering.png │ │ ├── pipeline_layout.md │ │ ├── rgby_texture.png │ │ ├── texture.md │ │ ├── view_matrix.md │ │ ├── view_matrix.png │ │ └── view_ubo.png │ ├── getting_started │ │ ├── README.md │ │ ├── class_app.md │ │ ├── high_level_loader.png │ │ ├── project_layout.md │ │ ├── validation_layers.md │ │ └── vkconfig_gui.png │ ├── initialization │ │ ├── README.md │ │ ├── device.md │ │ ├── glfw_window.md │ │ ├── gpu.md │ │ ├── instance.md │ │ ├── scoped_waiter.md │ │ ├── surface.md │ │ └── swapchain.md │ ├── memory │ │ ├── README.md │ │ ├── buffers.md │ │ ├── command_block.md │ │ ├── device_buffers.md │ │ ├── images.md │ │ ├── vbo_quad.png │ │ ├── vertex_buffer.md │ │ └── vma.md │ ├── rendering │ │ ├── README.md │ │ ├── dynamic_rendering.md │ │ ├── dynamic_rendering_red_clear.png │ │ ├── render_sync.md │ │ ├── swapchain_loop.md │ │ ├── swapchain_update.md │ │ └── wsi_engine.png │ └── shader_objects │ │ ├── README.md │ │ ├── drawing_triangle.md │ │ ├── glsl_to_spir_v.md │ │ ├── locating_assets.md │ │ ├── pipelines.md │ │ ├── shader_program.md │ │ ├── srgb_triangle.png │ │ ├── srgb_triangle_wireframe.png │ │ └── white_triangle.png ├── theme │ ├── highlight.js │ ├── index.hbs │ ├── lang_toggle.css │ └── lang_toggle.js └── translations │ ├── .gitignore │ └── ko-KR │ ├── book.toml │ └── src │ ├── README.md │ ├── SUMMARY.md │ ├── dear_imgui │ ├── README.md │ ├── dear_imgui.md │ ├── imgui_demo.png │ └── imgui_integration.md │ ├── descriptor_sets │ ├── README.md │ ├── descriptor_buffer.md │ ├── instanced_rendering.md │ ├── instanced_rendering.png │ ├── pipeline_layout.md │ ├── rgby_texture.png │ ├── texture.md │ ├── view_matrix.md │ ├── view_matrix.png │ └── view_ubo.png │ ├── getting_started │ ├── README.md │ ├── class_app.md │ ├── high_level_loader.png │ ├── project_layout.md │ ├── validation_layers.md │ └── vkconfig_gui.png │ ├── initialization │ ├── README.md │ ├── device.md │ ├── glfw_window.md │ ├── gpu.md │ ├── instance.md │ ├── scoped_waiter.md │ ├── surface.md │ └── swapchain.md │ ├── memory │ ├── README.md │ ├── buffers.md │ ├── command_block.md │ ├── device_buffers.md │ ├── images.md │ ├── vbo_quad.png │ ├── vertex_buffer.md │ └── vma.md │ ├── rendering │ ├── README.md │ ├── dynamic_rendering.md │ ├── dynamic_rendering_red_clear.png │ ├── render_sync.md │ ├── swapchain_loop.md │ ├── swapchain_update.md │ └── wsi_engine.png │ └── shader_objects │ ├── README.md │ ├── drawing_triangle.md │ ├── glsl_to_spir_v.md │ ├── locating_assets.md │ ├── pipelines.md │ ├── shader_program.md │ ├── srgb_triangle.png │ ├── srgb_triangle_wireframe.png │ └── white_triangle.png ├── scripts └── format_code.sh └── src ├── app.cpp ├── app.hpp ├── bitmap.hpp ├── command_block.cpp ├── command_block.hpp ├── dear_imgui.cpp ├── dear_imgui.hpp ├── descriptor_buffer.cpp ├── descriptor_buffer.hpp ├── glsl ├── shader.frag └── shader.vert ├── gpu.cpp ├── gpu.hpp ├── main.cpp ├── render_target.hpp ├── resource_buffering.hpp ├── scoped.hpp ├── scoped_waiter.hpp ├── shader_program.cpp ├── shader_program.hpp ├── swapchain.cpp ├── swapchain.hpp ├── texture.cpp ├── texture.hpp ├── transform.cpp ├── transform.hpp ├── vertex.hpp ├── vma.cpp ├── vma.hpp ├── window.cpp └── window.hpp /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: LLVM 4 | AlwaysBreakTemplateDeclarations: Yes 5 | BreakBeforeBraces: Attach 6 | ColumnLimit: 80 7 | SpaceAfterTemplateKeyword: true 8 | Standard: c++20 9 | TabWidth: 4 10 | IndentWidth: 4 11 | UseTab: Always 12 | AllowShortEnumsOnASingleLine: true 13 | AllowShortCaseLabelsOnASingleLine: true 14 | AllowShortFunctionsOnASingleLine: All 15 | AllowShortLambdasOnASingleLine: All 16 | AllowShortBlocksOnASingleLine: Always 17 | AllowShortIfStatementsOnASingleLine: Always 18 | AllowShortLoopsOnASingleLine: true 19 | IndentRequires: true 20 | IncludeCategories: 21 | # Headers in <> with .h extension. 22 | - Regex: '<([A-Za-z0-9\/-_])+\.h>' 23 | Priority: 10 24 | # Headers in <> with .hpp extension. 25 | - Regex: '<([A-Za-z0-9\/-_])+\.hpp>' 26 | Priority: 20 27 | # Headers in <> without extension. 28 | - Regex: '<([A-Za-z0-9\/-_])+>' 29 | Priority: 30 30 | PointerAlignment: Left 31 | QualifierAlignment: Right 32 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | Checks: 'clang-analyzer-*, 3 | concurrency-*, 4 | cppcoreguidelines-*, 5 | -cppcoreguidelines-non-private-member-variables-in-classes, 6 | -cppcoreguidelines-avoid-magic-numbers, 7 | -cppcoreguidelines-avoid-const-or-ref-data-members, 8 | misc-*, 9 | -misc-non-private-member-variables-in-classes, 10 | -misc-no-recursion, 11 | modernize-*, 12 | performance-*, 13 | portability-*, 14 | readability-*, 15 | -readability-identifier-length, 16 | -readability-magic-numbers, 17 | -readability-redundant-member-init, 18 | -readability-uppercase-literal-suffix' 19 | ... 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | insert_final_newline = true 3 | charset = utf-8 4 | indent_size = 4 5 | indent_style = tab 6 | # Optional: git will commit as lf, this will only affect local files 7 | end_of_line = lf 8 | 9 | [*.{py,md,yml,sh,cmake,json,js}] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.{py,md,yml,sh,cmake,json}.in] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [CMakeLists.txt] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.github/format_check_diff.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | [[ ! $(git --version) ]] && exit 1 4 | 5 | output=$(git diff) 6 | 7 | if [[ "$output" != "" ]]; then 8 | echo -e "One or more source files are not formatted!\n\n$output\n" 9 | echo -e "Using $(clang-format --version)\n" 10 | exit 1 11 | fi 12 | 13 | echo "All source files are formatted" 14 | exit 15 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | on: 3 | push: 4 | branches: 5 | - production 6 | - staging 7 | workflow_dispatch: 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write # To push a branch 13 | pages: write # To push to a GitHub Pages site 14 | id-token: write # To update the deployment status 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: init 20 | run: | 21 | url="https://github.com/rust-lang/mdBook/releases/download/v0.4.47/mdbook-v0.4.47-x86_64-unknown-linux-gnu.tar.gz" 22 | mkdir mdbook 23 | curl -sSL $url | tar -xz --directory=./mdbook 24 | echo `pwd`/mdbook >> $GITHUB_PATH 25 | - name: build book 26 | run: | 27 | cd guide 28 | cmake -P build.cmake || exit 1 29 | ls book 30 | - name: setup pages 31 | uses: actions/configure-pages@v4 32 | - name: upload artifact 33 | uses: actions/upload-pages-artifact@v3 34 | with: 35 | path: 'guide/book' 36 | - name: Deploy to GitHub Pages 37 | id: deployment 38 | uses: actions/deploy-pages@v4 39 | -------------------------------------------------------------------------------- /.github/workflows/guide.yml: -------------------------------------------------------------------------------- 1 | name: ci-guide 2 | on: 3 | pull_request: 4 | branches-ignore: 5 | - staging 6 | workflow_dispatch: 7 | jobs: 8 | build-book: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: init 15 | run: | 16 | url="https://github.com/rust-lang/mdBook/releases/download/v0.4.47/mdbook-v0.4.47-x86_64-unknown-linux-gnu.tar.gz" 17 | mkdir mdbook 18 | curl -sSL $url | tar -xz --directory=./mdbook 19 | echo `pwd`/mdbook >> $GITHUB_PATH 20 | - name: build 21 | run: | 22 | cd guide 23 | cmake -P build.cmake || exit 1 24 | ls book 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.vs/* 2 | **/.vscode/* 3 | build/* 4 | out/* 5 | .cache 6 | .DS_Store 7 | 8 | CMakeSettings.json 9 | compile_commands.json 10 | /CMakeUserPresets.json 11 | 12 | imgui.ini 13 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.24) 2 | 3 | project(learn-vk) 4 | 5 | # set C++ options 6 | set(CMAKE_CXX_STANDARD 23) 7 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 8 | set(CMAKE_CXX_EXTENSIONS OFF) 9 | 10 | # set other CMake options 11 | set(CMAKE_DEBUG_POSTFIX "-d") 12 | set(BUILD_SHARED_LIBS OFF) 13 | 14 | # add learn-vk::ext target 15 | add_subdirectory(ext) 16 | 17 | # declare executable target 18 | add_executable(${PROJECT_NAME}) 19 | 20 | # link to ext target 21 | target_link_libraries(${PROJECT_NAME} PRIVATE 22 | learn-vk::ext 23 | ) 24 | 25 | # setup precompiled header 26 | target_precompile_headers(${PROJECT_NAME} PRIVATE 27 | 28 | 29 | ) 30 | 31 | # enable including headers in 'src/' 32 | target_include_directories(${PROJECT_NAME} PRIVATE 33 | src 34 | ) 35 | 36 | # add all source files in 'src/' to target 37 | file(GLOB_RECURSE sources LIST_DIRECTORIES false "src/*.[hc]pp") 38 | target_sources(${PROJECT_NAME} PRIVATE 39 | ${sources} 40 | ) 41 | 42 | # setup compiler warnings 43 | if(CMAKE_CXX_COMPILER_ID STREQUAL Clang OR CMAKE_CXX_COMPILER_ID STREQUAL GNU) 44 | target_compile_options(${PROJECT_NAME} PRIVATE 45 | -Wall -Wextra -Wpedantic -Wconversion -Werror=return-type 46 | $<$>:-Werror> # warnings as errors if not Debug 47 | ) 48 | elseif(CMAKE_CXX_COMPILER_ID STREQUAL MSVC) 49 | target_compile_options(${PROJECT_NAME} PRIVATE 50 | $<$>:/WX> # warnings as errors if not Debug 51 | ) 52 | endif() 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Karn Kaul and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learn Vulkan 2 | 3 | [![Build status](https://github.com/cpp-gamedev/learn-vulkan/actions/workflows/ci.yml/badge.svg)](https://github.com/cpp-gamedev/learn-vulkan/actions/workflows/ci.yml) 4 | 5 | This repository hosts the [learn-vulkan](https://cpp-gamedev.github.io/learn-vulkan/) guide's C++ source code. It also hosts the sources of the [guide](./guide) itself. 6 | 7 | ## Building 8 | 9 | ### Requirements 10 | 11 | - CMake 3.24+ 12 | - C++23 compiler and standard library 13 | - [Linux] [GLFW dependencies](https://www.glfw.org/docs/latest/compile_guide.html#compile_deps_wayland) for X11 and Wayland 14 | 15 | ### Steps 16 | 17 | Standard CMake workflow. Using presets is recommended, in-source builds are not recommended. See the [CI script](.github/workflows/ci.yml) for building on the command line. 18 | 19 | ## Branches 20 | 21 | 1. `main`^: latest, stable code (builds and runs), stable history (never rewritten) 22 | 1. `production`^: guide deployment (live), stable code and history 23 | 1. `section/*`^: reflection of source at the end of corresponding section in the guide, stable code 24 | 1. `feature/*`: potential upcoming feature, shared contributions, stable history 25 | 1. others: unstable 26 | 27 | _^ rejects direct pushes (PR required)_ 28 | 29 | [Original Repository](https://github.com/cpp-gamedev/learn-vulkan) 30 | -------------------------------------------------------------------------------- /assets/shader.frag: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/assets/shader.frag -------------------------------------------------------------------------------- /assets/shader.vert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/assets/shader.vert -------------------------------------------------------------------------------- /ext/.gitignore: -------------------------------------------------------------------------------- 1 | src/ 2 | -------------------------------------------------------------------------------- /ext/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(learn-vk-ext) 2 | 3 | # extract src.zip 4 | file(ARCHIVE_EXTRACT INPUT "${CMAKE_CURRENT_SOURCE_DIR}/src.zip" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}") 5 | 6 | # add GLFW to build tree 7 | set(GLFW_INSTALL OFF) 8 | set(GLFW_BUILD_DOCS OFF) 9 | message(STATUS "[glfw]") 10 | add_subdirectory(src/glfw) 11 | add_library(glfw::glfw ALIAS glfw) 12 | 13 | # add GLM to build tree 14 | set(GLM_ENABLE_CXX_20 ON) 15 | message(STATUS "[glm]") 16 | add_subdirectory(src/glm) 17 | target_compile_definitions(glm PUBLIC 18 | GLM_FORCE_XYZW_ONLY 19 | GLM_FORCE_RADIANS 20 | GLM_FORCE_DEPTH_ZERO_TO_ONE 21 | GLM_FORCE_SILENT_WARNINGS 22 | GLM_ENABLE_EXPERIMENTAL 23 | GLM_EXT_INCLUDED 24 | ) 25 | 26 | # add Vulkan-Headers to build tree 27 | message(STATUS "[Vulkan-Headers]") 28 | add_subdirectory(src/Vulkan-Headers) 29 | 30 | # add VulkanMemoryAllocator to build tree 31 | message(STATUS "[VulkanMemoryAllocator]") 32 | add_subdirectory(src/VulkanMemoryAllocator) 33 | 34 | # setup Dear ImGui library 35 | message(STATUS "[Dear ImGui]") 36 | add_library(imgui) 37 | add_library(imgui::imgui ALIAS imgui) 38 | target_include_directories(imgui SYSTEM PUBLIC src/imgui) 39 | target_link_libraries(imgui PUBLIC 40 | glfw::glfw 41 | Vulkan::Headers 42 | ) 43 | target_compile_definitions(imgui PUBLIC 44 | VK_NO_PROTOTYPES # Dynamically load Vulkan at runtime 45 | ) 46 | target_sources(imgui PRIVATE 47 | src/imgui/imconfig.h 48 | src/imgui/imgui_demo.cpp 49 | src/imgui/imgui_draw.cpp 50 | src/imgui/imgui_internal.h 51 | src/imgui/imgui_tables.cpp 52 | src/imgui/imgui_widgets.cpp 53 | src/imgui/imgui.cpp 54 | src/imgui/imgui.h 55 | 56 | src/imgui/backends/imgui_impl_glfw.cpp 57 | src/imgui/backends/imgui_impl_glfw.h 58 | src/imgui/backends/imgui_impl_vulkan.cpp 59 | src/imgui/backends/imgui_impl_vulkan.h 60 | ) 61 | 62 | # setup vma library (source file with VMA interface) 63 | message(STATUS "[vma]") 64 | add_library(vma) 65 | add_library(vma::vma ALIAS vma) 66 | target_link_libraries(vma PUBLIC 67 | Vulkan::Headers 68 | GPUOpen::VulkanMemoryAllocator 69 | ) 70 | target_include_directories(vma SYSTEM PUBLIC 71 | src/VulkanMemoryAllocator/include 72 | ) 73 | target_compile_definitions(vma PUBLIC 74 | VMA_STATIC_VULKAN_FUNCTIONS=0 75 | VMA_DYNAMIC_VULKAN_FUNCTIONS=1 76 | ) 77 | target_sources(vma PRIVATE 78 | vk_mem_alloc.cpp 79 | ) 80 | 81 | # ignore compiler warnings 82 | target_compile_options(vma PRIVATE -w) 83 | 84 | # declare ext library target 85 | add_library(${PROJECT_NAME} INTERFACE) 86 | add_library(learn-vk::ext ALIAS ${PROJECT_NAME}) 87 | 88 | # link to all dependencies 89 | target_link_libraries(${PROJECT_NAME} INTERFACE 90 | glm::glm 91 | imgui::imgui 92 | vma::vma 93 | ) 94 | 95 | # setup preprocessor defines 96 | target_compile_definitions(${PROJECT_NAME} INTERFACE 97 | GLFW_INCLUDE_VULKAN # enable GLFW's Vulkan API 98 | ) 99 | 100 | if(CMAKE_SYSTEM_NAME STREQUAL Linux) 101 | # link to dynamic loader 102 | target_link_libraries(${PROJECT_NAME} INTERFACE dl) 103 | endif() 104 | -------------------------------------------------------------------------------- /ext/src.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/ext/src.zip -------------------------------------------------------------------------------- /ext/vk_mem_alloc.cpp: -------------------------------------------------------------------------------- 1 | #define VMA_IMPLEMENTATION 2 | 3 | #include 4 | -------------------------------------------------------------------------------- /guide/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /guide/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Karnage"] 3 | language = "en" 4 | src = "src" 5 | title = "Learn Vulkan" 6 | 7 | [output.html] 8 | theme = "theme" 9 | additional-js = ["theme/lang_toggle.js"] 10 | additional-css = ["theme/lang_toggle.css"] 11 | -------------------------------------------------------------------------------- /guide/build.cmake: -------------------------------------------------------------------------------- 1 | # Build the target languages 2 | function(BuildBook LANGUAGE SOURCE_DIR TARGET_DIR) 3 | set(LANGUAGE "${LANGUAGE}") 4 | 5 | if(NOT EXISTS "${SOURCE_DIR}/src/SUMMARY.md") 6 | message(WARNING "Skipping '${LANGUAGE}' – SUMMARY.md not found at ${SOURCE_DIR}") 7 | return() 8 | endif() 9 | 10 | if(NOT EXISTS "${SOURCE_DIR}/book.toml") 11 | message(WARNING "Skipping '${LANGUAGE}' – book.toml not found at ${SOURCE_DIR}") 12 | return() 13 | endif() 14 | 15 | message(STATUS "Building book for language: ${LANGUAGE}") 16 | execute_process( 17 | COMMAND mdbook build -d ${TARGET_DIR} 18 | WORKING_DIRECTORY ${SOURCE_DIR} 19 | COMMAND_ERROR_IS_FATAL ANY 20 | ) 21 | endfunction() 22 | 23 | # Copy the theme folder 24 | file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/theme" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/translations") 25 | 26 | BuildBook("en" "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/book") 27 | BuildBook("ko-KR" "${CMAKE_CURRENT_SOURCE_DIR}/translations/ko-KR" "${CMAKE_CURRENT_SOURCE_DIR}/book/ko-KR") 28 | -------------------------------------------------------------------------------- /guide/src/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Vulkan is known for being explicit and verbose. But the _required_ verbosity has steadily reduced with each successive version, its new features, and previous extensions being absorbed into the core API. Similarly, RAII has been a pillar of C++ since its inception, yet most existing Vulkan tutorials do not utilize it, instead choosing to "extend" the explicitness by manually cleaning up resources. 4 | 5 | To fill that gap, this guide has the following goals: 6 | 7 | - Leverage modern C++, VulkanHPP, and Vulkan 1.3 features 8 | - Focus on keeping it simple and straightforward, _not_ on performance 9 | - Develop a basic but dynamic rendering foundation 10 | 11 | To reiterate, the focus is _not on performance_, it is on a quick introduction to the current standard multi-platform graphics API while utilizing the modern paradigms and tools (at the time of writing). Even disregarding potential performance gains, Vulkan has a better and more modern design and ecosystem than OpenGL, eg: there is no global state machine, parameters are passed by filling structs with meaningful member variable names, multi-threading is largely trivial (yes, it is actually easier to do on Vulkan than OpenGL), there are a comprehensive set of validation layers to catch misuse which can be enabled without _any_ changes to application code, etc. 12 | 13 | For an in-depth Vulkan guide, the [official tutorial](https://docs.vulkan.org/tutorial/latest/00_Introduction.html) is recommended. [vkguide](https://vkguide.dev/) and the original [Vulkan Tutorial](https://vulkan-tutorial.com/) are also very popular and intensely detailed. 14 | 15 | ## Target Audience 16 | 17 | The guide is for you if you: 18 | 19 | - Understand the principles of modern C++ and its usage 20 | - Have created C++ projects using third-party libraries 21 | - Are somewhat familiar with graphics 22 | - Having done OpenGL tutorials would be ideal 23 | - Experience with frameworks like SFML / SDL is great 24 | - Don't mind if all the information you need isn't monolithically in one place (ie, this guide) 25 | 26 | Some examples of what this guide _does not_ focus on: 27 | 28 | - GPU-driven rendering 29 | - Real-time graphics from ground-up 30 | - Considerations for tiled GPUs (eg mobile devices / Android) 31 | 32 | ## Source 33 | 34 | The source code for the project (as well as this guide) is located in [this repository](https://github.com/cpp-gamedev/learn-vulkan). A `section/*` branch intends to reflect the state of the code at the end of a particular section of the guide. Bugfixes / changes are generally backported, but there may be some divergence from the current state of the code (ie, in `main`). The source of the guide itself is only up-to-date on `main`, changes are not backported. 35 | -------------------------------------------------------------------------------- /guide/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [Introduction](README.md) 4 | 5 | # Basics 6 | 7 | - [Getting Started](getting_started/README.md) 8 | - [Project Layout](getting_started/project_layout.md) 9 | - [Validation Layers](getting_started/validation_layers.md) 10 | - [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) 19 | 20 | # Hello Triangle 21 | 22 | - [Rendering](rendering/README.md) 23 | - [Swapchain Loop](rendering/swapchain_loop.md) 24 | - [Render Sync](rendering/render_sync.md) 25 | - [Swapchain Update](rendering/swapchain_update.md) 26 | - [Dynamic Rendering](rendering/dynamic_rendering.md) 27 | - [Dear ImGui](dear_imgui/README.md) 28 | - [class DearImGui](dear_imgui/dear_imgui.md) 29 | - [ImGui Integration](dear_imgui/imgui_integration.md) 30 | - [Shader Objects](shader_objects/README.md) 31 | - [Locating Assets](shader_objects/locating_assets.md) 32 | - [Shader Program](shader_objects/shader_program.md) 33 | - [GLSL to SPIR-V](shader_objects/glsl_to_spir_v.md) 34 | - [Drawing a Triangle](shader_objects/drawing_triangle.md) 35 | - [Graphics Pipelines](shader_objects/pipelines.md) 36 | 37 | # Shader Resources 38 | 39 | - [Memory Allocation](memory/README.md) 40 | - [Vulkan Memory Allocator](memory/vma.md) 41 | - [Buffers](memory/buffers.md) 42 | - [Vertex Buffer](memory/vertex_buffer.md) 43 | - [Command Block](memory/command_block.md) 44 | - [Device Buffers](memory/device_buffers.md) 45 | - [Images](memory/images.md) 46 | - [Descriptor Sets](descriptor_sets/README.md) 47 | - [Pipeline Layout](descriptor_sets/pipeline_layout.md) 48 | - [Descriptor Buffer](descriptor_sets/descriptor_buffer.md) 49 | - [Texture](descriptor_sets/texture.md) 50 | - [View Matrix](descriptor_sets/view_matrix.md) 51 | - [Instanced Rendering](descriptor_sets/instanced_rendering.md) 52 | -------------------------------------------------------------------------------- /guide/src/dear_imgui/README.md: -------------------------------------------------------------------------------- 1 | # Dear ImGui 2 | 3 | Dear ImGui does not have native CMake support, and while adding the sources to the executable is an option, we will add it as an external library target: `imgui` to isolate it (and compile warnings etc) from our own code. This requires some changes to the `ext` target structure, since `imgui` will itself need to link to GLFW and Vulkan-Headers, have `VK_NO_PROTOTYPES` defined, etc. `learn-vk-ext` then links to `imgui` and any other libraries (currently only `glm`). We are using Dear ImGui v1.91.9, which has decent support for Dynamic Rendering. 4 | -------------------------------------------------------------------------------- /guide/src/dear_imgui/imgui_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/src/dear_imgui/imgui_demo.png -------------------------------------------------------------------------------- /guide/src/dear_imgui/imgui_integration.md: -------------------------------------------------------------------------------- 1 | # ImGui Integration 2 | 3 | Update `Swapchain` to expose its image format: 4 | 5 | ```cpp 6 | [[nodiscard]] auto get_format() const -> vk::Format { 7 | return m_ci.imageFormat; 8 | } 9 | ``` 10 | 11 | `class App` can now store a `std::optional` member and add/call its create function: 12 | 13 | ```cpp 14 | void App::create_imgui() { 15 | auto const imgui_ci = DearImGui::CreateInfo{ 16 | .window = m_window.get(), 17 | .api_version = vk_version_v, 18 | .instance = *m_instance, 19 | .physical_device = m_gpu.device, 20 | .queue_family = m_gpu.queue_family, 21 | .device = *m_device, 22 | .queue = m_queue, 23 | .color_format = m_swapchain->get_format(), 24 | .samples = vk::SampleCountFlagBits::e1, 25 | }; 26 | m_imgui.emplace(imgui_ci); 27 | } 28 | ``` 29 | 30 | Start a new ImGui frame after resetting the render fence, and show the demo window: 31 | 32 | ```cpp 33 | m_device->resetFences(*render_sync.drawn); 34 | m_imgui->new_frame(); 35 | 36 | // ... 37 | command_buffer.beginRendering(rendering_info); 38 | ImGui::ShowDemoWindow(); 39 | // draw stuff here. 40 | command_buffer.endRendering(); 41 | ``` 42 | 43 | ImGui doesn't draw anything here (the actual draw command requires the Command Buffer), it's just a good customization point for all higher level logic. 44 | 45 | We use a separate render pass for Dear ImGui, again for isolation, and to enable us to change the main render pass later, eg by adding a depth buffer attachment (`DearImGui` is setup assuming its render pass will only use a single color attachment). 46 | 47 | ```cpp 48 | m_imgui->end_frame(); 49 | // we don't want to clear the image again, instead load it intact after the 50 | // previous pass. 51 | color_attachment.setLoadOp(vk::AttachmentLoadOp::eLoad); 52 | rendering_info.setColorAttachments(color_attachment) 53 | .setPDepthAttachment(nullptr); 54 | command_buffer.beginRendering(rendering_info); 55 | m_imgui->render(command_buffer); 56 | command_buffer.endRendering(); 57 | ``` 58 | 59 | ![ImGui Demo](./imgui_demo.png) 60 | -------------------------------------------------------------------------------- /guide/src/descriptor_sets/README.md: -------------------------------------------------------------------------------- 1 | # Descriptor Sets 2 | 3 | [Vulkan Descriptor](https://docs.vulkan.org/guide/latest/mapping_data_to_shaders.html#descriptors)s are essentially typed pointers to resources that shaders can use, eg uniform/storage buffers or combined image samplers (textures with samplers). A Descriptor Set is a collection of descriptors at various **bindings** that is bound together as an atomic unit. Shaders can declare input based on these set and binding numbers, and any sets the shader uses must have been updated and bound before drawing. A Descriptor Set Layout is a description of a collection of descriptor sets associated with a particular set number, usually describing all the sets in a shader. Descriptor sets are allocated using a Descriptor Pool and the desired set layout(s). 4 | 5 | Structuring set layouts and managing descriptor sets are complex topics with many viable approaches, each with their pros and cons. Some robust ones are described in this [page](https://docs.vulkan.org/samples/latest/samples/performance/descriptor_management/README.html). 2D frameworks - and even simple/basic 3D ones - can simply allocate and update sets every frame, as described in the docs as the "simplest approach". Here's an [extremely detailed](https://zeux.io/2020/02/27/writing-an-efficient-vulkan-renderer/) - albeit a bit dated now - post by Arseny on the subject. A more modern approach, namely "bindless" or Descriptor Indexing, is described in the official docs [here](https://docs.vulkan.org/samples/latest/samples/extensions/descriptor_indexing/README.html). 6 | -------------------------------------------------------------------------------- /guide/src/descriptor_sets/instanced_rendering.md: -------------------------------------------------------------------------------- 1 | # Instanced Rendering 2 | 3 | When multiple copies of a drawable object are desired, one option is to use instanced rendering. The basic idea is to store per-instance data in a uniform/storage buffer and index into it in the vertex shader. We shall represent one model matrix per instance, feel free to add more data like an overall tint (color) that gets multiplied to the existing output color in the fragment shader. This will be bound to a Storage Buffer (SSBO), which can be "unbounded" in the shader (size is determined during invocation). 4 | 5 | Store the SSBO and a buffer for instance matrices: 6 | 7 | ```cpp 8 | std::vector m_instance_data{}; // model matrices. 9 | std::optional m_instance_ssbo{}; 10 | ``` 11 | 12 | Add two `Transform`s as the source of rendering instances, and a function to update the matrices: 13 | 14 | ```cpp 15 | void update_instances(); 16 | 17 | // ... 18 | std::array m_instances{}; // generates model matrices. 19 | 20 | // ... 21 | void App::update_instances() { 22 | m_instance_data.clear(); 23 | m_instance_data.reserve(m_instances.size()); 24 | for (auto const& transform : m_instances) { 25 | m_instance_data.push_back(transform.model_matrix()); 26 | } 27 | // can't use bit_cast anymore, reinterpret data as a byte array instead. 28 | auto const span = std::span{m_instance_data}; 29 | void* data = span.data(); 30 | auto const bytes = 31 | std::span{static_cast(data), span.size_bytes()}; 32 | m_instance_ssbo->write_at(m_frame_index, bytes); 33 | } 34 | ``` 35 | 36 | Update the descriptor pool to also provide storage buffers: 37 | 38 | ```cpp 39 | // ... 40 | vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2}, 41 | vk::DescriptorPoolSize{vk::DescriptorType::eStorageBuffer, 2}, 42 | ``` 43 | 44 | Add set 2 and its new binding. Such a set layout keeps each "layer" isolated: 45 | 46 | * Set 0: view / camera 47 | * Set 1: textures / material 48 | * Set 2: draw instances 49 | 50 | ```cpp 51 | static constexpr auto set_2_bindings_v = std::array{ 52 | layout_binding(1, vk::DescriptorType::eStorageBuffer), 53 | }; 54 | auto set_layout_cis = std::array{}; 55 | // ... 56 | set_layout_cis[2].setBindings(set_2_bindings_v); 57 | ``` 58 | 59 | Create the instance SSBO after the view UBO: 60 | 61 | ```cpp 62 | m_instance_ssbo.emplace(m_allocator.get(), m_gpu.queue_family, 63 | vk::BufferUsageFlagBits::eStorageBuffer); 64 | ``` 65 | 66 | Call `update_instances()` after `update_view()`: 67 | 68 | ```cpp 69 | // ... 70 | update_view(); 71 | update_instances(); 72 | ``` 73 | 74 | Extract transform inspection into a lambda and inspect each instance transform too: 75 | 76 | ```cpp 77 | static auto const inspect_transform = [](Transform& out) { 78 | ImGui::DragFloat2("position", &out.position.x); 79 | ImGui::DragFloat("rotation", &out.rotation); 80 | ImGui::DragFloat2("scale", &out.scale.x, 0.1f); 81 | }; 82 | 83 | ImGui::Separator(); 84 | if (ImGui::TreeNode("View")) { 85 | inspect_transform(m_view_transform); 86 | ImGui::TreePop(); 87 | } 88 | 89 | ImGui::Separator(); 90 | if (ImGui::TreeNode("Instances")) { 91 | for (std::size_t i = 0; i < m_instances.size(); ++i) { 92 | auto const label = std::to_string(i); 93 | if (ImGui::TreeNode(label.c_str())) { 94 | inspect_transform(m_instances.at(i)); 95 | ImGui::TreePop(); 96 | } 97 | } 98 | ImGui::TreePop(); 99 | } 100 | ``` 101 | 102 | Add another descriptor write for the SSBO: 103 | 104 | ```cpp 105 | auto writes = std::array{}; 106 | // ... 107 | auto const set2 = descriptor_sets[2]; 108 | auto const instance_ssbo_info = 109 | m_instance_ssbo->descriptor_info_at(m_frame_index); 110 | write.setBufferInfo(instance_ssbo_info) 111 | .setDescriptorType(vk::DescriptorType::eStorageBuffer) 112 | .setDescriptorCount(1) 113 | .setDstSet(set2) 114 | .setDstBinding(0); 115 | writes[2] = write; 116 | ``` 117 | 118 | Finally, change the instance count in the draw call: 119 | 120 | ```cpp 121 | auto const instances = static_cast(m_instances.size()); 122 | // m_vbo has 6 indices. 123 | command_buffer.drawIndexed(6, instances, 0, 0, 0); 124 | ``` 125 | 126 | Update the vertex shader to incorporate the instance model matrix: 127 | 128 | ```glsl 129 | // ... 130 | layout (set = 1, binding = 1) readonly buffer Instances { 131 | mat4 mat_ms[]; 132 | }; 133 | 134 | // ... 135 | const mat4 mat_m = mat_ms[gl_InstanceIndex]; 136 | const vec4 world_pos = mat_m * vec4(a_pos, 0.0, 1.0); 137 | ``` 138 | 139 | ![Instanced Rendering](./instanced_rendering.png) 140 | -------------------------------------------------------------------------------- /guide/src/descriptor_sets/instanced_rendering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/src/descriptor_sets/instanced_rendering.png -------------------------------------------------------------------------------- /guide/src/descriptor_sets/pipeline_layout.md: -------------------------------------------------------------------------------- 1 | # Pipeline Layout 2 | 3 | A [Vulkan Pipeline Layout](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineLayout.html) represents a sequence of descriptor sets (and push constants) associated with a shader program. Even when using Shader Objects, a Pipeline Layout is needed to utilize descriptor sets. 4 | 5 | Starting with the layout of a single descriptor set containing a uniform buffer to set the view/projection matrices in, store a descriptor pool in `App` and create it before the shader: 6 | 7 | ```cpp 8 | vk::UniqueDescriptorPool m_descriptor_pool{}; 9 | 10 | // ... 11 | void App::create_descriptor_pool() { 12 | static constexpr auto pool_sizes_v = std::array{ 13 | // 2 uniform buffers, can be more if desired. 14 | vk::DescriptorPoolSize{vk::DescriptorType::eUniformBuffer, 2}, 15 | }; 16 | auto pool_ci = vk::DescriptorPoolCreateInfo{}; 17 | // allow 16 sets to be allocated from this pool. 18 | pool_ci.setPoolSizes(pool_sizes_v).setMaxSets(16); 19 | m_descriptor_pool = m_device->createDescriptorPoolUnique(pool_ci); 20 | } 21 | ``` 22 | 23 | Add new members to `App` to store the set layouts and pipeline layout. `m_set_layout_views` is just a copy of the descriptor set layout handles in a contiguous vector: 24 | 25 | ```cpp 26 | std::vector m_set_layouts{}; 27 | std::vector m_set_layout_views{}; 28 | vk::UniquePipelineLayout m_pipeline_layout{}; 29 | 30 | // ... 31 | constexpr auto layout_binding(std::uint32_t binding, 32 | vk::DescriptorType const type) { 33 | return vk::DescriptorSetLayoutBinding{ 34 | binding, type, 1, vk::ShaderStageFlagBits::eAllGraphics}; 35 | } 36 | 37 | // ... 38 | void App::create_pipeline_layout() { 39 | static constexpr auto set_0_bindings_v = std::array{ 40 | layout_binding(0, vk::DescriptorType::eUniformBuffer), 41 | }; 42 | auto set_layout_cis = std::array{}; 43 | set_layout_cis[0].setBindings(set_0_bindings_v); 44 | 45 | for (auto const& set_layout_ci : set_layout_cis) { 46 | m_set_layouts.push_back( 47 | m_device->createDescriptorSetLayoutUnique(set_layout_ci)); 48 | m_set_layout_views.push_back(*m_set_layouts.back()); 49 | } 50 | 51 | auto pipeline_layout_ci = vk::PipelineLayoutCreateInfo{}; 52 | pipeline_layout_ci.setSetLayouts(m_set_layout_views); 53 | m_pipeline_layout = 54 | m_device->createPipelineLayoutUnique(pipeline_layout_ci); 55 | } 56 | ``` 57 | 58 | Add a helper function that allocates a set of descriptor sets for the entire layout: 59 | 60 | ```cpp 61 | auto App::allocate_sets() const -> std::vector { 62 | auto allocate_info = vk::DescriptorSetAllocateInfo{}; 63 | allocate_info.setDescriptorPool(*m_descriptor_pool) 64 | .setSetLayouts(m_set_layout_views); 65 | return m_device->allocateDescriptorSets(allocate_info); 66 | } 67 | ``` 68 | 69 | Store a Buffered copy of descriptor sets for one drawable object: 70 | 71 | ```cpp 72 | Buffered> m_descriptor_sets{}; 73 | 74 | // ... 75 | 76 | void App::create_descriptor_sets() { 77 | for (auto& descriptor_sets : m_descriptor_sets) { 78 | descriptor_sets = allocate_sets(); 79 | } 80 | } 81 | ``` 82 | -------------------------------------------------------------------------------- /guide/src/descriptor_sets/rgby_texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/src/descriptor_sets/rgby_texture.png -------------------------------------------------------------------------------- /guide/src/descriptor_sets/view_matrix.md: -------------------------------------------------------------------------------- 1 | # View Matrix 2 | 3 | Integrating the view matrix will be quite simple and short. First, transformations for objects and cameras/views can be encapsulated into a single struct: 4 | 5 | ```cpp 6 | struct Transform { 7 | glm::vec2 position{}; 8 | float rotation{}; 9 | glm::vec2 scale{1.0f}; 10 | 11 | [[nodiscard]] auto model_matrix() const -> glm::mat4; 12 | [[nodiscard]] auto view_matrix() const -> glm::mat4; 13 | }; 14 | ``` 15 | 16 | Extracting the common logic into a helper, both member functions can be implemented easily: 17 | 18 | ```cpp 19 | namespace { 20 | struct Matrices { 21 | glm::mat4 translation; 22 | glm::mat4 orientation; 23 | glm::mat4 scale; 24 | }; 25 | 26 | [[nodiscard]] auto to_matrices(glm::vec2 const position, float rotation, 27 | glm::vec2 const scale) -> Matrices { 28 | static constexpr auto mat_v = glm::identity(); 29 | static constexpr auto axis_v = glm::vec3{0.0f, 0.0f, 1.0f}; 30 | return Matrices{ 31 | .translation = glm::translate(mat_v, glm::vec3{position, 0.0f}), 32 | .orientation = glm::rotate(mat_v, glm::radians(rotation), axis_v), 33 | .scale = glm::scale(mat_v, glm::vec3{scale, 1.0f}), 34 | }; 35 | } 36 | } // namespace 37 | 38 | auto Transform::model_matrix() const -> glm::mat4 { 39 | auto const [t, r, s] = to_matrices(position, rotation, scale); 40 | // right to left: scale first, then rotate, then translate. 41 | return t * r * s; 42 | } 43 | 44 | auto Transform::view_matrix() const -> glm::mat4 { 45 | // view matrix is the inverse of the model matrix. 46 | // instead, perform translation and rotation in reverse order and with 47 | // negative values. or, use glm::lookAt(). 48 | // scale is kept unchanged as the first transformation for 49 | // "intuitive" scaling on cameras. 50 | auto const [t, r, s] = to_matrices(-position, -rotation, scale); 51 | return r * t * s; 52 | } 53 | ``` 54 | 55 | Add a `Transform` member to `App` to represent the view/camera, inspect its members, and combine with the existing projection matrix: 56 | 57 | ```cpp 58 | Transform m_view_transform{}; // generates view matrix. 59 | 60 | // ... 61 | ImGui::Separator(); 62 | if (ImGui::TreeNode("View")) { 63 | ImGui::DragFloat2("position", &m_view_transform.position.x); 64 | ImGui::DragFloat("rotation", &m_view_transform.rotation); 65 | ImGui::DragFloat2("scale", &m_view_transform.scale.x); 66 | ImGui::TreePop(); 67 | } 68 | 69 | // ... 70 | auto const mat_view = m_view_transform.view_matrix(); 71 | auto const mat_vp = mat_projection * mat_view; 72 | auto const bytes = 73 | std::bit_cast>(mat_vp); 74 | m_view_ubo->write_at(m_frame_index, bytes); 75 | ``` 76 | 77 | Naturally, moving the view left moves everything else - currently only a single RGBY quad - to the _right_. 78 | 79 | ![View Matrix](./view_matrix.png) 80 | -------------------------------------------------------------------------------- /guide/src/descriptor_sets/view_matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/src/descriptor_sets/view_matrix.png -------------------------------------------------------------------------------- /guide/src/descriptor_sets/view_ubo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/src/descriptor_sets/view_ubo.png -------------------------------------------------------------------------------- /guide/src/getting_started/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Vulkan is platform agnostic, which is one of the main reasons for its verbosity: it has to account for a wide range of implementations in its API. We shall be constraining our approach to Windows and Linux (x64 or aarch64), and focusing on discrete GPUs, enabing us to sidestep quite a bit of that verbosity. Vulkan 1.3 is widely supported by the target desktop platforms and reasonably recent graphics cards. 4 | 5 | > This doesn't mean that eg an integrated graphics chip will not be supported, it will just not be particularly designed/optimized for. 6 | 7 | ## Technical Requirements 8 | 9 | 1. Vulkan 1.3+ capable GPU and loader 10 | 1. [Vulkan 1.3+ SDK](https://vulkan.lunarg.com/sdk/home) 11 | 1. This is required for validation layers, a critical component/tool to use when developing Vulkan applications. The project itself does not use the SDK. 12 | 1. Always using the latest SDK is recommended (1.4.x at the time of writing). 13 | 1. Desktop operating system that natively supports Vulkan 14 | 1. Windows and/or Linux (distros that use repos with recent packages) is recommended. 15 | 1. MacOS does _not_ natively support Vulkan. It _can_ be used through MoltenVk, but at the time of writing MoltenVk does not fully support Vulkan 1.3, so if you decide to take this route, you may face some roadblocks. 16 | 1. C++23 compiler and standard library 17 | 1. GCC14+, Clang18+, and/or latest MSVC are recommended. MinGW/MSYS is _not_ recommended. 18 | 1. Using C++20 with replacements for C++23 specific features is possible. Eg replace `std::print()` with `fmt::print()`, add `()` to lambdas, etc. 19 | 1. CMake 3.24+ 20 | 21 | ## Overview 22 | 23 | While support for C++ modules is steadily growing, tooling is not yet ready on all platforms/IDEs we want to target, so we will unfortuntely still be using headers. This might change in the near future, followed by a refactor of this guide. 24 | 25 | The project uses a "Build the World" approach, enabling usage of sanitizers, reproducible builds on any supported platform, and requiring minimum pre-installed things on target machines. Feel free to use pre-built binaries instead, it doesn't change anything about how you would use Vulkan. 26 | 27 | ## Dependencies 28 | 29 | 1. [GLFW](https://github.com/glfw/glfw) for windowing, input, and Surface creation 30 | 1. [VulkanHPP](https://github.com/KhronosGroup/Vulkan-Hpp) (via [Vulkan-Headers](https://github.com/KhronosGroup/Vulkan-Headers)) for interacting with Vulkan 31 | 1. While Vulkan is a C API, it offers an official C++ wrapper library with many quality-of-life features. This guide almost exclusively uses that, except at the boundaries of other C libraries that themselves use the C API (eg GLFW and VMA). 32 | 1. [Vulkan Memory Allocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator/) for dealing with Vulkan memory heaps 33 | 1. [GLM](https://github.com/g-truc/glm) for GLSL-like linear algebra in C++ 34 | 1. [Dear ImGui](https://github.com/ocornut/imgui) for UI 35 | -------------------------------------------------------------------------------- /guide/src/getting_started/class_app.md: -------------------------------------------------------------------------------- 1 | # Application 2 | 3 | `class App` will serve as the owner and driver of the entire application. While there will only be one instance, using a class enables us to leverage RAII and destroy all its resources automatically and in the correct order, and avoids the need for globals. 4 | 5 | ```cpp 6 | // app.hpp 7 | namespace lvk { 8 | class App { 9 | public: 10 | void run(); 11 | }; 12 | } // namespace lvk 13 | 14 | // app.cpp 15 | namespace lvk { 16 | void App::run() { 17 | // TODO 18 | } 19 | } // namespace lvk 20 | ``` 21 | 22 | ## Main 23 | 24 | `main.cpp` will not do much: it's mainly responsible for transferring control to the actual entry point, and catching fatal exceptions. 25 | 26 | ```cpp 27 | // main.cpp 28 | auto main() -> int { 29 | try { 30 | lvk::App{}.run(); 31 | } catch (std::exception const& e) { 32 | std::println(stderr, "PANIC: {}", e.what()); 33 | return EXIT_FAILURE; 34 | } catch (...) { 35 | std::println("PANIC!"); 36 | return EXIT_FAILURE; 37 | } 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /guide/src/getting_started/high_level_loader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/src/getting_started/high_level_loader.png -------------------------------------------------------------------------------- /guide/src/getting_started/project_layout.md: -------------------------------------------------------------------------------- 1 | # Project Layout 2 | 3 | This page describes the layout used by the code in this guide. Everything here is just an opinionated option used by the guide, and is not related to Vulkan usage. 4 | 5 | External dependencies are stuffed into a zip file that's decompressed by CMake during the configure stage. Using FetchContent is a viable alternative. 6 | 7 | `Ninja Multi-Config` is the assumed generator used, regardless of OS/compiler. This is set up in a `CMakePresets.json` file in the project root. Additional custom presets can be added via `CMakeUserPresets.json`. 8 | 9 | > On Windows, Visual Studio CMake Mode uses this generator and automatically loads presets. With Visual Studio Code, the CMake Tools extension automatically uses presets. For other IDEs, refer to their documentation on using CMake presets. 10 | 11 | **Filesystem** 12 | 13 | ``` 14 | . 15 | |-- CMakeLists.txt <== executable target 16 | |-- CMakePresets.json 17 | |-- [other project files] 18 | |-- ext/ 19 | │ |-- CMakeLists.txt <== external dependencies target 20 | |-- src/ 21 | |-- [sources and headers] 22 | ``` -------------------------------------------------------------------------------- /guide/src/getting_started/validation_layers.md: -------------------------------------------------------------------------------- 1 | # Validation Layers 2 | 3 | The area of Vulkan that apps interact with: the loader, is very powerful and flexible. Read more about it [here](https://github.com/KhronosGroup/Vulkan-Loader/blob/main/docs/LoaderInterfaceArchitecture.md). Its design enables it to chain API calls through configurable **layers**, eg for overlays, and most importantly for us: [Validation Layers](https://github.com/KhronosGroup/Vulkan-ValidationLayers/blob/main/docs/README.md). 4 | 5 | ![Vulkan Loader](high_level_loader.png) 6 | 7 | As [suggested](https://github.com/KhronosGroup/Vulkan-ValidationLayers/blob/main/docs/khronos_validation_layer.md#vkconfig) by the Khronos Group, the guide strongly recommends using [Vulkan Configurator (GUI)](https://github.com/LunarG/VulkanTools/tree/main/vkconfig_gui) for validation layers. It is included in the Vulkan SDK, just keep it running while developing Vulkan applications, and ensure it is setup to inject validation layers into all detected applications, with Synchronization Validation enabled. This approach provides a lot of flexibility at runtime, including the ability to have VkConfig break the debugger on encountering an error, and also eliminates the need for validation layer specific code in the applications. 8 | 9 | > Note: modify your development (or desktop) environment's `PATH` (or use `LD_LIBRARY_PATH` on supported systems) to make sure the SDK's binaries (shared libraries) are visible first. 10 | 11 | ![Vulkan Configurator](./vkconfig_gui.png) 12 | -------------------------------------------------------------------------------- /guide/src/getting_started/vkconfig_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/src/getting_started/vkconfig_gui.png -------------------------------------------------------------------------------- /guide/src/initialization/README.md: -------------------------------------------------------------------------------- 1 | # Initialization 2 | 3 | This section deals with initialization of all the systems needed, including: 4 | 5 | - Initializing GLFW and creating a Window 6 | - Creating a Vulkan Instance 7 | - Creating a Vulkan Surface 8 | - Selecting a Vulkan Physical Device 9 | - Creating a Vulkan logical Device 10 | - Creating a Vulkan Swapchain 11 | 12 | If any step here fails, it is a fatal error as we can't do anything meaningful beyond that point. 13 | -------------------------------------------------------------------------------- /guide/src/initialization/device.md: -------------------------------------------------------------------------------- 1 | # Vulkan Device 2 | 3 | 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). 4 | 5 | Setup a `vk::QueueCreateInfo` object: 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 | Setup the core device features: 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 | Setup the additional features, using `setPNext()` to chain them: 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 | Setup a `vk::DeviceCreateInfo` object: 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 | Declare a `vk::UniqueDevice` member after `m_gpu`, create it, and initialize the dispatcher against it: 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 | Declare a `vk::Queue` member (order doesn't matter since it's just a handle, the actual Queue is owned by the Device) and initialize it: 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 | ``` 67 | -------------------------------------------------------------------------------- /guide/src/initialization/glfw_window.md: -------------------------------------------------------------------------------- 1 | # GLFW Window 2 | 3 | We will use GLFW (3.4) for windowing and related events. The library - like all external dependencies - is configured and added to the build tree in `ext/CMakeLists.txt`. `GLFW_INCLUDE_VULKAN` is defined for all consumers, to enable GLFW's Vulkan related functions (known as **Window System Integration (WSI)**). GLFW 3.4 supports Wayland on Linux, and by default it builds backends for both X11 and Wayland. For this reason it will need the development packages for [both platforms](https://www.glfw.org/docs/latest/compile_guide.html#compile_deps_wayland) (and some other Wayland/CMake dependencies) to configure/build successfully. A particular backend can be requested at runtime if desired via `GLFW_PLATFORM`. 4 | 5 | Although it is quite feasible to have multiple windows in a Vulkan-GLFW application, that is out of scope for this guide. For our purposes GLFW (the library) and a single window are a monolithic unit - initialized and destroyed together. This can be encapsulated in a `std::unique_ptr` with a custom deleter, especially since GLFW returns an opaque pointer (`GLFWwindow*`). 6 | 7 | ```cpp 8 | // window.hpp 9 | namespace lvk::glfw { 10 | struct Deleter { 11 | void operator()(GLFWwindow* window) const noexcept; 12 | }; 13 | 14 | using Window = std::unique_ptr; 15 | 16 | // Returns a valid Window if successful, else throws. 17 | [[nodiscard]] auto create_window(glm::ivec2 size, char const* title) -> Window; 18 | } // namespace lvk::glfw 19 | 20 | // window.cpp 21 | void Deleter::operator()(GLFWwindow* window) const noexcept { 22 | glfwDestroyWindow(window); 23 | glfwTerminate(); 24 | } 25 | ``` 26 | 27 | GLFW can create fullscreen and borderless windows, but we will stick to a standard window with decorations. Since we cannot do anything useful if we are unable to create a window, all other branches throw a fatal exception (caught in main). 28 | 29 | ```cpp 30 | auto glfw::create_window(glm::ivec2 const size, char const* title) -> Window { 31 | static auto const on_error = [](int const code, char const* description) { 32 | std::println(stderr, "[GLFW] Error {}: {}", code, description); 33 | }; 34 | glfwSetErrorCallback(on_error); 35 | if (glfwInit() != GLFW_TRUE) { 36 | throw std::runtime_error{"Failed to initialize GLFW"}; 37 | } 38 | // check for Vulkan support. 39 | if (glfwVulkanSupported() != GLFW_TRUE) { 40 | throw std::runtime_error{"Vulkan not supported"}; 41 | } 42 | auto ret = Window{}; 43 | // tell GLFW that we don't want an OpenGL context. 44 | glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); 45 | ret.reset(glfwCreateWindow(size.x, size.y, title, nullptr, nullptr)); 46 | if (!ret) { throw std::runtime_error{"Failed to create GLFW Window"}; } 47 | return ret; 48 | } 49 | ``` 50 | 51 | `App` can now store a `glfw::Window` and keep polling it in `run()` until it gets closed by the user. We will not be able to draw anything to the window for a while, but this is the first step in that journey. 52 | 53 | Declare it as a private member: 54 | 55 | ```cpp 56 | private: 57 | glfw::Window m_window{}; 58 | ``` 59 | 60 | Add some private member functions to encapsulate each operation: 61 | 62 | ```cpp 63 | void create_window(); 64 | 65 | void main_loop(); 66 | ``` 67 | 68 | Implement them and call them in `run()`: 69 | 70 | ```cpp 71 | void App::run() { 72 | create_window(); 73 | 74 | main_loop(); 75 | } 76 | 77 | void App::create_window() { 78 | m_window = glfw::create_window({1280, 720}, "Learn Vulkan"); 79 | } 80 | 81 | void App::main_loop() { 82 | while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) { 83 | glfwPollEvents(); 84 | } 85 | } 86 | ``` 87 | 88 | > On Wayland you will not even see a window yet: it is only shown _after_ the application presents a framebuffer to it. 89 | 90 | -------------------------------------------------------------------------------- /guide/src/initialization/gpu.md: -------------------------------------------------------------------------------- 1 | # Vulkan Physical Device 2 | 3 | A [Physical Device](https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#devsandqueues-physical-device-enumeration) represents a single complete implementation of Vulkan, for our intents and purposes a single GPU. (It could also be eg a software renderer like Mesa/lavapipe.) Some machines may have multiple Physical Devices available, like laptops with dual-GPUs. We need to select the one we want to use, given our constraints: 4 | 5 | 1. Vulkan 1.3 must be supported 6 | 1. Vulkan Swapchains must be supported 7 | 1. A Vulkan Queue that supports Graphics and Transfer operations must be available 8 | 1. It must be able to present to the previously created Vulkan Surface 9 | 1. (Optional) Prefer discrete GPUs 10 | 11 | We wrap the actual Physical Device and a few other useful objects into `struct Gpu`. Since it will be accompanied by a hefty utility function, we put it in its own hpp/cpp files, and move the `vk_version_v` constant to this new header: 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 | The implementation: 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(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 | Finally, add a `Gpu` member in `App` and initialize it after `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 | ``` 95 | -------------------------------------------------------------------------------- /guide/src/initialization/instance.md: -------------------------------------------------------------------------------- 1 | # Vulkan Instance 2 | 3 | Instead of linking to Vulkan (via the SDK) at build-time, we will load Vulkan at runtime. This requires a few adjustments: 4 | 5 | 1. In the CMake ext target `VK_NO_PROTOTYPES` is defined, which turns API function declarations into function pointers 6 | 1. In `app.cpp` this line is added to the global scope: `VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE` 7 | 1. Before and during initialization `VULKAN_HPP_DEFAULT_DISPATCHER.init()` is called 8 | 9 | The first thing to do in Vulkan is to create an [Instance](https://docs.vulkan.org/spec/latest/chapters/initialization.html#initialization-instances), which will enable enumeration of physical devices (GPUs) and creation of a logical device. 10 | 11 | Since we require Vulkan 1.3, store that in a constant to be easily referenced: 12 | 13 | ```cpp 14 | namespace { 15 | constexpr auto vk_version_v = VK_MAKE_VERSION(1, 3, 0); 16 | } // namespace 17 | ``` 18 | 19 | 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: 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 | We will need the WSI instance extensions, which GLFW conveniently provides for us. Add a helper function in `window.hpp/cpp`: 33 | 34 | ```cpp 35 | auto glfw::instance_extensions() -> std::span { 36 | auto count = std::uint32_t{}; 37 | auto const* extensions = glfwGetRequiredInstanceExtensions(&count); 38 | return {extensions, static_cast(count)}; 39 | } 40 | ``` 41 | 42 | Continuing with instance creation, create a `vk::ApplicationInfo` object and fill it up: 43 | 44 | ```cpp 45 | auto app_info = vk::ApplicationInfo{}; 46 | app_info.setPApplicationName("Learn Vulkan").setApiVersion(vk_version_v); 47 | ``` 48 | 49 | Create a `vk::InstanceCreateInfo` object and fill it up: 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 | Add a `vk::UniqueInstance` member _after_ `m_window`: this must be destroyed before terminating GLFW. Create it, and initialize the dispatcher against it: 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 | Make sure VkConfig is running with validation layers enabled, and debug/run the app. If "Information" level loader messages are enabled, you should see quite a bit of console output at this point: information about layers being loaded, physical devices and their ICDs being enumerated, etc. 72 | 73 | If this line or equivalent is not visible in the logs, re-check your Vulkan Configurator setup and `PATH`: 74 | 75 | ``` 76 | INFO | LAYER: Insert instance layer "VK_LAYER_KHRONOS_validation" 77 | ``` 78 | 79 | For instance, if `libVkLayer_khronos_validation.so` / `VkLayer_khronos_validation.dll` is not visible to the app / loader, you'll see a line similar to: 80 | 81 | ``` 82 | INFO | LAYER: Requested layer "VK_LAYER_KHRONOS_validation" failed to load. 83 | ``` 84 | 85 | Congratulations, you have successfully initialized a Vulkan Instance! 86 | 87 | > Wayland users: seeing the window is still a long way off, these VkConfig/validation logs are your only feedback for now. 88 | -------------------------------------------------------------------------------- /guide/src/initialization/scoped_waiter.md: -------------------------------------------------------------------------------- 1 | # Scoped Waiter 2 | 3 | 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. 4 | 5 | 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: 6 | 7 | 1. `Type` must be default constructible 8 | 1. Assumes a default constructed `Type` is equivalent to null (does not call `Deleter`) 9 | 10 | ```cpp 11 | template 12 | concept Scopeable = 13 | std::equality_comparable && std::is_default_constructible_v; 14 | 15 | template 16 | class Scoped { 17 | public: 18 | Scoped(Scoped const&) = delete; 19 | auto operator=(Scoped const&) = delete; 20 | 21 | Scoped() = default; 22 | 23 | constexpr Scoped(Scoped&& rhs) noexcept 24 | : m_t(std::exchange(rhs.m_t, Type{})) {} 25 | 26 | constexpr auto operator=(Scoped&& rhs) noexcept -> Scoped& { 27 | if (&rhs != this) { std::swap(m_t, rhs.m_t); } 28 | return *this; 29 | } 30 | 31 | explicit(false) constexpr Scoped(Type t) : m_t(std::move(t)) {} 32 | 33 | constexpr ~Scoped() { 34 | if (m_t == Type{}) { return; } 35 | Deleter{}(m_t); 36 | } 37 | 38 | [[nodiscard]] constexpr auto get() const -> Type const& { return m_t; } 39 | [[nodiscard]] constexpr auto get() -> Type& { return m_t; } 40 | 41 | private: 42 | Type m_t{}; 43 | }; 44 | ``` 45 | 46 | Don't worry if this doesn't make a lot of sense: the implementation isn't important, what it does and how to use it is what matters. 47 | 48 | A `ScopedWaiter` can now be implemented quite easily: 49 | 50 | ```cpp 51 | struct ScopedWaiterDeleter { 52 | void operator()(vk::Device const device) const noexcept { 53 | device.waitIdle(); 54 | } 55 | }; 56 | 57 | using ScopedWaiter = Scoped; 58 | ``` 59 | 60 | Add a `ScopedWaiter` member to `App` _at the end_ of its member list: this must remain at the end to be the first member that gets destroyed, thus guaranteeing the device will be idle before the destruction of any other members begins. Initialize it after creating the Device: 61 | 62 | ```cpp 63 | m_waiter = *m_device; 64 | ``` 65 | -------------------------------------------------------------------------------- /guide/src/initialization/surface.md: -------------------------------------------------------------------------------- 1 | # Vulkan Surface 2 | 3 | Being platform agnostic, Vulkan interfaces with the WSI via the [`VK_KHR_surface` extension](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_surface.html). A [Surface](https://docs.vulkan.org/guide/latest/wsi.html#_surface) enables displaying images on the window through the presentation engine. 4 | 5 | Add another helper function in `window.hpp/cpp`: 6 | 7 | ```cpp 8 | auto glfw::create_surface(GLFWwindow* window, vk::Instance const instance) 9 | -> vk::UniqueSurfaceKHR { 10 | VkSurfaceKHR ret{}; 11 | auto const result = 12 | glfwCreateWindowSurface(instance, window, nullptr, &ret); 13 | if (result != VK_SUCCESS || ret == VkSurfaceKHR{}) { 14 | throw std::runtime_error{"Failed to create Vulkan Surface"}; 15 | } 16 | return vk::UniqueSurfaceKHR{ret, instance}; 17 | } 18 | ``` 19 | 20 | Add a `vk::UniqueSurfaceKHR` member to `App` after `m_instance`, and create the surface: 21 | 22 | ```cpp 23 | void App::create_surface() { 24 | m_surface = glfw::create_surface(m_window.get(), *m_instance); 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /guide/src/memory/README.md: -------------------------------------------------------------------------------- 1 | # Memory Allocation 2 | 3 | Being an explicit API, [allocating memory](https://docs.vulkan.org/guide/latest/memory_allocation.html) in Vulkan that can be used by the device is the application's responsibility. The specifics can get quite complicated, but as recommended by the spec, we shall simply defer all that to a library: [Vulkan Memory Allocator (VMA)](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator). 4 | 5 | Vulkan exposes two kinds of objects that use such allocated memory: Buffers and Images, VMA offers transparent support for both: we just have to allocate/free buffers and images through VMA instead of the device directly. Unlike memory allocation / object construction on the CPU, there are many more parameters (than say alignment and size) to provide for the creation of buffers and images. As you might have guessed, we shall constrain ourselves to a subset that's relevant for shader resources: vertex buffers, uniform/storage buffers, and texture images. 6 | -------------------------------------------------------------------------------- /guide/src/memory/buffers.md: -------------------------------------------------------------------------------- 1 | # Buffers 2 | 3 | First add the RAII wrapper components for VMA buffers: 4 | 5 | ```cpp 6 | struct RawBuffer { 7 | [[nodiscard]] auto mapped_span() const -> std::span { 8 | return std::span{static_cast(mapped), size}; 9 | } 10 | 11 | auto operator==(RawBuffer const& rhs) const -> bool = default; 12 | 13 | VmaAllocator allocator{}; 14 | VmaAllocation allocation{}; 15 | vk::Buffer buffer{}; 16 | vk::DeviceSize size{}; 17 | void* mapped{}; 18 | }; 19 | 20 | struct BufferDeleter { 21 | void operator()(RawBuffer const& raw_buffer) const noexcept; 22 | }; 23 | 24 | // ... 25 | void BufferDeleter::operator()(RawBuffer const& raw_buffer) const noexcept { 26 | vmaDestroyBuffer(raw_buffer.allocator, raw_buffer.buffer, 27 | raw_buffer.allocation); 28 | } 29 | ``` 30 | 31 | Buffers can be backed by host (RAM) or device (VRAM) memory: the former is mappable and thus useful for data that changes every frame, latter is faster to access for the GPU but needs more complex methods to copy data to. Add the related types and a create function: 32 | 33 | ```cpp 34 | struct BufferCreateInfo { 35 | VmaAllocator allocator; 36 | vk::BufferUsageFlags usage; 37 | std::uint32_t queue_family; 38 | }; 39 | 40 | enum class BufferMemoryType : std::int8_t { Host, Device }; 41 | 42 | [[nodiscard]] auto create_buffer(BufferCreateInfo const& create_info, 43 | BufferMemoryType memory_type, 44 | vk::DeviceSize size) -> Buffer; 45 | 46 | // ... 47 | auto vma::create_buffer(BufferCreateInfo const& create_info, 48 | BufferMemoryType const memory_type, 49 | vk::DeviceSize const size) -> Buffer { 50 | if (size == 0) { 51 | std::println(stderr, "Buffer cannot be 0-sized"); 52 | return {}; 53 | } 54 | 55 | auto allocation_ci = VmaAllocationCreateInfo{}; 56 | allocation_ci.flags = 57 | VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; 58 | auto usage = create_info.usage; 59 | if (memory_type == BufferMemoryType::Device) { 60 | allocation_ci.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; 61 | // device buffers need to support TransferDst. 62 | usage |= vk::BufferUsageFlagBits::eTransferDst; 63 | } else { 64 | allocation_ci.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST; 65 | // host buffers can provide mapped memory. 66 | allocation_ci.flags |= VMA_ALLOCATION_CREATE_MAPPED_BIT; 67 | } 68 | 69 | auto buffer_ci = vk::BufferCreateInfo{}; 70 | buffer_ci.setQueueFamilyIndices(create_info.queue_family) 71 | .setSize(size) 72 | .setUsage(usage); 73 | auto vma_buffer_ci = static_cast(buffer_ci); 74 | 75 | VmaAllocation allocation{}; 76 | VkBuffer buffer{}; 77 | auto allocation_info = VmaAllocationInfo{}; 78 | auto const result = 79 | vmaCreateBuffer(create_info.allocator, &vma_buffer_ci, &allocation_ci, 80 | &buffer, &allocation, &allocation_info); 81 | if (result != VK_SUCCESS) { 82 | std::println(stderr, "Failed to create VMA Buffer"); 83 | return {}; 84 | } 85 | 86 | return RawBuffer{ 87 | .allocator = create_info.allocator, 88 | .allocation = allocation, 89 | .buffer = buffer, 90 | .size = size, 91 | .mapped = allocation_info.pMappedData, 92 | }; 93 | } 94 | ``` 95 | -------------------------------------------------------------------------------- /guide/src/memory/command_block.md: -------------------------------------------------------------------------------- 1 | # Command Block 2 | 3 | Long-lived vertex buffers perform better when backed by Device memory, especially for 3D meshes. Data is transferred to device buffers in two steps: 4 | 5 | 1. Allocate a host buffer and copy the data to its mapped memory 6 | 1. Allocate a device buffer, record a Buffer Copy operation and submit it 7 | 8 | The second step requires a command buffer and queue submission (_and_ waiting for the submitted work to complete). Encapsulate this behavior into a class, it will also be used for creating images: 9 | 10 | ```cpp 11 | class CommandBlock { 12 | public: 13 | explicit CommandBlock(vk::Device device, vk::Queue queue, 14 | vk::CommandPool command_pool); 15 | 16 | [[nodiscard]] auto command_buffer() const -> vk::CommandBuffer { 17 | return *m_command_buffer; 18 | } 19 | 20 | void submit_and_wait(); 21 | 22 | private: 23 | vk::Device m_device{}; 24 | vk::Queue m_queue{}; 25 | vk::UniqueCommandBuffer m_command_buffer{}; 26 | }; 27 | ``` 28 | 29 | The constructor takes an existing command pool created for such ad-hoc allocations, and the queue for submission later. This way it can be passed around after creation and used by other code. 30 | 31 | ```cpp 32 | CommandBlock::CommandBlock(vk::Device const device, vk::Queue const queue, 33 | vk::CommandPool const command_pool) 34 | : m_device(device), m_queue(queue) { 35 | // allocate a UniqueCommandBuffer which will free the underlying command 36 | // buffer from its owning pool on destruction. 37 | auto allocate_info = vk::CommandBufferAllocateInfo{}; 38 | allocate_info.setCommandPool(command_pool) 39 | .setCommandBufferCount(1) 40 | .setLevel(vk::CommandBufferLevel::ePrimary); 41 | // all the current VulkanHPP functions for UniqueCommandBuffer allocation 42 | // return vectors. 43 | auto command_buffers = m_device.allocateCommandBuffersUnique(allocate_info); 44 | m_command_buffer = std::move(command_buffers.front()); 45 | 46 | // start recording commands before returning. 47 | auto begin_info = vk::CommandBufferBeginInfo{}; 48 | begin_info.setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit); 49 | m_command_buffer->begin(begin_info); 50 | } 51 | ``` 52 | 53 | `submit_and_wait()` resets the unique command buffer at the end, to free it from its command pool: 54 | 55 | ```cpp 56 | void CommandBlock::submit_and_wait() { 57 | if (!m_command_buffer) { return; } 58 | 59 | // end recording and submit. 60 | m_command_buffer->end(); 61 | auto submit_info = vk::SubmitInfo2KHR{}; 62 | auto const command_buffer_info = 63 | vk::CommandBufferSubmitInfo{*m_command_buffer}; 64 | submit_info.setCommandBufferInfos(command_buffer_info); 65 | auto fence = m_device.createFenceUnique({}); 66 | m_queue.submit2(submit_info, *fence); 67 | 68 | // wait for submit fence to be signaled. 69 | static constexpr auto timeout_v = 70 | static_cast(std::chrono::nanoseconds(30s).count()); 71 | auto const result = m_device.waitForFences(*fence, vk::True, timeout_v); 72 | if (result != vk::Result::eSuccess) { 73 | std::println(stderr, "Failed to submit Command Buffer"); 74 | } 75 | // free the command buffer. 76 | m_command_buffer.reset(); 77 | } 78 | ``` 79 | 80 | ## Multithreading considerations 81 | 82 | Instead of blocking the main thread on every Command Block's `submit_and_wait()`, you might be wondering if command block usage could be multithreaded. The answer is yes! But with some extra work: each thread will require its own command pool - just using one owned (unique) pool per Command Block (with no need to free the buffer) is a good starting point. All queue operations need to be synchronized, ie a critical section protected by a mutex. This includes Swapchain acquire/present calls, and Queue submissions. A `class Queue` value type that stores a copy of the `vk::Queue` and a pointer/reference to its `std::mutex` - and wraps the submit call - can be passed to command blocks. Just this much will enable asynchronous asset loading etc, as each loading thread will use its own command pool, and queue submissions all around will be critical sections. `VmaAllocator` is internally synchronized (can be disabled at build time), so performing allocations through the same allocator on multiple threads is safe. 83 | 84 | For multi-threaded rendering, use a Secondary command buffer per thread to record rendering commands, accumulate and execute them in the main (Primary) command buffer currently in `RenderSync`. This is not particularly helpful unless you have thousands of expensive draw calls and dozens of render passes, as recording even a hundred draws will likely be faster on a single thread. 85 | -------------------------------------------------------------------------------- /guide/src/memory/vbo_quad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/src/memory/vbo_quad.png -------------------------------------------------------------------------------- /guide/src/memory/vertex_buffer.md: -------------------------------------------------------------------------------- 1 | # Vertex Buffer 2 | 3 | The goal here is to move the hard-coded vertices in the shader to application code. For the time being we will use an ad-hoc Host `vma::Buffer` and focus more on the rest of the infrastructure like vertex attributes. 4 | 5 | First add a new header, `vertex.hpp`: 6 | 7 | ```cpp 8 | struct Vertex { 9 | glm::vec2 position{}; 10 | glm::vec3 color{1.0f}; 11 | }; 12 | 13 | // two vertex attributes: position at 0, color at 1. 14 | constexpr auto vertex_attributes_v = std::array{ 15 | // the format matches the type and layout of data: vec2 => 2x 32-bit floats. 16 | vk::VertexInputAttributeDescription2EXT{0, 0, vk::Format::eR32G32Sfloat, 17 | offsetof(Vertex, position)}, 18 | // vec3 => 3x 32-bit floats 19 | vk::VertexInputAttributeDescription2EXT{1, 0, vk::Format::eR32G32B32Sfloat, 20 | offsetof(Vertex, color)}, 21 | }; 22 | 23 | // one vertex binding at location 0. 24 | constexpr auto vertex_bindings_v = std::array{ 25 | // we are using interleaved data with a stride of sizeof(Vertex). 26 | vk::VertexInputBindingDescription2EXT{0, sizeof(Vertex), 27 | vk::VertexInputRate::eVertex, 1}, 28 | }; 29 | ``` 30 | 31 | Add the vertex attributes and bindings to the Shader Create Info: 32 | 33 | ```cpp 34 | // ... 35 | static constexpr auto vertex_input_v = ShaderVertexInput{ 36 | .attributes = vertex_attributes_v, 37 | .bindings = vertex_bindings_v, 38 | }; 39 | auto const shader_ci = ShaderProgram::CreateInfo{ 40 | .device = *m_device, 41 | .vertex_spirv = vertex_spirv, 42 | .fragment_spirv = fragment_spirv, 43 | .vertex_input = vertex_input_v, 44 | .set_layouts = {}, 45 | }; 46 | // ... 47 | ``` 48 | 49 | With the vertex input defined, we can update the vertex shader and recompile it: 50 | 51 | ```glsl 52 | #version 450 core 53 | 54 | layout (location = 0) in vec2 a_pos; 55 | layout (location = 1) in vec3 a_color; 56 | 57 | layout (location = 0) out vec3 out_color; 58 | 59 | void main() { 60 | const vec2 position = a_pos; 61 | 62 | out_color = a_color; 63 | gl_Position = vec4(position, 0.0, 1.0); 64 | } 65 | ``` 66 | 67 | Add a VBO (Vertex Buffer Object) member and create it: 68 | 69 | ```cpp 70 | void App::create_vertex_buffer() { 71 | // vertices moved from the shader. 72 | static constexpr auto vertices_v = std::array{ 73 | Vertex{.position = {-0.5f, -0.5f}, .color = {1.0f, 0.0f, 0.0f}}, 74 | Vertex{.position = {0.5f, -0.5f}, .color = {0.0f, 1.0f, 0.0f}}, 75 | Vertex{.position = {0.0f, 0.5f}, .color = {0.0f, 0.0f, 1.0f}}, 76 | }; 77 | 78 | // we want to write vertices_v to a Host VertexBuffer. 79 | auto const buffer_ci = vma::BufferCreateInfo{ 80 | .allocator = m_allocator.get(), 81 | .usage = vk::BufferUsageFlagBits::eVertexBuffer, 82 | .queue_family = m_gpu.queue_family, 83 | }; 84 | m_vbo = vma::create_buffer(buffer_ci, vma::BufferMemoryType::Host, 85 | sizeof(vertices_v)); 86 | 87 | // host buffers have a memory-mapped pointer available to memcpy data to. 88 | std::memcpy(m_vbo.get().mapped, vertices_v.data(), sizeof(vertices_v)); 89 | } 90 | ``` 91 | 92 | Bind the VBO before recording the draw call: 93 | 94 | ```cpp 95 | // single VBO at binding 0 at no offset. 96 | command_buffer.bindVertexBuffers(0, m_vbo->get_raw().buffer, 97 | vk::DeviceSize{}); 98 | // m_vbo has 3 vertices. 99 | command_buffer.draw(3, 1, 0, 0); 100 | ``` 101 | 102 | You should see the same triangle as before. But now we can use whatever set of vertices we like! The Primitive Topology is Triange List by default, so every three vertices in the array is drawn as a triangle, eg for 9 vertices: `[[0, 1, 2], [3, 4, 5], [6, 7, 8]]`, where each inner `[]` represents a triangle comprised of the vertices at those indices. Try playing around with customized vertices and topologies, use Render Doc to debug unexpected outputs / bugs. 103 | 104 | Host Vertex Buffers are useful for primitives that are temporary and/or frequently changing, such as UI objects. A 2D framework can use such VBOs exclusively: a simple approach would be a pool of buffers per virtual frame where for each draw a buffer is obtained from the current virtual frame's pool and vertices are copied in. 105 | -------------------------------------------------------------------------------- /guide/src/memory/vma.md: -------------------------------------------------------------------------------- 1 | # Vulkan Memory Allocator 2 | 3 | VMA has full CMake support, but it is also a single-header library that requires users to "instantiate" it in a single translation unit. Isolating that into a wrapper library to minimize warning pollution etc, we create our own `vma::vma` target that compiles this source file: 4 | 5 | ```cpp 6 | // vk_mem_alloc.cpp 7 | #define VMA_IMPLEMENTATION 8 | 9 | #include 10 | ``` 11 | 12 | Unlike VulkanHPP, VMA's interface is C only, thus we shall use our `Scoped` class template to wrap objects in RAII types. The first thing we need is a `VmaAllocator`, which is similar to a `vk::Device` or `GLFWwindow*`: 13 | 14 | ```cpp 15 | // vma.hpp 16 | namespace lvk::vma { 17 | struct Deleter { 18 | void operator()(VmaAllocator allocator) const noexcept; 19 | }; 20 | 21 | using Allocator = Scoped; 22 | 23 | [[nodiscard]] auto create_allocator(vk::Instance instance, 24 | vk::PhysicalDevice physical_device, 25 | vk::Device device) -> Allocator; 26 | } // namespace lvk::vma 27 | 28 | // vma.cpp 29 | void Deleter::operator()(VmaAllocator allocator) const noexcept { 30 | vmaDestroyAllocator(allocator); 31 | } 32 | 33 | // ... 34 | auto vma::create_allocator(vk::Instance const instance, 35 | vk::PhysicalDevice const physical_device, 36 | vk::Device const device) -> Allocator { 37 | auto const& dispatcher = VULKAN_HPP_DEFAULT_DISPATCHER; 38 | // need to zero initialize C structs, unlike VulkanHPP. 39 | auto vma_vk_funcs = VmaVulkanFunctions{}; 40 | vma_vk_funcs.vkGetInstanceProcAddr = dispatcher.vkGetInstanceProcAddr; 41 | vma_vk_funcs.vkGetDeviceProcAddr = dispatcher.vkGetDeviceProcAddr; 42 | 43 | auto allocator_ci = VmaAllocatorCreateInfo{}; 44 | allocator_ci.physicalDevice = physical_device; 45 | allocator_ci.device = device; 46 | allocator_ci.pVulkanFunctions = &vma_vk_funcs; 47 | allocator_ci.instance = instance; 48 | VmaAllocator ret{}; 49 | auto const result = vmaCreateAllocator(&allocator_ci, &ret); 50 | if (result == VK_SUCCESS) { return ret; } 51 | 52 | throw std::runtime_error{"Failed to create Vulkan Memory Allocator"}; 53 | } 54 | ``` 55 | 56 | `App` stores and creates a `vma::Allocator` object: 57 | 58 | ```cpp 59 | // ... 60 | vma::Allocator m_allocator{}; // anywhere between m_device and m_shader. 61 | 62 | // ... 63 | void App::create_allocator() { 64 | m_allocator = vma::create_allocator(*m_instance, m_gpu.device, *m_device); 65 | } 66 | ``` 67 | -------------------------------------------------------------------------------- /guide/src/rendering/README.md: -------------------------------------------------------------------------------- 1 | # Rendering 2 | 3 | This section implements Render Sync, the Swapchain loop, performs Swapchain image layout transitions, and introduces [Dynamic Rendering](https://docs.vulkan.org/samples/latest/samples/extensions/dynamic_rendering/README.html). Originally Vulkan only supported [Render Passes](https://docs.vulkan.org/tutorial/latest/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.html), which are quite verbose to setup, require somewhat confusing subpass dependencies, and are ironically _less_ explicit: they can perform implicit layout transitions on their framebuffer attachments. They are also tightly coupled to Graphics Pipelines, you need a separate pipeline object for each Render Pass, even if they are identical in all other respects. This RenderPass/Subpass model was primarily beneficial for GPUs with tiled renderers, and in Vulkan 1.3 Dynamic Rendering was promoted to the core API (previously it was an extension) as an alternative to using Render Passes. 4 | -------------------------------------------------------------------------------- /guide/src/rendering/dynamic_rendering_red_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/src/rendering/dynamic_rendering_red_clear.png -------------------------------------------------------------------------------- /guide/src/rendering/render_sync.md: -------------------------------------------------------------------------------- 1 | # Render Sync 2 | 3 | Create a new header `resource_buffering.hpp`: 4 | 5 | ```cpp 6 | // Number of virtual frames. 7 | inline constexpr std::size_t buffering_v{2}; 8 | 9 | // Alias for N-buffered resources. 10 | template 11 | using Buffered = std::array; 12 | ``` 13 | 14 | Add a private `struct RenderSync` to `App`: 15 | 16 | ```cpp 17 | struct RenderSync { 18 | // signaled when Swapchain image has been acquired. 19 | vk::UniqueSemaphore draw{}; 20 | // signaled with present Semaphore, waited on before next render. 21 | vk::UniqueFence drawn{}; 22 | // used to record rendering commands. 23 | vk::CommandBuffer command_buffer{}; 24 | }; 25 | ``` 26 | 27 | Add the new members associated with the Swapchain loop: 28 | 29 | ```cpp 30 | // command pool for all render Command Buffers. 31 | vk::UniqueCommandPool m_render_cmd_pool{}; 32 | // Sync and Command Buffer for virtual frames. 33 | Buffered m_render_sync{}; 34 | // Current virtual frame index. 35 | std::size_t m_frame_index{}; 36 | ``` 37 | 38 | Add, implement, and call the create function: 39 | 40 | ```cpp 41 | void App::create_render_sync() { 42 | // Command Buffers are 'allocated' from a Command Pool (which is 'created' 43 | // like all other Vulkan objects so far). We can allocate all the buffers 44 | // from a single pool here. 45 | auto command_pool_ci = vk::CommandPoolCreateInfo{}; 46 | // this flag enables resetting the command buffer for re-recording (unlike a 47 | // single-time submit scenario). 48 | command_pool_ci.setFlags(vk::CommandPoolCreateFlagBits::eResetCommandBuffer) 49 | .setQueueFamilyIndex(m_gpu.queue_family); 50 | m_render_cmd_pool = m_device->createCommandPoolUnique(command_pool_ci); 51 | 52 | auto command_buffer_ai = vk::CommandBufferAllocateInfo{}; 53 | command_buffer_ai.setCommandPool(*m_render_cmd_pool) 54 | .setCommandBufferCount(static_cast(resource_buffering_v)) 55 | .setLevel(vk::CommandBufferLevel::ePrimary); 56 | auto const command_buffers = 57 | m_device->allocateCommandBuffers(command_buffer_ai); 58 | assert(command_buffers.size() == m_render_sync.size()); 59 | 60 | // we create Render Fences as pre-signaled so that on the first render for 61 | // each virtual frame we don't wait on their fences (since there's nothing 62 | // to wait for yet). 63 | static constexpr auto fence_create_info_v = 64 | vk::FenceCreateInfo{vk::FenceCreateFlagBits::eSignaled}; 65 | for (auto [sync, command_buffer] : 66 | std::views::zip(m_render_sync, command_buffers)) { 67 | sync.command_buffer = command_buffer; 68 | sync.draw = m_device->createSemaphoreUnique({}); 69 | sync.drawn = m_device->createFenceUnique(fence_create_info_v); 70 | } 71 | } 72 | ``` 73 | -------------------------------------------------------------------------------- /guide/src/rendering/swapchain_loop.md: -------------------------------------------------------------------------------- 1 | # Swapchain Loop 2 | 3 | One part of rendering in the main loop is the Swapchain loop, which at a high level comprises of these steps: 4 | 5 | 1. Acquire a Swapchain Image 6 | 1. Render to the acquired Image 7 | 1. Present the Image (this releases the image back to the Swapchain) 8 | 9 | ![WSI Engine](./wsi_engine.png) 10 | 11 | There are a few nuances to deal with, for instance: 12 | 13 | 1. Acquiring (and/or presenting) will sometimes fail (eg because the Swapchain is out of date), in which case the remaining steps need to be skipped 14 | 1. The acquire command can return before the image is actually ready for use, rendering needs to be synchronized to only start after the image is ready 15 | 1. Similarly, presentation needs to be synchronized to only occur after rendering has completed 16 | 1. The images need appropriate Layout Transitions at each stage 17 | 18 | Additionally, the number of swapchain images can vary, whereas the engine should use a fixed number of _virtual frames_: 2 for double buffering, 3 for triple (more is usually overkill). More info is available [here](https://docs.vulkan.org/samples/latest/samples/performance/swapchain_images/README.html#_double_buffering_or_triple_buffering). It's also possible for the main loop to acquire the same image before a previous render command has finished (or even started), if the Swapchain is using Mailbox Present Mode. While FIFO will block until the oldest submitted image is available (also known as vsync), we should still synchronize and wait until the acquired image has finished rendering. 19 | 20 | ## Virtual Frames 21 | 22 | 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. 23 | 24 | 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. 25 | 26 | ## Image Layouts 27 | 28 | Vulkan Images have a property known as [Image Layout](https://docs.vulkan.org/spec/latest/chapters/resources.html#resources-image-layouts). Most operations on images and their subresources require them to be in certain specific layouts, requiring transitions before (and after). A layout transition conveniently also functions as a Pipeline Barrier (think memory barrier on the GPU), enabling us to synchronize operations before and after the transition. 29 | 30 | Vulkan Synchronization is arguably the most complicated aspect of the API, a good amount of research is recommended. Here is an [article explaining barriers](https://gpuopen.com/learn/vulkan-barriers-explained/). 31 | -------------------------------------------------------------------------------- /guide/src/rendering/wsi_engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/src/rendering/wsi_engine.png -------------------------------------------------------------------------------- /guide/src/shader_objects/README.md: -------------------------------------------------------------------------------- 1 | # Shader Objects 2 | 3 | A [Vulkan Graphics Pipeline](https://docs.vulkan.org/spec/latest/chapters/pipelines.html) is a large object that encompasses the entire graphics pipeline. It consists of many stages - all this happens during a single `draw()` call. There is however an extension called [`VK_EXT_shader_object`](https://www.khronos.org/blog/you-can-use-vulkan-without-pipelines-today) which enables avoiding graphics pipelines entirely. Almost all pipeline state becomes dynamic, ie set at draw time, and the only Vulkan handles to own are `ShaderEXT` objects. For a comprehensive guide, check out the [Vulkan Sample from Khronos](https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/extensions/shader_object). 4 | 5 | Vulkan requires shader code to be provided as SPIR-V (IR). We shall use `glslc` (part of the Vulkan SDK) to compile GLSL to SPIR-V manually when required. 6 | -------------------------------------------------------------------------------- /guide/src/shader_objects/drawing_triangle.md: -------------------------------------------------------------------------------- 1 | # Drawing a Triangle 2 | 3 | Add a `ShaderProgram` to `App` and its create function: 4 | 5 | ```cpp 6 | [[nodiscard]] auto asset_path(std::string_view uri) const -> fs::path; 7 | 8 | // ... 9 | void create_shader(); 10 | 11 | // ... 12 | std::optional m_shader{}; 13 | ``` 14 | 15 | Implement and call `create_shader()` (and `asset_path()`): 16 | 17 | ```cpp 18 | void App::create_shader() { 19 | auto const vertex_spirv = to_spir_v(asset_path("shader.vert")); 20 | auto const fragment_spirv = to_spir_v(asset_path("shader.frag")); 21 | auto const shader_ci = ShaderProgram::CreateInfo{ 22 | .device = *m_device, 23 | .vertex_spirv = vertex_spirv, 24 | .fragment_spirv = fragment_spirv, 25 | .vertex_input = {}, 26 | .set_layouts = {}, 27 | }; 28 | m_shader.emplace(shader_ci); 29 | } 30 | 31 | auto App::asset_path(std::string_view const uri) const -> fs::path { 32 | return m_assets_dir / uri; 33 | } 34 | ``` 35 | 36 | Before `render()` grows to an unwieldy size, extract the higher level logic into two member functions: 37 | 38 | ```cpp 39 | // ImGui code goes here. 40 | void inspect(); 41 | // Issue draw calls here. 42 | void draw(vk::CommandBuffer command_buffer) const; 43 | 44 | // ... 45 | void App::inspect() { 46 | ImGui::ShowDemoWindow(); 47 | // TODO 48 | } 49 | 50 | // ... 51 | command_buffer.beginRendering(rendering_info); 52 | inspect(); 53 | draw(command_buffer); 54 | command_buffer.endRendering(); 55 | ``` 56 | 57 | We can now bind the shader and use it to draw the triangle in the shader. Making `draw()` `const` forces us to ensure no `App` state is changed: 58 | 59 | ```cpp 60 | void App::draw(vk::CommandBuffer const command_buffer) const { 61 | m_shader->bind(command_buffer, m_framebuffer_size); 62 | // current shader has hard-coded logic for 3 vertices. 63 | command_buffer.draw(3, 1, 0, 0); 64 | } 65 | ``` 66 | 67 | ![White Triangle](./white_triangle.png) 68 | 69 | Updating the shaders to use interpolated RGB on each vertex: 70 | 71 | ```glsl 72 | // shader.vert 73 | 74 | layout (location = 0) out vec3 out_color; 75 | 76 | // ... 77 | const vec3 colors[] = { 78 | vec3(1.0, 0.0, 0.0), 79 | vec3(0.0, 1.0, 0.0), 80 | vec3(0.0, 0.0, 1.0), 81 | }; 82 | 83 | // ... 84 | out_color = colors[gl_VertexIndex]; 85 | 86 | // shader.frag 87 | 88 | layout (location = 0) in vec3 in_color; 89 | 90 | // ... 91 | out_color = vec4(in_color, 1.0); 92 | ``` 93 | 94 | > Make sure to recompile both the SPIR-V shaders in assets/. 95 | 96 | And a black clear color: 97 | 98 | ```cpp 99 | // ... 100 | .setClearValue(vk::ClearColorValue{0.0f, 0.0f, 0.0f, 1.0f}); 101 | ``` 102 | 103 | Gives us the renowned Vulkan sRGB triangle: 104 | 105 | ![sRGB Triangle](./srgb_triangle.png) 106 | 107 | ## Modifying Dynamic State 108 | 109 | We can use an ImGui window to inspect / tweak some pipeline state: 110 | 111 | ```cpp 112 | ImGui::SetNextWindowSize({200.0f, 100.0f}, ImGuiCond_Once); 113 | if (ImGui::Begin("Inspect")) { 114 | if (ImGui::Checkbox("wireframe", &m_wireframe)) { 115 | m_shader->polygon_mode = 116 | m_wireframe ? vk::PolygonMode::eLine : vk::PolygonMode::eFill; 117 | } 118 | if (m_wireframe) { 119 | auto const& line_width_range = 120 | m_gpu.properties.limits.lineWidthRange; 121 | ImGui::SetNextItemWidth(100.0f); 122 | ImGui::DragFloat("line width", &m_shader->line_width, 0.25f, 123 | line_width_range[0], line_width_range[1]); 124 | } 125 | } 126 | ImGui::End(); 127 | ``` 128 | 129 | ![sRGB Triangle (wireframe)](./srgb_triangle_wireframe.png) 130 | 131 | -------------------------------------------------------------------------------- /guide/src/shader_objects/glsl_to_spir_v.md: -------------------------------------------------------------------------------- 1 | # GLSL to SPIR-V 2 | 3 | Shaders work in NDC space: -1 to +1 for X and Y. We output a triangle's coordinates in a new vertex shader and save it to `src/glsl/shader.vert`: 4 | 5 | ```glsl 6 | #version 450 core 7 | 8 | void main() { 9 | const vec2 positions[] = { 10 | vec2(-0.5, -0.5), 11 | vec2(0.5, -0.5), 12 | vec2(0.0, 0.5), 13 | }; 14 | 15 | const vec2 position = positions[gl_VertexIndex]; 16 | 17 | gl_Position = vec4(position, 0.0, 1.0); 18 | } 19 | ``` 20 | 21 | The fragment shader just outputs white for now, in `src/glsl/shader.frag`: 22 | 23 | ```glsl 24 | #version 450 core 25 | 26 | layout (location = 0) out vec4 out_color; 27 | 28 | void main() { 29 | out_color = vec4(1.0); 30 | } 31 | ``` 32 | 33 | Compile both shaders into `assets/`: 34 | 35 | ``` 36 | glslc src/glsl/shader.vert -o assets/shader.vert 37 | glslc src/glsl/shader.frag -o assets/shader.frag 38 | ``` 39 | 40 | > glslc is part of the Vulkan SDK. 41 | 42 | ## Loading SPIR-V 43 | 44 | SPIR-V shaders are binary files with a stride/alignment of 4 bytes. As we have seen, the Vulkan API accepts a span of `std::uint32_t`s, so we need to load it into such a buffer (and _not_ `std::vector` or other 1-byte equivalents). Add a helper function in `app.cpp`: 45 | 46 | ```cpp 47 | [[nodiscard]] auto to_spir_v(fs::path const& path) 48 | -> std::vector { 49 | // open the file at the end, to get the total size. 50 | auto file = std::ifstream{path, std::ios::binary | std::ios::ate}; 51 | if (!file.is_open()) { 52 | throw std::runtime_error{ 53 | std::format("Failed to open file: '{}'", path.generic_string())}; 54 | } 55 | 56 | auto const size = file.tellg(); 57 | auto const usize = static_cast(size); 58 | // file data must be uint32 aligned. 59 | if (usize % sizeof(std::uint32_t) != 0) { 60 | throw std::runtime_error{std::format("Invalid SPIR-V size: {}", usize)}; 61 | } 62 | 63 | // seek to the beginning before reading. 64 | file.seekg({}, std::ios::beg); 65 | auto ret = std::vector{}; 66 | ret.resize(usize / sizeof(std::uint32_t)); 67 | void* data = ret.data(); 68 | file.read(static_cast(data), size); 69 | return ret; 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /guide/src/shader_objects/locating_assets.md: -------------------------------------------------------------------------------- 1 | # Locating Assets 2 | 3 | Before we can use shaders, we need to load them as asset/data files. To do that correctly, first the asset directory needs to be located. There are a few ways to go about this, we will use the approach of looking for a particular subdirectory, starting from the working directory and walking up the parent directory tree. This enables `app` in any project/build subdirectory to locate `assets/` in the various examples below: 4 | 5 | ``` 6 | . 7 | |-- assets/ 8 | |-- app 9 | |-- build/ 10 | |-- app 11 | |-- out/ 12 | |-- default/Release/ 13 | |-- app 14 | |-- ubsan/Debug/ 15 | |-- app 16 | ``` 17 | 18 | In a release package you would want to use the path to the executable instead (and probably not perform an "upfind" walk), the working directory could be anywhere whereas assets shipped with the package will be in the vicinity of the executable. 19 | 20 | ## Assets Directory 21 | 22 | Add a member to `App` to store this path to `assets/`: 23 | 24 | ```cpp 25 | namespace fs = std::filesystem; 26 | 27 | // ... 28 | fs::path m_assets_dir{}; 29 | ``` 30 | 31 | Add a helper function to locate the assets dir, and assign `m_assets_dir` to its return value at the top of `run()`: 32 | 33 | ```cpp 34 | [[nodiscard]] auto locate_assets_dir() -> fs::path { 35 | // look for '/assets/', starting from the working 36 | // directory and walking up the parent directory tree. 37 | static constexpr std::string_view dir_name_v{"assets"}; 38 | for (auto path = fs::current_path(); 39 | !path.empty() && path.has_parent_path(); path = path.parent_path()) { 40 | auto ret = path / dir_name_v; 41 | if (fs::is_directory(ret)) { return ret; } 42 | } 43 | std::println("[lvk] Warning: could not locate '{}' directory", dir_name_v); 44 | return fs::current_path(); 45 | } 46 | 47 | // ... 48 | m_assets_dir = locate_assets_dir(); 49 | ``` 50 | -------------------------------------------------------------------------------- /guide/src/shader_objects/srgb_triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/src/shader_objects/srgb_triangle.png -------------------------------------------------------------------------------- /guide/src/shader_objects/srgb_triangle_wireframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/src/shader_objects/srgb_triangle_wireframe.png -------------------------------------------------------------------------------- /guide/src/shader_objects/white_triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/src/shader_objects/white_triangle.png -------------------------------------------------------------------------------- /guide/theme/lang_toggle.css: -------------------------------------------------------------------------------- 1 | /* 1) Make .left-buttons positioned so its child popups can anchor to it */ 2 | .left-buttons { 3 | position: relative; 4 | } 5 | 6 | /* 2) Specifically override #lang-list to appear at the right edge under the button */ 7 | .left-buttons #lang-list.theme-popup { 8 | left: auto; /* remove the default left: 10px */ 9 | right: 0; /* pin to the right edge of .left-buttons */ 10 | top: calc(var(--menu-bar-height) + 4px); 11 | width: auto; /* don’t stretch across the screen */ 12 | white-space: nowrap; /* keep the list from wrapping into multiple lines */ 13 | } 14 | -------------------------------------------------------------------------------- /guide/translations/.gitignore: -------------------------------------------------------------------------------- 1 | theme -------------------------------------------------------------------------------- /guide/translations/ko-KR/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Karnage", "DDing"] 3 | language = "ko-KR" 4 | src = "src" 5 | title = "Learn Vulkan" 6 | 7 | [output.html] 8 | theme = "../theme" 9 | additional-js = ["../theme/lang_toggle.js"] 10 | additional-css = ["../theme/lang_toggle.css"] 11 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/README.md: -------------------------------------------------------------------------------- 1 | # 소개 2 | 3 | Vulkan은 매우 명시적이고 어려운 API로 알려져 있습니다. 하지만 버전이 거듭될수록 새로운 기능이 추가되고 기존 확장 기능들이 핵심 API에 통합되면서, 필수적인 어려움은 점차 줄어들고 있습니다. RAII는 C++의 핵심 개념 중 하나이지만, 대부분의 Vulkan 가이드에서는 이를 제대로 활용하지 않고, 자원을 수동으로 해제하는 방식으로 오히려 명시성을 강조하는 경우가 많습니다. 4 | 5 | 이러한 격차를 메우기 위해 이 가이드는 다음과 같은 목표를 가지고 있습니다. 6 | 7 | - 모던 C++, VulkanHPP, Vulkan 1.3 기능을 적극 활용합니다. 8 | - 성능이 아닌, 단순하고 직관적인 접근에 초점을 맞춥니다. 9 | - 기본적인 렌더링 기능을 갖춘 동적 렌더링 기반을 구축합니다. 10 | 11 | 다시 한번 말하자면, 이 가이드의 목적은 성능이 아닙니다. 이 가이드는 현대 패러다임과 도구를을 활용해 현재 표준으로 자리잡은 멀티 플랫폼 그래픽스 API를 빠르게 소개하는 데 중점을 둡니다. 성능을 고려하지 않더라도 Vulkan은 OpenGL보다 현대적이고 우수한 설계를 갖추고 있습니다. 예를 들어, Vulkan에는 전역 상태 기계가 없고, 파라미터는 의미있는 멤버로 구성된 구조체를 통해 전달되며, 멀티쓰레딩 역시 상당히 간단하게 구현할 수 있습니다(실제로 OpenGL보다 Vulkan에서 멀티쓰레딩이 더 쉽습니다). 또한, 애플리케이션 코드를 변경하지 않고도 오용을 가밎할 수 있는 강력한 검증 레이어를 활성화할 수 있습니다. 12 | 13 | 더 깊이 Vulkan에 대해 학습하고 싶다면 [공식 튜토리얼](https://docs.vulkan.org/tutorial/latest/00_Introduction.html)이 권장됩니다. [vkguide](https://vkguide.dev/)와 [Vulkan Tutorial](https://vulkan-tutorial.com/) 또한 많이 참고되는 자료로, 내용이 매우 자세하게 정리되어 있습니다. 14 | 15 | ## 대상 독자 16 | 17 | 이 가이드는 이런 분들께 추천합니다. 18 | 19 | - 모던 C++의 원리와 사용법을 이해하시는 분 20 | - 써드파티 라이브러리를 사용해 C++ 프로젝트를 진행해보신 분 21 | - 그래픽스에 어느 정도 익숙하신 분 22 | - OpenGL 튜토리얼을 따라 해본 경험이 있다면 이상적입니다 23 | - SFML / SDL과 같은 프레임워크를 사용해본 경험도 도움이 됩니다 24 | - 필요한 모든 정보가 이 가이드 하나에 전부 담겨 있지 않아도 괜찮으신 분 25 | 26 | 이 책은 다음 내용을 다루지 않습니다. 27 | 28 | - GPU 기반 렌더링 기법 29 | - 그래픽스 시스템의 근본적인 구조부터 시작하는 실시간 렌더링 30 | - 타일 기반 GPU(예: 모바일 기기나 Android)를 위한 고려 사항 31 | 32 | ## 소스코드 33 | 34 | 프로젝트의 소스코드와 본 가이드는 여기에서 확인할 수 있습니다. `section/*` 브랜치는 각 섹션의 끝에서의 코드 상태를 반영하는 것을 목표로 합니다. 버그 수정이나 일부 변경사항은 가능한 한 반영하지만, `main`브랜치의 최신 상태와는 일부 차이가 있을 수 있습니다. 가이드 자체의 소스는 오직 `main` 브랜치에만 최신 상태로 유지되며, 변경사항은 다른 브랜치로 반영되지 않습니다. 35 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | [소개](README.md) 4 | 5 | # 기초 6 | 7 | - [시작하기](getting_started/README.md) 8 | - [프로젝트 레이아웃](getting_started/project_layout.md) 9 | - [검증 레이어](getting_started/validation_layers.md) 10 | - [class App](getting_started/class_app.md) 11 | - [초기화](initialization/README.md) 12 | - [GLFW Window](initialization/glfw_window.md) 13 | - [Vulkan 인스턴스](initialization/instance.md) 14 | - [Vulkan Surface](initialization/surface.md) 15 | - [Vulkan 물리 디바이스](initialization/gpu.md) 16 | - [Vulkan 디바이스](initialization/device.md) 17 | - [Scoped Waiter](initialization/scoped_waiter.md) 18 | - [스왑체인](initialization/swapchain.md) 19 | 20 | # Hello Triangle 21 | 22 | - [렌더링](rendering/README.md) 23 | - [스왑체인 루프](rendering/swapchain_loop.md) 24 | - [렌더 싱크](rendering/render_sync.md) 25 | - [스왑체인 업데이트](rendering/swapchain_update.md) 26 | - [동적 렌더링](rendering/dynamic_rendering.md) 27 | - [Dear ImGui](dear_imgui/README.md) 28 | - [class DearImGui](dear_imgui/dear_imgui.md) 29 | - [ImGui 통합](dear_imgui/imgui_integration.md) 30 | - [셰이더 오브젝트](shader_objects/README.md) 31 | - [에셋 위치](shader_objects/locating_assets.md) 32 | - [셰이더 프로그램](shader_objects/shader_program.md) 33 | - [GLSL 에서 SPIR-V](shader_objects/glsl_to_spir_v.md) 34 | - [삼각형 그리기](shader_objects/drawing_triangle.md) 35 | - [그래픽스 파이프라인](shader_objects/pipelines.md) 36 | 37 | # 셰이더 자원 38 | 39 | - [메모리 할당](memory/README.md) 40 | - [Vulkan Memory Allocator](memory/vma.md) 41 | - [버퍼](memory/buffers.md) 42 | - [정점 버퍼](memory/vertex_buffer.md) 43 | - [Command Block](memory/command_block.md) 44 | - [디바이스 버퍼](memory/device_buffers.md) 45 | - [이미지](memory/images.md) 46 | - [디스크립터 셋](descriptor_sets/README.md) 47 | - [파이프라인 레이아웃](descriptor_sets/pipeline_layout.md) 48 | - [Descriptor Buffer](descriptor_sets/descriptor_buffer.md) 49 | - [텍스쳐](descriptor_sets/texture.md) 50 | - [뷰 행렬](descriptor_sets/view_matrix.md) 51 | - [인스턴스 렌더링](descriptor_sets/instanced_rendering.md) 52 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/dear_imgui/README.md: -------------------------------------------------------------------------------- 1 | # Dear ImGui 2 | 3 | Dear ImGui는 네이티브 CMake를 지원하지 않기 때문에, 소스를 실행 파일에 직접 추가하는 방법도 있지만, 컴파일 경고 등에서 우리 코드와 분리하기 위해 외부 라이브러리 타겟인 `imgui`로 추가할 예정입니다. 이를 위해 `imgui`는 GLFW 및 Vulkan-Headers에 연결되어야 하고, `VK_NO_PROTOTYPES`도 정의되어야 하므로 `ext` 타겟 구조에 약간의 변경이 필요합니다. 이후 `learn-vk-ext`는 `imgui` 및 기타 라이브러리들(현재는 `glm`만 있음)과 연결됩니다. 우리는 동적 렌더링을 지원하는 Dear ImGui v1.91.9 버전을 사용할 예정입니다. 4 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/dear_imgui/dear_imgui.md: -------------------------------------------------------------------------------- 1 | # class DearImGui 2 | 3 | Dear ImGui는 자체적인 초기화 과정과 렌더링 루프를 가지고 있으며, 이를 `class DearImGui`로 캡슐화하겠습니다. 4 | 5 | ```cpp 6 | struct DearImGuiCreateInfo { 7 | GLFWwindow* window{}; 8 | std::uint32_t api_version{}; 9 | vk::Instance instance{}; 10 | vk::PhysicalDevice physical_device{}; 11 | std::uint32_t queue_family{}; 12 | vk::Device device{}; 13 | vk::Queue queue{}; 14 | vk::Format color_format{}; // single color attachment. 15 | vk::SampleCountFlagBits samples{}; 16 | }; 17 | 18 | class DearImGui { 19 | public: 20 | using CreateInfo = DearImGuiCreateInfo; 21 | 22 | explicit DearImGui(CreateInfo const& create_info); 23 | 24 | void new_frame(); 25 | void end_frame(); 26 | void render(vk::CommandBuffer command_buffer) const; 27 | 28 | private: 29 | enum class State : std::int8_t { Ended, Begun }; 30 | 31 | struct Deleter { 32 | void operator()(vk::Device device) const; 33 | }; 34 | 35 | State m_state{}; 36 | 37 | Scoped m_device{}; 38 | }; 39 | ``` 40 | 41 | 생성자에서는 ImGui 컨텍스트를 생성하고, Vulkan 함수를 불러와 Vulkan을 위한 GLFW 초기화를 진행합니다 42 | 43 | ```cpp 44 | IMGUI_CHECKVERSION(); 45 | ImGui::CreateContext(); 46 | 47 | static auto const load_vk_func = +[](char const* name, void* user_data) { 48 | return VULKAN_HPP_DEFAULT_DISPATCHER.vkGetInstanceProcAddr( 49 | *static_cast(user_data), name); 50 | }; 51 | auto instance = create_info.instance; 52 | ImGui_ImplVulkan_LoadFunctions(create_info.api_version, load_vk_func, 53 | &instance); 54 | 55 | if (!ImGui_ImplGlfw_InitForVulkan(create_info.window, true)) { 56 | throw std::runtime_error{"Failed to initialize Dear ImGui"}; 57 | } 58 | ``` 59 | 60 | 그 후 Vulkan용 Dear ImGui를 초기화합니다. 61 | 62 | ```cpp 63 | auto init_info = ImGui_ImplVulkan_InitInfo{}; 64 | init_info.ApiVersion = create_info.api_version; 65 | init_info.Instance = create_info.instance; 66 | init_info.PhysicalDevice = create_info.physical_device; 67 | init_info.Device = create_info.device; 68 | init_info.QueueFamily = create_info.queue_family; 69 | init_info.Queue = create_info.queue; 70 | init_info.MinImageCount = 2; 71 | init_info.ImageCount = static_cast(resource_buffering_v); 72 | init_info.MSAASamples = 73 | static_cast(create_info.samples); 74 | init_info.DescriptorPoolSize = 2; 75 | auto pipline_rendering_ci = vk::PipelineRenderingCreateInfo{}; 76 | pipline_rendering_ci.setColorAttachmentCount(1).setColorAttachmentFormats( 77 | create_info.color_format); 78 | init_info.PipelineRenderingCreateInfo = pipline_rendering_ci; 79 | init_info.UseDynamicRendering = true; 80 | if (!ImGui_ImplVulkan_Init(&init_info)) { 81 | throw std::runtime_error{"Failed to initialize Dear ImGui"}; 82 | } 83 | ImGui_ImplVulkan_CreateFontsTexture(); 84 | ``` 85 | 86 | sRGB 포맷을 사용하고 있지만 Dear ImGui는 색상 공간에 대한 인식이 없기 때문에, 스타일 색상들을 선형 공간으로 변환해주어야 합니다. 이렇게 하면 감마 보정 과정을 통해 의도한 색상이 출력됩니다. 87 | 88 | ```cpp 89 | ImGui::StyleColorsDark(); 90 | // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay) 91 | for (auto& colour : ImGui::GetStyle().Colors) { 92 | auto const linear = glm::convertSRGBToLinear( 93 | glm::vec4{colour.x, colour.y, colour.z, colour.w}); 94 | colour = ImVec4{linear.x, linear.y, linear.z, linear.w}; 95 | } 96 | ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = 0.99f; // more opaque 97 | ``` 98 | 99 | 마지막으로 삭제자(Deleter)를 생성하고 구현합니다. 100 | 101 | ```cpp 102 | m_device = Scoped{create_info.device}; 103 | 104 | // ... 105 | void DearImGui::Deleter::operator()(vk::Device const device) const { 106 | device.waitIdle(); 107 | ImGui_ImplVulkan_DestroyFontsTexture(); 108 | ImGui_ImplVulkan_Shutdown(); 109 | ImGui_ImplGlfw_Shutdown(); 110 | ImGui::DestroyContext(); 111 | } 112 | ``` 113 | 114 | 이 외의 나머지 함수들은 비교적 단순합니다. 115 | 116 | ```cpp 117 | void DearImGui::new_frame() { 118 | if (m_state == State::Begun) { end_frame(); } 119 | ImGui_ImplGlfw_NewFrame(); 120 | ImGui_ImplVulkan_NewFrame(); 121 | ImGui::NewFrame(); 122 | m_state = State::Begun; 123 | } 124 | 125 | void DearImGui::end_frame() { 126 | if (m_state == State::Ended) { return; } 127 | ImGui::Render(); 128 | m_state = State::Ended; 129 | } 130 | 131 | // NOLINTNEXTLINE(readability-convert-member-functions-to-static) 132 | void DearImGui::render(vk::CommandBuffer const command_buffer) const { 133 | auto* data = ImGui::GetDrawData(); 134 | if (data == nullptr) { return; } 135 | ImGui_ImplVulkan_RenderDrawData(data, command_buffer); 136 | } 137 | ``` 138 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/dear_imgui/imgui_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/translations/ko-KR/src/dear_imgui/imgui_demo.png -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/dear_imgui/imgui_integration.md: -------------------------------------------------------------------------------- 1 | # ImGui 통합 2 | 3 | `Swapchain`이 이미지 포맷을 외부에 노출하도록 수정하겠습니다. 4 | 5 | ```cpp 6 | [[nodiscard]] auto get_format() const -> vk::Format { 7 | return m_ci.imageFormat; 8 | } 9 | ``` 10 | 11 | `class App`은 이제 `std::optional` 멤버를 담을 수 있으며, 이를 생성하는 함수를 추가하고 호출할 수 있습니다. 12 | 13 | ```cpp 14 | void App::create_imgui() { 15 | auto const imgui_ci = DearImGui::CreateInfo{ 16 | .window = m_window.get(), 17 | .api_version = vk_version_v, 18 | .instance = *m_instance, 19 | .physical_device = m_gpu.device, 20 | .queue_family = m_gpu.queue_family, 21 | .device = *m_device, 22 | .queue = m_queue, 23 | .color_format = m_swapchain->get_format(), 24 | .samples = vk::SampleCountFlagBits::e1, 25 | }; 26 | m_imgui.emplace(imgui_ci); 27 | } 28 | ``` 29 | 30 | 렌더 패스를 리셋한 이후에 새로운 ImGui 프레임을 시작하고, 데모 창을 띄워봅시다. 31 | 32 | ```cpp 33 | m_device->resetFences(*render_sync.drawn); 34 | m_imgui->new_frame(); 35 | 36 | // ... 37 | command_buffer.beginRendering(rendering_info); 38 | ImGui::ShowDemoWindow(); 39 | // draw stuff here. 40 | command_buffer.endRendering(); 41 | ``` 42 | 43 | ImGui는 이 시점에서는 아무것도 그리지 않습니다(실제 그리기 명령은 커맨드 버퍼가 필요합니다). 이 부분은 상위 로직을 구성하기 위한 커스터마이징 지점입니다. 44 | 45 | 우리는 Dear ImGui를 위한 별도의 렌더 패스를 사용합니다. 이는 코드의 분리를 위한 목적도 있고, 메인 렌더 패스를 나중에 깊이 버퍼를 추가하는 것과 같은 상황에 변경할 수 있도록 하기 위함입니다 `DearImGui`는 하나의 색상 어태치먼트만 사용하는 전용 렌더 패스를 설정한다고 간주합니다. 46 | 47 | ```cpp 48 | m_imgui->end_frame(); 49 | // we don't want to clear the image again, instead load it intact after the 50 | // previous pass. 51 | color_attachment.setLoadOp(vk::AttachmentLoadOp::eLoad); 52 | rendering_info.setColorAttachments(color_attachment) 53 | .setPDepthAttachment(nullptr); 54 | command_buffer.beginRendering(rendering_info); 55 | m_imgui->render(command_buffer); 56 | command_buffer.endRendering(); 57 | ``` 58 | 59 | ![ImGui Demo](./imgui_demo.png) 60 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/descriptor_sets/README.md: -------------------------------------------------------------------------------- 1 | # 디스크립터 셋 2 | 3 | [Vulkan 디스크립터](https://docs.vulkan.org/guide/latest/mapping_data_to_shaders.html#descriptors)는 기본적으로 셰이더가 접근할 수 있는 자원(예 : 유니폼 버퍼, 스토리지 버퍼, 샘플러가 결합된 텍스쳐 등)에 대한 타입이 지정된 포인터입니다. 디스크립터 셋은 이러한 디스크립터들을 다양한 **바인딩**에 모아 하나의 단위로 결합한 집합이며, 셰이더는 특정 셋 번호와 바인딩 번호를 기준으로 입력을 선언합니다. 셰이더에서 사용하는 모든 디스크립터 셋은 드로우 콜 전에 반드시 업데이트, 바인딩되어야 합니다. 디스크립터 셋 레이아웃은 특정 셋 번호에 해당하는 디스크립터 셋의 구성 방식을 나타내며, 일반적으로 셰이더에서 사용하는 모든 디스크립터 셋을 나타냅니다. 디스크립터 셋은 디스크립터 풀과 원하는 디스크립터 셋 레이아웃을 이용해 할당됩니다. 4 | 5 | 디스크립터 셋 레이아웃을 구성하고 디스크립터 셋을 관리하는 것은 다양한 접근 방법이 가능하며, 각각 장단점이 있어 꽤 복잡한 주제입니다. [이 페이지](https://docs.vulkan.org/samples/latest/samples/performance/descriptor_management/README.html)에서는 그중에서도 신뢰할 수 있는 몇 가지 방법들을 설명합니다. 2D 프레임워크와 간단한 3D 프레임워크의 경우 문서에서 가장 단순한 접근 방식이라 설명하는 것처럼, 디스크립터 셋을 매 프레임마다 할당하고 업데이트하는 방식으로 처리할 수 있습니다. [이 글은](https://zeux.io/2020/02/27/writing-an-efficient-vulkan-renderer/) 매우 상세하지만 현재는 다소 오래된 자료입니다. 더 현대적인 접근법으로는 바인드리스 혹은 디스크립터 인덱싱이라 불리는 방식이 있으며,이에 대해서는 [공식 문서](https://docs.vulkan.org/samples/latest/samples/extensions/descriptor_indexing/README.html)에서 다루고 있습니다. 6 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/descriptor_sets/instanced_rendering.md: -------------------------------------------------------------------------------- 1 | # 인스턴스 렌더링 2 | 3 | 하나의 객체를 여러 번 그려야 할 때 사용할 수 있는 방법 중 하나는 인스턴스 렌더링입니다. 기본 아이디어는 인스턴스별 데이터를 유니폼 버퍼 또는 스토리지 버퍼에 담고, 이를 정점 셰이더에서 참조하는 것입니다. 우리는 인스턴스마다 하나의 모델 행렬을 표현하겠습니다. 필요하다면 색상과 같은 정보를 포함해 프래그먼트 셰이더에서 기존 출력 색상에 곱하는 방식으로 사용할 수도 있습니다. 이러한 데이터는 스토리지 버퍼(SSBO)에 바인딩되며, 버퍼의 크기는 호출 시점에 결정됩니다. 4 | 5 | SSBO와 인스턴스 행렬을 저장할 버퍼를 추가합니다. 6 | 7 | ```cpp 8 | std::vector m_instance_data{}; // model matrices. 9 | std::optional m_instance_ssbo{}; 10 | ``` 11 | 12 | 렌더링할 인스턴스에 사용할 `Transform`을 추가하고 이를 기반으로 행렬을 업데이트하는 함수를 추가합니다. 13 | 14 | ```cpp 15 | void update_instances(); 16 | 17 | // ... 18 | std::array m_instances{}; // generates model matrices. 19 | 20 | // ... 21 | void App::update_instances() { 22 | m_instance_data.clear(); 23 | m_instance_data.reserve(m_instances.size()); 24 | for (auto const& transform : m_instances) { 25 | m_instance_data.push_back(transform.model_matrix()); 26 | } 27 | // can't use bit_cast anymore, reinterpret data as a byte array instead. 28 | auto const span = std::span{m_instance_data}; 29 | void* data = span.data(); 30 | auto const bytes = 31 | std::span{static_cast(data), span.size_bytes()}; 32 | m_instance_ssbo->write_at(m_frame_index, bytes); 33 | } 34 | ``` 35 | 36 | 디스크립터 풀을 업데이트하여 스토리지 버퍼를 지원하도록 합니다. 37 | 38 | ```cpp 39 | // ... 40 | vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2}, 41 | vk::DescriptorPoolSize{vk::DescriptorType::eStorageBuffer, 2}, 42 | ``` 43 | 44 | 디스크립터 셋을 2번과 해당 바인딩을 추가합니다. 이처럼 각 디스크립터 셋을 명확하게 역할별로 분리하는 것이 좋습니다. 45 | 46 | * 디스크립터 셋 0 - : 뷰 / 카메라 47 | * 디스크립터 셋 1 - 텍스쳐 / 머테리얼 48 | * 디스크립터 셋 2 : 인스턴싱 49 | 50 | ```cpp 51 | static constexpr auto set_2_bindings_v = std::array{ 52 | layout_binding(1, vk::DescriptorType::eStorageBuffer), 53 | }; 54 | auto set_layout_cis = std::array{}; 55 | // ... 56 | set_layout_cis[2].setBindings(set_2_bindings_v); 57 | ``` 58 | 59 | 뷰 UBO를 생성한 이후 인스턴스용 SSBO를 생성합니다. 60 | 61 | ```cpp 62 | m_instance_ssbo.emplace(m_allocator.get(), m_gpu.queue_family, 63 | vk::BufferUsageFlagBits::eStorageBuffer); 64 | ``` 65 | 66 | `update_view()`를 호출한 다음 `update_instances()`를 호출합니다. 67 | 68 | ```cpp 69 | // ... 70 | update_view(); 71 | update_instances(); 72 | ``` 73 | 74 | 트랜스폼 확인 로직을 람다로 분리해 각 인스턴스의 트랜스폼을 검사합니다. 75 | 76 | ```cpp 77 | static auto const inspect_transform = [](Transform& out) { 78 | ImGui::DragFloat2("position", &out.position.x); 79 | ImGui::DragFloat("rotation", &out.rotation); 80 | ImGui::DragFloat2("scale", &out.scale.x, 0.1f); 81 | }; 82 | 83 | ImGui::Separator(); 84 | if (ImGui::TreeNode("View")) { 85 | inspect_transform(m_view_transform); 86 | ImGui::TreePop(); 87 | } 88 | 89 | ImGui::Separator(); 90 | if (ImGui::TreeNode("Instances")) { 91 | for (std::size_t i = 0; i < m_instances.size(); ++i) { 92 | auto const label = std::to_string(i); 93 | if (ImGui::TreeNode(label.c_str())) { 94 | inspect_transform(m_instances.at(i)); 95 | ImGui::TreePop(); 96 | } 97 | } 98 | ImGui::TreePop(); 99 | } 100 | ``` 101 | 102 | SSBO를 위한 descriptorWrite도 추가합니다. 103 | 104 | ```cpp 105 | auto writes = std::array{}; 106 | // ... 107 | auto const set2 = descriptor_sets[2]; 108 | auto const instance_ssbo_info = 109 | m_instance_ssbo->descriptor_info_at(m_frame_index); 110 | write.setBufferInfo(instance_ssbo_info) 111 | .setDescriptorType(vk::DescriptorType::eStorageBuffer) 112 | .setDescriptorCount(1) 113 | .setDstSet(set2) 114 | .setDstBinding(0); 115 | writes[2] = write; 116 | ``` 117 | 118 | 마지막으로, 드로우 콜의 인스턴스 수를 변경합니다. 119 | 120 | ```cpp 121 | auto const instances = static_cast(m_instances.size()); 122 | // m_vbo has 6 indices. 123 | command_buffer.drawIndexed(6, instances, 0, 0, 0); 124 | ``` 125 | 126 | 정점 셰이더를 수정하여 인스턴스별 모델 행렬을 적용합니다. 127 | 128 | ```glsl 129 | // ... 130 | layout (set = 1, binding = 1) readonly buffer Instances { 131 | mat4 mat_ms[]; 132 | }; 133 | 134 | // ... 135 | const mat4 mat_m = mat_ms[gl_InstanceIndex]; 136 | const vec4 world_pos = mat_m * vec4(a_pos, 0.0, 1.0); 137 | ``` 138 | 139 | ![Instanced Rendering](./instanced_rendering.png) 140 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/descriptor_sets/instanced_rendering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/translations/ko-KR/src/descriptor_sets/instanced_rendering.png -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/descriptor_sets/pipeline_layout.md: -------------------------------------------------------------------------------- 1 | # 파이프라인 레이아웃 2 | 3 | [Vulkan 파이프라인 레이아웃](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineLayout.html)은 셰이더 프로그램과 연결된 디스크립터 셋과 푸시 상수를 나타냅니다. 셰이더 오브젝트를 사용할 경우에도 디스크립터 셋을 활용하기 위해 파이프라인 레이아웃이 필요합니다. 4 | 5 | 뷰/프로젝션 행렬을 담기 위한 유니폼 버퍼를 포함하는 단일 디스크립터 셋 레이아웃부터 시작합니다. 디스크립터 풀을 `App`에 추가하고 셰이더보다 먼저 생성합니다. 6 | 7 | ```cpp 8 | vk::UniqueDescriptorPool m_descriptor_pool{}; 9 | 10 | // ... 11 | void App::create_descriptor_pool() { 12 | static constexpr auto pool_sizes_v = std::array{ 13 | // 2 uniform buffers, can be more if desired. 14 | vk::DescriptorPoolSize{vk::DescriptorType::eUniformBuffer, 2}, 15 | }; 16 | auto pool_ci = vk::DescriptorPoolCreateInfo{}; 17 | // allow 16 sets to be allocated from this pool. 18 | pool_ci.setPoolSizes(pool_sizes_v).setMaxSets(16); 19 | m_descriptor_pool = m_device->createDescriptorPoolUnique(pool_ci); 20 | } 21 | ``` 22 | 23 | 새로운 멤버를 `App`에 추가해 디스크립터 셋 레이아웃과 파이프라인 레이아웃을 담도록 합니다. `m_set_layout_views`는 디스크립터 셋 레이아웃의 핸들을 연속된 vector로 복사한 것입니다. 24 | 25 | ```cpp 26 | std::vector m_set_layouts{}; 27 | std::vector m_set_layout_views{}; 28 | vk::UniquePipelineLayout m_pipeline_layout{}; 29 | 30 | // ... 31 | constexpr auto layout_binding(std::uint32_t binding, 32 | vk::DescriptorType const type) { 33 | return vk::DescriptorSetLayoutBinding{ 34 | binding, type, 1, vk::ShaderStageFlagBits::eAllGraphics}; 35 | } 36 | 37 | // ... 38 | void App::create_pipeline_layout() { 39 | static constexpr auto set_0_bindings_v = std::array{ 40 | layout_binding(0, vk::DescriptorType::eUniformBuffer), 41 | }; 42 | auto set_layout_cis = std::array{}; 43 | set_layout_cis[0].setBindings(set_0_bindings_v); 44 | 45 | for (auto const& set_layout_ci : set_layout_cis) { 46 | m_set_layouts.push_back( 47 | m_device->createDescriptorSetLayoutUnique(set_layout_ci)); 48 | m_set_layout_views.push_back(*m_set_layouts.back()); 49 | } 50 | 51 | auto pipeline_layout_ci = vk::PipelineLayoutCreateInfo{}; 52 | pipeline_layout_ci.setSetLayouts(m_set_layout_views); 53 | m_pipeline_layout = 54 | m_device->createPipelineLayoutUnique(pipeline_layout_ci); 55 | } 56 | ``` 57 | 58 | 레이아웃 전체에 해당하는 디스크립터 셋을 할당하는 함수를 추가합니다. 59 | 60 | ```cpp 61 | auto App::allocate_sets() const -> std::vector { 62 | auto allocate_info = vk::DescriptorSetAllocateInfo{}; 63 | allocate_info.setDescriptorPool(*m_descriptor_pool) 64 | .setSetLayouts(m_set_layout_views); 65 | return m_device->allocateDescriptorSets(allocate_info); 66 | } 67 | ``` 68 | 69 | 그릴 객체에 쓰일 디스크립터 셋을 저장합니다. 70 | 71 | ```cpp 72 | Buffered> m_descriptor_sets{}; 73 | 74 | // ... 75 | 76 | void App::create_descriptor_sets() { 77 | for (auto& descriptor_sets : m_descriptor_sets) { 78 | descriptor_sets = allocate_sets(); 79 | } 80 | } 81 | ``` 82 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/descriptor_sets/rgby_texture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/translations/ko-KR/src/descriptor_sets/rgby_texture.png -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/descriptor_sets/view_matrix.md: -------------------------------------------------------------------------------- 1 | # 뷰 행렬 2 | 3 | 뷰 행렬을 통합하는 작업은 꽤 간단합니다. 먼저, 오브젝트와 카메라/뷰의 변환 정보를 하나의 구조체로 캡슐화합니다. 4 | 5 | ```cpp 6 | struct Transform { 7 | glm::vec2 position{}; 8 | float rotation{}; 9 | glm::vec2 scale{1.0f}; 10 | 11 | [[nodiscard]] auto model_matrix() const -> glm::mat4; 12 | [[nodiscard]] auto view_matrix() const -> glm::mat4; 13 | }; 14 | ``` 15 | 16 | 공통된 로직을 함수로 사용하도록 두 가지 멤버 함수를 추가하겠습니다. 17 | 18 | ```cpp 19 | namespace { 20 | struct Matrices { 21 | glm::mat4 translation; 22 | glm::mat4 orientation; 23 | glm::mat4 scale; 24 | }; 25 | 26 | [[nodiscard]] auto to_matrices(glm::vec2 const position, float rotation, 27 | glm::vec2 const scale) -> Matrices { 28 | static constexpr auto mat_v = glm::identity(); 29 | static constexpr auto axis_v = glm::vec3{0.0f, 0.0f, 1.0f}; 30 | return Matrices{ 31 | .translation = glm::translate(mat_v, glm::vec3{position, 0.0f}), 32 | .orientation = glm::rotate(mat_v, glm::radians(rotation), axis_v), 33 | .scale = glm::scale(mat_v, glm::vec3{scale, 1.0f}), 34 | }; 35 | } 36 | } // namespace 37 | 38 | auto Transform::model_matrix() const -> glm::mat4 { 39 | auto const [t, r, s] = to_matrices(position, rotation, scale); 40 | // right to left: scale first, then rotate, then translate. 41 | return t * r * s; 42 | } 43 | 44 | auto Transform::view_matrix() const -> glm::mat4 { 45 | // view matrix is the inverse of the model matrix. 46 | // instead, perform translation and rotation in reverse order and with 47 | // negative values. or, use glm::lookAt(). 48 | // scale is kept unchanged as the first transformation for 49 | // "intuitive" scaling on cameras. 50 | auto const [t, r, s] = to_matrices(-position, -rotation, scale); 51 | return r * t * s; 52 | } 53 | ``` 54 | 55 | `App`에 `Transform` 멤버를 추가하여 뷰/카메라를 나타내고, 해당 멤버를 확인하여 기존의 프로젝션 행렬과 결합합니다. 56 | 57 | ```cpp 58 | Transform m_view_transform{}; // generates view matrix. 59 | 60 | // ... 61 | ImGui::Separator(); 62 | if (ImGui::TreeNode("View")) { 63 | ImGui::DragFloat2("position", &m_view_transform.position.x); 64 | ImGui::DragFloat("rotation", &m_view_transform.rotation); 65 | ImGui::DragFloat2("scale", &m_view_transform.scale.x); 66 | ImGui::TreePop(); 67 | } 68 | 69 | // ... 70 | auto const mat_view = m_view_transform.view_matrix(); 71 | auto const mat_vp = mat_projection * mat_view; 72 | auto const bytes = 73 | std::bit_cast>(mat_vp); 74 | m_view_ubo->write_at(m_frame_index, bytes); 75 | ``` 76 | 77 | 자연스럽게 뷰를 왼쪽으로 이동하면 현재는 사각형 하나뿐이지만 이것이 오른쪽으로 이동한 것으로 보일 것입니다. 78 | 79 | ![View Matrix](./view_matrix.png) 80 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/descriptor_sets/view_matrix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/translations/ko-KR/src/descriptor_sets/view_matrix.png -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/descriptor_sets/view_ubo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/translations/ko-KR/src/descriptor_sets/view_ubo.png -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/getting_started/README.md: -------------------------------------------------------------------------------- 1 | # 시작하기 2 | 3 | Vulkan은 플랫폼에 독립적인 API입니다. 이로 인해 다양한 구현을 포괄해야 하므로, 다소 장황해질 수 밖에 없습니다. 이 가이드에서는 Windows와 Linux(x64 혹은 aarch64)에 집중하고, 외장 GPU를 중심으로 설명할 것입니다. 이를 통해 Vulkan의 복잡함을 어느 정도 완화할 수 있습니다. Vulkan 1.3은 우리가 다룰 데스크탑 플랫폼과 대부분의 최신 그래픽 카드에서 널리 지원됩니다. 4 | 5 | > 이것이 내장 GPU를 지원하지 않는다는 뜻은 아닙니다. 다만 특별히 그에 맞게 설계하거나 최적화하지는 않습니다. 6 | 7 | ## 요구사항 8 | 9 | 1. Vulkan 1.3 버전 이상을 지원하는 GPU와 로더 10 | 1. [Vulkan 1.3 이상의 SDK](https://vulkan.lunarg.com/sdk/home) 11 | 1. 이는 검증 레이어에 필요하며, Vulkan 애플리케이션 개발 시 중요한 구성요소입니다. 단, 프로젝트 자체는 SDK에 직접 의존하지 않습니다. 12 | 1. 항상 최신 SDK 사용이 권장됩니다. (이 문서를 작성하는 시점 기준으로는 1.4) 13 | 1. Vulkan을 기본적으로 지원하는 데스크탑 운영체제 14 | 1. Windows 혹은 최신 패키지를 제공하는 Linux 배포판 사용이 권장됩니다. 15 | 2. MacOS는 Vulkan을 기본적으로 지원하지 않습니다. MoltenVk를 통해 사용할 수는 있으나, 작성 시점 기준 MoltenVk는 Vulkan 1.3을 완전히 지원하지 않기 때문에, 이 환경에서는 제약이 있을 수 있습니다. 16 | 2. C++23을 지원하는 컴파일러 및 표준 라이브러리 17 | 1. GCC14+, Clang18+, 최신 MSVC가 권장되며, MinGW/MSYS는 권장되지 않습니다. 18 | 2. C++20을 사용하고 C++23의 특정 기능을 대체하는 것도 가능합니다.(예: `std::print()` 대신 `fmt::print()` 사용, 람다식에 `()` 추가 등) 19 | 3. CMake 3.24 이상 20 | 21 | ## 개요 22 | 23 | C++ 모듈에 대한 지원은 점차 확대되고 있지만, 우리가 목표로 하는 모든 플랫폼과 IDE에서는 아직 완전히 지원되지 않습니다. 따라서 당분간은 헤더 파일을 사용할 수 밖에 없습니다. 이는 가까운 미래에 도구들이 개선되면, 가이드를 리팩토링하면서 변경될 수 있습니다. 24 | 25 | 이 프로젝트는 "Build the World" 접근 방식을 사용합니다. 이는 Sanitizer 사용을 가능하게 하고, 모든 지원 플랫폼에서 재현 가능한 빌드를 제공하며, 대상 시스템에서의 사전 설치 요구사항을 최소화합니다. 물론, 미리 빌드된 바이너리를 사용하는 것도 가능하며, Vulkan을 사용하는 방식에는 영향을 주지 않습니다. 26 | 27 | ## 라이브러리 28 | 29 | 1. 창과 입력, Surface를 위한 [GLFW](https://github.com/glfw/glfw) 30 | 1. Vulkan과 상호작용하기 위한 [VulkanHPP](https://github.com/KhronosGroup/Vulkan-Hpp), [Vulkan-Headers](https://github.com/KhronosGroup/Vulkan-Headers)를 통해 사용. 31 | 1. Vulkan은 C API이지만, 공식 C++ 래핑 라이브러리가 제공되어 많은 편리한 기능을 제공합니다. 이 가이드는 C API를 사용하는 다른 라이브러리들(예: GLFW,VMA)을 사용할 때를 제외하고는 거의 C++만 사용합니다. 32 | 2. Vulkan 메모리 힙을 다루기 위한 [Vulkan Memory Allocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator/) 33 | 3. C++에서 GLSL과 유사한 선형대수학을 위한 [GLM](https://github.com/g-truc/glm) 34 | 4. UI를 위한 [Dear ImGui](https://github.com/ocornut/imgui) 35 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/getting_started/class_app.md: -------------------------------------------------------------------------------- 1 | # Application 2 | 3 | `class App`은 전체 애플리케이션의 소유자이자 실행 주체 역할을 합니다. 인스턴스는 하나뿐이지만, 클래스를 사용함으로써 RAII의 이점을 활용해 자원을 올바른 순서로 자동 해제할 수 있으며, 전역 변수를 사용할 필요도 없습니다. 4 | 5 | ```cpp 6 | // app.hpp 7 | namespace lvk { 8 | class App { 9 | public: 10 | void run(); 11 | }; 12 | } // namespace lvk 13 | 14 | // app.cpp 15 | namespace lvk { 16 | void App::run() { 17 | // TODO 18 | } 19 | } // namespace lvk 20 | ``` 21 | 22 | ## Main 23 | 24 | `main.cpp`는 많은 역할을 하지 않습니다. 주로 실제 진입점으로 제어를 넘기고, 치명적인 예외를 처리하는 역할을 합니다. 25 | 26 | ```cpp 27 | // main.cpp 28 | auto main() -> int { 29 | try { 30 | lvk::App{}.run(); 31 | } catch (std::exception const& e) { 32 | std::println(stderr, "PANIC: {}", e.what()); 33 | return EXIT_FAILURE; 34 | } catch (...) { 35 | std::println("PANIC!"); 36 | return EXIT_FAILURE; 37 | } 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/getting_started/high_level_loader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/translations/ko-KR/src/getting_started/high_level_loader.png -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/getting_started/project_layout.md: -------------------------------------------------------------------------------- 1 | # 프로젝트 레이아웃 2 | 3 | 이 페이지는 이 가이드에서 사용하는 코드 레이아웃에 대해 설명합니다. 여기서 설명하는 내용은 이 가이드에서의 참고사항일 뿐이며, Vulkan 사용과는 관련이 없습니다. 4 | 5 | 외부 의존성은 zip파일로 묶여있으며, CMake가 구성(configure) 단계에서 이를 압축 해제합니다. FetchContent를 사용하는 것도 유효한 대안입니다. 6 | 7 | `Ninja Multi-Config`는 OS나 컴파일러에 관계없이 사용되는 기본 생성기로 가정합니다. 이는 프로젝트 루트 디렉토리에 있는 `CMakePresets.json`파일에 설정되어 있으며, 사용자 정의 프리셋은 `CMakeUserPresets.json`을 통해 추가할 수 있습니다. 8 | 9 | > Windows에서는 Visual Studio의 CMake Mode가 이 생성기를 사용하며, 자동으로 프리셋을 불러옵니다. Visual Studio Code에서는 CMake Tools 확장이 자동으로 프리셋을 사용합니다. 그 외의 IDE에서는 CMake 프리셋 사용 방법에 대한 해당 IDE의 문서를 참고하세요. 10 | 11 | **Filesystem** 12 | 13 | ``` 14 | . 15 | |-- CMakeLists.txt <== executable target 16 | |-- CMakePresets.json 17 | |-- [other project files] 18 | |-- ext/ 19 | │ |-- CMakeLists.txt <== external dependencies target 20 | |-- src/ 21 | |-- [sources and headers] 22 | ``` -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/getting_started/validation_layers.md: -------------------------------------------------------------------------------- 1 | # 검증 레이어 2 | 3 | 애플리케이션이 Vulkan과 상호작용할 때 거치는 영역인 로더(loader)는 매우 강력하고 유연합니다. [여기](https://github.com/KhronosGroup/Vulkan-Loader/blob/main/docs/LoaderInterfaceArchitecture.md)에서 더 자세한 내용을 확인할 수 있습니다. 이 로더의 설계는 API 호출을 구성 가능한 레이어를 통해 연결할 수 있게 해주며, 이는 예를 들어 오버레이 구현에 사용될 수 있고, 이 중 우리에게 가장 중요한 것은 [검증 레이어](https://github.com/KhronosGroup/Vulkan-ValidationLayers/blob/main/docs/README.md) 입니다. 4 | 5 | ![Vulkan Loader](high_level_loader.png) 6 | 7 | 크로노스 그룹의 [권장사항](https://github.com/KhronosGroup/Vulkan-ValidationLayers/blob/main/docs/khronos_validation_layer.md#vkconfig)대로, 검증 레이어를 사용할 때는 [Vulkan Configurator (GUI)](https://github.com/LunarG/VulkanTools/tree/main/vkconfig_gui)를 사용하는 것이 좋습니다. 이 애플리케이션은 Vulkan SDK에 포함되어 있으며, 애플리케이션을 개발하는 동안 실행 상태로 유지해야 합니다. 또한, 이 툴이 감지된 애플리케이션에 검증 레이어를 주입하고, 동기화 검증 기능이 활성화되어 있는지 확인하세요. 이 접근 방식은 런타임 시 많은 유연성을 제공하며, 예를 들어 오류 발생 시 디버거 중단 설정(VkConfig가 디버거를 중단시킴) 같은 기능도 포함됩니다. 이와 더불어, 애플리케이션 내부에 별도의 검증 레이어 관련 코드를 작성할 필요가 없어집니다. 8 | 9 | > 주의 : 개발(또는 데스크탑) 환경의 `PATH` 환경 변수를 수정하거나, 지원되는 시스템에서는 `LD_LIBRARY_PATH`를 사용하여 SDK의 바이너리 경로가 우선적으로 인식되도록 설정해야 합니다. 10 | 11 | ![Vulkan Configurator](./vkconfig_gui.png) 12 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/getting_started/vkconfig_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/translations/ko-KR/src/getting_started/vkconfig_gui.png -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/initialization/README.md: -------------------------------------------------------------------------------- 1 | # 초기화 2 | 3 | 여기서는 다음을 포함하여, 애플리케이션 실행에 필요한 모든 시스템의 초기화 과정을 다룹니다. 4 | 5 | - GLFW를 초기화하고 창 생성하기 6 | - Vulkan Instance 생성하기 7 | - Vulkan Surface 생성하기 8 | - Vulkan Physical Device 선택하기 9 | - Vulkan logical Device 생성하기 10 | - Vulkan Swapchain 생성하기 11 | 12 | 여기서 어느 한 단계라도 실패하면, 그 이후에는 의미 있는 작업을 진행할 수 없기 때문에 치명적인 오류로 간주됩니다. 13 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/initialization/device.md: -------------------------------------------------------------------------------- 1 | # Vulkan 디바이스 2 | 3 | [디바이스](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)같은 기능들을 명시적으로 선언해야 합니다. 4 | 5 | `vk::QueueCreateInfo`객체를 설정합시다. 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 | `vk::UniqueDevice` 멤버를 `m_gpu` 이후에 선언하고, 이를 생성한 다음 디스패쳐를 해당 디바이스로 다시 초기화합니다. 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` 멤버도 선언하고 초기화합니다(순서는 중요하지 않습니다. 이는 단순한 핸들이며 실제 큐는 디바이스가 관리하기 때문입니다). 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 | ``` 67 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/initialization/glfw_window.md: -------------------------------------------------------------------------------- 1 | # GLFW Window 2 | 3 | 창 생성과 이벤트 처리를 위해 GLFW 3.4를 사용할 것입니다. 모든 외부 의존 라이브러리들은 `ext/CMakeLists.txt`에 구성되어 빌드 트리에 추가됩니다. `GLFW_INCLUDE_VULKAN`은 GLFW의 Vulkan 관련 기능(WSI, Window System Integration)을 활성화하기 위해 GLFW를 사용하는 측에서 정의되어야 합니다. GLFW 3.4는 Linux에서 Wayland를 지원하며, 기본적으로 X11과 Wayland 모두를 위한 백엔드를 빌드합니다. 따라서 빌드를 성공적으로 진행하려면 두 플랫폼 모두에 필요한 패키지와 일부 Wayland 및 CMake 의존성이 필요합니다. 특정 백엔드를 사용하고자 할 경우, 런타임에서 `GLFW_PLATFORM`을 통해 지정할 수 있습니다. 4 | 5 | Vulkan-GLFW 애플리케이션에서 다중 창을 사용할 수는 있지만, 이 가이드에서는 다루지 않습니다. 여기서는 GLFW 라이브러리와 단일 창을 하나의 단위로 보고 함께 초기화하고 해제하는 방식으로 구성합니다. GLFW는 구조를 알 수 없는(Opaque) 포인터 `GLFWwindow*`를 반환하므로, 이를 `std::unique_ptr`과 커스텀 파괴자를 사용해 캡슐화하는 것이 적절합니다. 6 | 7 | ```cpp 8 | // window.hpp 9 | namespace lvk::glfw { 10 | struct Deleter { 11 | void operator()(GLFWwindow* window) const noexcept; 12 | }; 13 | 14 | using Window = std::unique_ptr; 15 | 16 | // Returns a valid Window if successful, else throws. 17 | [[nodiscard]] auto create_window(glm::ivec2 size, char const* title) -> Window; 18 | } // namespace lvk::glfw 19 | 20 | // window.cpp 21 | void Deleter::operator()(GLFWwindow* window) const noexcept { 22 | glfwDestroyWindow(window); 23 | glfwTerminate(); 24 | } 25 | ``` 26 | 27 | GLFW는 전체 화면이나 테두리 없는 창을 만들 수 있지만, 이 가이드에서는 일반 창을 사용합니다. 창을 생성하지 못하면 이후 작업이 불가능하기 때문에, 실패한 경우에는 모두 치명적인 예외를 발생시키도록 되어 있습니다. 28 | 29 | ```cpp 30 | auto glfw::create_window(glm::ivec2 const size, char const* title) -> Window { 31 | static auto const on_error = [](int const code, char const* description) { 32 | std::println(stderr, "[GLFW] Error {}: {}", code, description); 33 | }; 34 | glfwSetErrorCallback(on_error); 35 | if (glfwInit() != GLFW_TRUE) { 36 | throw std::runtime_error{"Failed to initialize GLFW"}; 37 | } 38 | // check for Vulkan support. 39 | if (glfwVulkanSupported() != GLFW_TRUE) { 40 | throw std::runtime_error{"Vulkan not supported"}; 41 | } 42 | auto ret = Window{}; 43 | // tell GLFW that we don't want an OpenGL context. 44 | glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); 45 | ret.reset(glfwCreateWindow(size.x, size.y, title, nullptr, nullptr)); 46 | if (!ret) { throw std::runtime_error{"Failed to create GLFW Window"}; } 47 | return ret; 48 | } 49 | ``` 50 | 51 | `App`은 이제 `glfw::Window`를 멤버로 저장해 사용자가 창을 닫을 때 까지 `run()`에서 이를 사용할 수 있습니다. 아직은 창에 아무것도 그릴 수 없지만, 이는 앞으로의 과정을 시작하는 첫 단계입니다. 52 | 53 | 이를 private 멤버로 선언합니다. 54 | 55 | ```cpp 56 | private: 57 | glfw::Window m_window{}; 58 | ``` 59 | 60 | 각 작업을 캡슐화하는 몇 가지 private 멤버 함수를 추가합니다. 61 | 62 | ```cpp 63 | void create_window(); 64 | 65 | void main_loop(); 66 | ``` 67 | 68 | 이를 구현하고 `run()`에서 호출합니다. 69 | 70 | ```cpp 71 | void App::run() { 72 | create_window(); 73 | 74 | main_loop(); 75 | } 76 | 77 | void App::create_window() { 78 | m_window = glfw::create_window({1280, 720}, "Learn Vulkan"); 79 | } 80 | 81 | void App::main_loop() { 82 | while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) { 83 | glfwPollEvents(); 84 | } 85 | } 86 | ``` 87 | 88 | > Wayland에서는 아직 창이 보이지 않을 수 있습니다. 창은 애플리케이션이 프레임버퍼를 렌더링한 후에야 화면에 표시됩니다. 89 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/initialization/gpu.md: -------------------------------------------------------------------------------- 1 | # Vulkan 물리 디바이스 2 | 3 | [물리 디바이스](https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#devsandqueues-physical-device-enumeration)는 Vulkan의 완전한 구현체를 나타내며, 일반적으로 하나의 GPU를 의미합니다(단, Mesa/lavapipe와 같은 소프트웨어 렌더러일 수도 있습니다). 여러 GPU가 장착된 랩탑과 같은 일부 기기에서는 여러 개의 물리 디바이스가 존재할 수 있습니다. 이 중에서 다음 조건을 만족하는 것을 하나 선택해야 합니다. 4 | 5 | 1. Vulkan 1.3을 지원해야 합니다. 6 | 2. 스왑체인을 지원해야 합니다. 7 | 3. Graphics와 Transfer작업을 지원하는 Vulkan Queue가 존재해야 합니다. 8 | 4. 이전에 생성한 Vulkan Surface로 출력(present)할 수 있어야 합니다. 9 | 5. (선택 사항) 외장 GPU를 우선적으로 고려합니다. 10 | 11 | 실제 물리 디바이스와 몇 가지 유용한 객체들을 `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(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 | 마지막으로 `Gpu` 멤버를 `App`에 추가하고 `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 | ``` 95 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/initialization/instance.md: -------------------------------------------------------------------------------- 1 | # Vulkan 인스턴스 2 | 3 | Vulkan을 SDK를 통해 빌드 시점에 링킹하는 대신, 런타임에 동적으로 로드할 것입니다. 이를 위해 몇 가지 조정이 필요합니다. 4 | 5 | 1. CMake의 ext target에서 `VK_NO_PROTOTYPES`를 정의해, Vulkan API 함수 선언이 함수 포인터로 변환되도록 합니다. 6 | 2. `app.cpp`에서 전역에 `VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE`를 추가합니다. 7 | 3. 초기화 이전과 초기화 과정 중에 `VULKAN_HPP_DEFAULT_DISPATCHER.init()`을 호출합니다. 8 | 9 | Vulkan에서 가장 먼저 해야할 것은 [인스턴스](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()`호출 직후에 이를 호출하세요. 디스패쳐를 초기화한 후에는, 로더가 요구하는 Vulkan 버전을 충족하는지 확인합니다. 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 관련 인스턴스 확장이 필요하며, 이는 GLFW가 제공해줍니다. 관련 확장을 받아오는 함수를 `window.hpp/cpp`에 추가합니다. 33 | 34 | ```cpp 35 | auto glfw::instance_extensions() -> std::span { 36 | auto count = std::uint32_t{}; 37 | auto const* extensions = glfwGetRequiredInstanceExtensions(&count); 38 | return {extensions, static_cast(count)}; 39 | } 40 | ``` 41 | 42 | 인스턴스 생성에 이어서, `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 종료 전에 반드시 파괴되어야 하므로 순서를 지키는 것이 중요합니다. 인스턴스를 생성한 뒤, 해당 인스턴스를 기반으로 디스패쳐를 다시 초기화합니다. 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가 검증 레이어가 활성화된 상태인지 확인한 후, 애플리케이션을 디버그 혹은 실행하세요. 만약 로더 메시지의 "Information" 레벨 로그가 활성화되어 있다면, 이 시점에서 로드된 레이어 정보, 물리 디바이스 및 해당 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/Validation 유일한 확인 수단입니다. -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/initialization/scoped_waiter.md: -------------------------------------------------------------------------------- 1 | # Scoped Waiter 2 | 3 | 소멸자에서 디바이스가 Idle한 상태가 될 때까지 기다리거나 블록하는 객체는 매우 유용한 추상화입니다. GPU가 Vulkan 객체를 사용 중일 때 해당 객체를 파괴하는 것은 잘못된 사용입니다. 이 객체는 의존성이 있는 자원이 파괴되기 전에 디바이스가 idle 상태임을 보장하는 데 도움이 됩니다. 4 | 5 | 스코프가 끝날 때 임의의 작업을 수행할 수 있는 기능은 다른 곳에서도 유용할 수 있기 때문에, 이를 기본 템플릿 클래스 `Scoped`로 캡슐화합니다. 이 클래스는 포인터 타입 `Type*` 대신에 값 `Type`을 담는 `unique_ptr`와 유사하지만, 다음과 같은 제약이 있습니다. 6 | 7 | 1. `Type`은 기본 생성자가 있어야 합니다. 8 | 2. 기본 생성자를 통한 `Type`은 null과 동일하다고 가정하며, 이 경우 `Deleter`를 호출하지 않습니다. 9 | 10 | ```cpp 11 | template 12 | concept Scopeable = 13 | std::equality_comparable && std::is_default_constructible_v; 14 | 15 | template 16 | class Scoped { 17 | public: 18 | Scoped(Scoped const&) = delete; 19 | auto operator=(Scoped const&) = delete; 20 | 21 | Scoped() = default; 22 | 23 | constexpr Scoped(Scoped&& rhs) noexcept 24 | : m_t(std::exchange(rhs.m_t, Type{})) {} 25 | 26 | constexpr auto operator=(Scoped&& rhs) noexcept -> Scoped& { 27 | if (&rhs != this) { std::swap(m_t, rhs.m_t); } 28 | return *this; 29 | } 30 | 31 | explicit(false) constexpr Scoped(Type t) : m_t(std::move(t)) {} 32 | 33 | constexpr ~Scoped() { 34 | if (m_t == Type{}) { return; } 35 | Deleter{}(m_t); 36 | } 37 | 38 | [[nodiscard]] constexpr auto get() const -> Type const& { return m_t; } 39 | [[nodiscard]] constexpr auto get() -> Type& { return m_t; } 40 | 41 | private: 42 | Type m_t{}; 43 | }; 44 | ``` 45 | 46 | 이 내용이 이해가 되지 않더라도 걱정하지 마세요. 구현 자체는 중요하지 않고, 이 객체가 무엇을 하는지, 그리고 어떻게 사용하는지가 중요합니다. 47 | 48 | `ScopeWaiter`는 이제 비교적 간단하게 구현할 수 있습니다. 49 | 50 | ```cpp 51 | struct ScopedWaiterDeleter { 52 | void operator()(vk::Device const device) const noexcept { 53 | device.waitIdle(); 54 | } 55 | }; 56 | 57 | using ScopedWaiter = Scoped; 58 | ``` 59 | 60 | `ScopeWaiter` 멤버를 `App`의 멤버 리스트 맨 마지막에 추가하세요. 이는 반드시 마지막에 선언되어야 하며, 그렇게 해야 이 멤버가 가장 먼저 파괴되기 때문에 다른 멤버들이 파괴되기 전에 idle 상태가 되는 것을 보장할 수 있습니다. 이를 디바이스 생성 후에 초기화합니다. 61 | 62 | ```cpp 63 | m_waiter = *m_device; 64 | ``` 65 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/initialization/surface.md: -------------------------------------------------------------------------------- 1 | # Vulkan Surface 2 | 3 | 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)는 프레젠테이션 엔진을 통해 창에 이미지를 표시할 수 있게 해줍니다. 4 | 5 | `window.hpp/cpp`에 또 다른 함수를 추가합시다. 6 | 7 | ```cpp 8 | auto glfw::create_surface(GLFWwindow* window, vk::Instance const instance) 9 | -> vk::UniqueSurfaceKHR { 10 | VkSurfaceKHR ret{}; 11 | auto const result = 12 | glfwCreateWindowSurface(instance, window, nullptr, &ret); 13 | if (result != VK_SUCCESS || ret == VkSurfaceKHR{}) { 14 | throw std::runtime_error{"Failed to create Vulkan Surface"}; 15 | } 16 | return vk::UniqueSurfaceKHR{ret, instance}; 17 | } 18 | ``` 19 | 20 | `App`에 `vk::UniqueSurfaceKHR`이라는 멤버를 `m_instance` 이후에 추가하고 Surface를 생성합니다. 21 | 22 | ```cpp 23 | void App::create_surface() { 24 | m_surface = glfw::create_surface(m_window.get(), *m_instance); 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/memory/README.md: -------------------------------------------------------------------------------- 1 | # 메모리 할당 2 | 3 | 명시적인 API인 Vulkan에서는 디바이스가 사용할 메모리를 애플리케이션이 직접 [메모리 할당](https://docs.vulkan.org/guide/latest/memory_allocation.html)을 해야 합니다. 이 과정은 다소 복잡할 수 있기 때문에, Vulkan 사양에서 권장하듯이 이 모든 세부 사항을 [Vulkan Memory Allocator (VMA)](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator)에 맡겨 간단하게 처리할 것입니다. 4 | 5 | Vulkan은 할당된 메모리를 사용하는 두 가지 객체 유형을 제공합니다. 버퍼와 이미지입니다. VMA는 이 둘에 대해 투명한(transparent) 지원을 제공합니다. 우리는 그저 버퍼와 이미지를 디바이스를 통해 직접 할당하는 대신, VMA를 통해 할당 및 해제하면 됩니다. CPU에서의 메모리 할당과 객체 생성과는 달리, Vulkan에는 버퍼와 이미지 생성에는 정렬(alignment)이나 크기(size) 외에도 훨씬 더 많은 파라미터가 필요합니다. 예상하셨겠지만, 여기서는 정점 버퍼, 유니폼/스토리지 버퍼, 그리고 텍스쳐 이미지와 같은 셰이더 자원과 관련된 하위 집합만을 다룰 예정입니다. 6 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/memory/buffers.md: -------------------------------------------------------------------------------- 1 | # 버퍼 2 | 3 | 먼저 VMA 버퍼를 위한 RAII 래퍼 컴포넌트를 추가합니다. 4 | 5 | ```cpp 6 | struct RawBuffer { 7 | [[nodiscard]] auto mapped_span() const -> std::span { 8 | return std::span{static_cast(mapped), size}; 9 | } 10 | 11 | auto operator==(RawBuffer const& rhs) const -> bool = default; 12 | 13 | VmaAllocator allocator{}; 14 | VmaAllocation allocation{}; 15 | vk::Buffer buffer{}; 16 | vk::DeviceSize size{}; 17 | void* mapped{}; 18 | }; 19 | 20 | struct BufferDeleter { 21 | void operator()(RawBuffer const& raw_buffer) const noexcept; 22 | }; 23 | 24 | // ... 25 | void BufferDeleter::operator()(RawBuffer const& raw_buffer) const noexcept { 26 | vmaDestroyBuffer(raw_buffer.allocator, raw_buffer.buffer, 27 | raw_buffer.allocation); 28 | } 29 | ``` 30 | 31 | 버퍼는 호스트(RAM)와 디바이스(VRAM) 메모리를 기반으로 할당될 수 있습니다. 호스트 메모리는 매핑이 가능하므로 매 프레임마다 바뀌는 정보를 담기에 적합하며, 디바이스 메모리는 GPU에서 접근하기에 빠르지만 데이터를 복사하는 데 더 복잡한 절차가 필요합니다. 이를 고려하여 관련된 타입과 생성 함수를 추가하겠습니다. 32 | 33 | ```cpp 34 | struct BufferCreateInfo { 35 | VmaAllocator allocator; 36 | vk::BufferUsageFlags usage; 37 | std::uint32_t queue_family; 38 | }; 39 | 40 | enum class BufferMemoryType : std::int8_t { Host, Device }; 41 | 42 | [[nodiscard]] auto create_buffer(BufferCreateInfo const& create_info, 43 | BufferMemoryType memory_type, 44 | vk::DeviceSize size) -> Buffer; 45 | 46 | // ... 47 | auto vma::create_buffer(BufferCreateInfo const& create_info, 48 | BufferMemoryType const memory_type, 49 | vk::DeviceSize const size) -> Buffer { 50 | if (size == 0) { 51 | std::println(stderr, "Buffer cannot be 0-sized"); 52 | return {}; 53 | } 54 | 55 | auto allocation_ci = VmaAllocationCreateInfo{}; 56 | allocation_ci.flags = 57 | VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; 58 | auto usage = create_info.usage; 59 | if (memory_type == BufferMemoryType::Device) { 60 | allocation_ci.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; 61 | // device buffers need to support TransferDst. 62 | usage |= vk::BufferUsageFlagBits::eTransferDst; 63 | } else { 64 | allocation_ci.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST; 65 | // host buffers can provide mapped memory. 66 | allocation_ci.flags |= VMA_ALLOCATION_CREATE_MAPPED_BIT; 67 | } 68 | 69 | auto buffer_ci = vk::BufferCreateInfo{}; 70 | buffer_ci.setQueueFamilyIndices(create_info.queue_family) 71 | .setSize(size) 72 | .setUsage(usage); 73 | auto vma_buffer_ci = static_cast(buffer_ci); 74 | 75 | VmaAllocation allocation{}; 76 | VkBuffer buffer{}; 77 | auto allocation_info = VmaAllocationInfo{}; 78 | auto const result = 79 | vmaCreateBuffer(create_info.allocator, &vma_buffer_ci, &allocation_ci, 80 | &buffer, &allocation, &allocation_info); 81 | if (result != VK_SUCCESS) { 82 | std::println(stderr, "Failed to create VMA Buffer"); 83 | return {}; 84 | } 85 | 86 | return RawBuffer{ 87 | .allocator = create_info.allocator, 88 | .allocation = allocation, 89 | .buffer = buffer, 90 | .size = size, 91 | .mapped = allocation_info.pMappedData, 92 | }; 93 | } 94 | ``` 95 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/memory/command_block.md: -------------------------------------------------------------------------------- 1 | # Command Block 2 | 3 | 오래 유지되는 정점 버퍼의 경우 디바이스 메모리에 위치하는 편이 더 나은 성능을 보입니다. 특히 3D 메시와 같은 경우에 그렇습니다. 데이터를 디바이스 버퍼로 전송하려면 두 단계가 필요합니다. 4 | 5 | 1. 호스트 버퍼를 할당하고 데이터를 매핑된 메모리로 복사합니다. 6 | 2. 디바이스 버퍼를 할당하고 메모리 복사 명령을 기록한 뒤, 이를 제출합니다. 7 | 8 | 두 번째 단계는 커맨드 버퍼와 큐 제출이 필요하며, 제출된 작업이 완료될 때까지 기다려야 합니다. 이 동작을 클래스로 캡슐화하겠습니다. 이 구조는 이미지를 생성할때에도 쓰일 수 있습니다. 9 | 10 | ```cpp 11 | class CommandBlock { 12 | public: 13 | explicit CommandBlock(vk::Device device, vk::Queue queue, 14 | vk::CommandPool command_pool); 15 | 16 | [[nodiscard]] auto command_buffer() const -> vk::CommandBuffer { 17 | return *m_command_buffer; 18 | } 19 | 20 | void submit_and_wait(); 21 | 22 | private: 23 | vk::Device m_device{}; 24 | vk::Queue m_queue{}; 25 | vk::UniqueCommandBuffer m_command_buffer{}; 26 | }; 27 | ``` 28 | 29 | 이 클래스의 생성자는 임시 할당용으로 미리 생성된 커맨드 풀과, 나중에 명령을 제출할 큐를 인자로 받습니다. 이렇게 하면 해당 객체를 생성 후 다른 코드로 전달해 재사용할 수 있습니다. 30 | 31 | ```cpp 32 | CommandBlock::CommandBlock(vk::Device const device, vk::Queue const queue, 33 | vk::CommandPool const command_pool) 34 | : m_device(device), m_queue(queue) { 35 | // allocate a UniqueCommandBuffer which will free the underlying command 36 | // buffer from its owning pool on destruction. 37 | auto allocate_info = vk::CommandBufferAllocateInfo{}; 38 | allocate_info.setCommandPool(command_pool) 39 | .setCommandBufferCount(1) 40 | .setLevel(vk::CommandBufferLevel::ePrimary); 41 | // all the current VulkanHPP functions for UniqueCommandBuffer allocation 42 | // return vectors. 43 | auto command_buffers = m_device.allocateCommandBuffersUnique(allocate_info); 44 | m_command_buffer = std::move(command_buffers.front()); 45 | 46 | // start recording commands before returning. 47 | auto begin_info = vk::CommandBufferBeginInfo{}; 48 | begin_info.setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit); 49 | m_command_buffer->begin(begin_info); 50 | } 51 | ``` 52 | 53 | `submit_and_wait()`은 커맨드 버퍼의 작업이 끝난 뒤 리셋하여 커맨드 풀로 반환합니다. 54 | 55 | ```cpp 56 | void CommandBlock::submit_and_wait() { 57 | if (!m_command_buffer) { return; } 58 | 59 | // end recording and submit. 60 | m_command_buffer->end(); 61 | auto submit_info = vk::SubmitInfo2KHR{}; 62 | auto const command_buffer_info = 63 | vk::CommandBufferSubmitInfo{*m_command_buffer}; 64 | submit_info.setCommandBufferInfos(command_buffer_info); 65 | auto fence = m_device.createFenceUnique({}); 66 | m_queue.submit2(submit_info, *fence); 67 | 68 | // wait for submit fence to be signaled. 69 | static constexpr auto timeout_v = 70 | static_cast(std::chrono::nanoseconds(30s).count()); 71 | auto const result = m_device.waitForFences(*fence, vk::True, timeout_v); 72 | if (result != vk::Result::eSuccess) { 73 | std::println(stderr, "Failed to submit Command Buffer"); 74 | } 75 | // free the command buffer. 76 | m_command_buffer.reset(); 77 | } 78 | ``` 79 | 80 | ## 멀티쓰레딩 시 고려사항 81 | 82 | "`submit_and_wait()`를 호출할 때마다 메인 쓰레드를 블록하는 대신, 멀티쓰레드로 CommandBlock을 구현하는 편이 낫지 않을까?" 라고 생각하실 수 있습니다. 맞습니다! 하지만 멀티 쓰레딩을 위해서는 몇 가지 추가 작업이 필요합니다. 각 쓰레드가 고유한 커맨드 풀이 필요합니다. CommandBlock마다 하나의 고유한 커맨드 풀을 사용하며, 임계 영역을 뮤텍스로 보호하는 것, 스왑체인으로부터 이미지를 가져오고 표시하는 작업 등 큐에 대한 모든 작업은 동기화되어야 합니다. 이를 위해 `vk::Queue`객체와 해당 큐를 보호하는 `std::mutex` 포인터 혹은 참조를 보관하는 `class Queue`를 설계할 수 있으며, 큐 제출은 이 클래스를 통해 수행할 것입니다. 이렇게 하면 각 에셋을 불러오는 쓰레드가 고유한 커맨드 풀을 사용하면서도 큐 제출은 안전하게 이루어질 수 있습니다. `VmaAllocator`는 내부적으로 동기화되어 있으며, 빌드 시 동기화를 끌 수도 있지만 기본적으로는 멀티쓰레드 환경에서 안전하게 사용할 수 있습니다. 83 | 84 | 멀티쓰레드 렌더링을 구현하려면 각 쓰레드에서 Secondary 커맨드 버퍼에 렌더링 명령을 기록한 후, 이를 Primary 커맨드 버퍼로 모아 `RenderSync`에서 실행할 수 있습니다. 다만 수백개의 드로우콜 정도는 하나의 쓰레드에서 처리하는 편이 더 빠르기 때문에, 수천개의 비싼 드로우콜과 수십개의 렌더패스를 사용하지 않는 한 사용할 일이 없습니다. 85 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/memory/vbo_quad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/translations/ko-KR/src/memory/vbo_quad.png -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/memory/vertex_buffer.md: -------------------------------------------------------------------------------- 1 | # 정점 버퍼 2 | 3 | 여기서의 목표는 셰이더에 하드코딩되어 있던 정점 정보를 애플리케이션 코드로 옮기는 것입니다. 당분간은 임시로 호스트 메모리에 위치한 `vma::Buffer`를 사용하고, 정점 속성과 같은 나머지 구조에 더 집중하겠습니다. 4 | 5 | 먼저 새로운 헤더 `vertex.hpp`를 추가합니다. 6 | 7 | ```cpp 8 | struct Vertex { 9 | glm::vec2 position{}; 10 | glm::vec3 color{1.0f}; 11 | }; 12 | 13 | // two vertex attributes: position at 0, color at 1. 14 | constexpr auto vertex_attributes_v = std::array{ 15 | // the format matches the type and layout of data: vec2 => 2x 32-bit floats. 16 | vk::VertexInputAttributeDescription2EXT{0, 0, vk::Format::eR32G32Sfloat, 17 | offsetof(Vertex, position)}, 18 | // vec3 => 3x 32-bit floats 19 | vk::VertexInputAttributeDescription2EXT{1, 0, vk::Format::eR32G32B32Sfloat, 20 | offsetof(Vertex, color)}, 21 | }; 22 | 23 | // one vertex binding at location 0. 24 | constexpr auto vertex_bindings_v = std::array{ 25 | // we are using interleaved data with a stride of sizeof(Vertex). 26 | vk::VertexInputBindingDescription2EXT{0, sizeof(Vertex), 27 | vk::VertexInputRate::eVertex, 1}, 28 | }; 29 | ``` 30 | 31 | ShaderCreateInfo에 정점 속성과 바인딩 정보를 추가합니다. 32 | 33 | ```cpp 34 | // ... 35 | static constexpr auto vertex_input_v = ShaderVertexInput{ 36 | .attributes = vertex_attributes_v, 37 | .bindings = vertex_bindings_v, 38 | }; 39 | auto const shader_ci = ShaderProgram::CreateInfo{ 40 | .device = *m_device, 41 | .vertex_spirv = vertex_spirv, 42 | .fragment_spirv = fragment_spirv, 43 | .vertex_input = vertex_input_v, 44 | .set_layouts = {}, 45 | }; 46 | // ... 47 | ``` 48 | 49 | 정점 입력이 정의되었으므로 정점 셰이더를 업데이트하고 다시 컴파일합니다. 50 | 51 | ```glsl 52 | #version 450 core 53 | 54 | layout (location = 0) in vec2 a_pos; 55 | layout (location = 1) in vec3 a_color; 56 | 57 | layout (location = 0) out vec3 out_color; 58 | 59 | void main() { 60 | const vec2 position = a_pos; 61 | 62 | out_color = a_color; 63 | gl_Position = vec4(position, 0.0, 1.0); 64 | } 65 | ``` 66 | 67 | VBO(Vertex Buffer Object) 멤버를 추가하고, 해당 버퍼를 생성합니다. 68 | 69 | ```cpp 70 | void App::create_vertex_buffer() { 71 | // vertices moved from the shader. 72 | static constexpr auto vertices_v = std::array{ 73 | Vertex{.position = {-0.5f, -0.5f}, .color = {1.0f, 0.0f, 0.0f}}, 74 | Vertex{.position = {0.5f, -0.5f}, .color = {0.0f, 1.0f, 0.0f}}, 75 | Vertex{.position = {0.0f, 0.5f}, .color = {0.0f, 0.0f, 1.0f}}, 76 | }; 77 | 78 | // we want to write vertices_v to a Host VertexBuffer. 79 | auto const buffer_ci = vma::BufferCreateInfo{ 80 | .allocator = m_allocator.get(), 81 | .usage = vk::BufferUsageFlagBits::eVertexBuffer, 82 | .queue_family = m_gpu.queue_family, 83 | }; 84 | m_vbo = vma::create_buffer(buffer_ci, vma::BufferMemoryType::Host, 85 | sizeof(vertices_v)); 86 | 87 | // host buffers have a memory-mapped pointer available to memcpy data to. 88 | std::memcpy(m_vbo.get().mapped, vertices_v.data(), sizeof(vertices_v)); 89 | } 90 | ``` 91 | 92 | 드로우 콜을 기록하기 전에 VBO를 바인딩합니다. 93 | 94 | ```cpp 95 | // single VBO at binding 0 at no offset. 96 | command_buffer.bindVertexBuffers(0, m_vbo->get_raw().buffer, 97 | vk::DeviceSize{}); 98 | // m_vbo has 3 vertices. 99 | command_buffer.draw(3, 1, 0, 0); 100 | ``` 101 | 102 | 아마 이전과 동일한 삼각형을 볼 수 있을 것입니다. 하지만 이제는 원하는 정점 데이터를 자유롭게 사용할 수 있습니다. 프리미티브 토폴로지는 기본적으로 Triangle List로 설정하며, 정점 배열에서 매 3개의 정점이 삼각형으로 그려질 것입니다. 예를 들어 정점 9개가 `[[0, 1, 2], [3, 4, 5], [6, 7, 8]]` 있다면, 각 3개의 정점이 하나의 삼각형을 형성하게 됩니다. 정점 데이터와 토폴로지를 다양하게 바꿔보며 실험해 보세요. 예상치 못한 출력이나 버그가 발생한다면 RenderDoc을 사용해 디버깅할 수 있습니다. 103 | 104 | 호스트 정점 버퍼는 UI 객체처럼 임시로 쓰이거나 자주 변경되는 프리미티브에 유용합니다. 2D 프레임워크에서는 이러한 VBO를 독점적으로 사용하는 것도 가능합니다. 예를 들어, 가상 프레임마다 별도의 버퍼 풀을 두고, 각 드로우마다 현재 프레임의 풀에서 버퍼를 하나 가져와 정점을 복사하는 방식이 단순하면서도 효과적입니다. 105 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/memory/vma.md: -------------------------------------------------------------------------------- 1 | # Vulkan Memory Allocator 2 | 3 | VMA는 CMake를 완벽히 지원하지만, 단일 번역 단위에서 정의(instantiate)되어야 하는 단일 헤더 라이브러리이기도 합니다. 이를 관리하기 위해 고유한 `vma::vma` 타겟을 직접 생성하여 소스 파일을 컴파일하겠습니다. 4 | 5 | ```cpp 6 | // vk_mem_alloc.cpp 7 | #define VMA_IMPLEMENTATION 8 | 9 | #include 10 | ``` 11 | 12 | VulkanHPP와는 달리 VMA는 C만을 지원합니다. 따라서 RAII 방식으로 관리하기 위해 `Scope`클래스 템플릿을 사용할 것입니다. 가장 먼저 필요한 것은 `VmaAllocator`으로, 이는 `vk::Device` 혹은 `GLFWwindow*`와 유사한 역할을 합니다. 13 | 14 | ```cpp 15 | // vma.hpp 16 | namespace lvk::vma { 17 | struct Deleter { 18 | void operator()(VmaAllocator allocator) const noexcept; 19 | }; 20 | 21 | using Allocator = Scoped; 22 | 23 | [[nodiscard]] auto create_allocator(vk::Instance instance, 24 | vk::PhysicalDevice physical_device, 25 | vk::Device device) -> Allocator; 26 | } // namespace lvk::vma 27 | 28 | // vma.cpp 29 | void Deleter::operator()(VmaAllocator allocator) const noexcept { 30 | vmaDestroyAllocator(allocator); 31 | } 32 | 33 | // ... 34 | auto vma::create_allocator(vk::Instance const instance, 35 | vk::PhysicalDevice const physical_device, 36 | vk::Device const device) -> Allocator { 37 | auto const& dispatcher = VULKAN_HPP_DEFAULT_DISPATCHER; 38 | // need to zero initialize C structs, unlike VulkanHPP. 39 | auto vma_vk_funcs = VmaVulkanFunctions{}; 40 | vma_vk_funcs.vkGetInstanceProcAddr = dispatcher.vkGetInstanceProcAddr; 41 | vma_vk_funcs.vkGetDeviceProcAddr = dispatcher.vkGetDeviceProcAddr; 42 | 43 | auto allocator_ci = VmaAllocatorCreateInfo{}; 44 | allocator_ci.physicalDevice = physical_device; 45 | allocator_ci.device = device; 46 | allocator_ci.pVulkanFunctions = &vma_vk_funcs; 47 | allocator_ci.instance = instance; 48 | VmaAllocator ret{}; 49 | auto const result = vmaCreateAllocator(&allocator_ci, &ret); 50 | if (result == VK_SUCCESS) { return ret; } 51 | 52 | throw std::runtime_error{"Failed to create Vulkan Memory Allocator"}; 53 | } 54 | ``` 55 | 56 | `App`은 `vma::Allocator` 객체를 생성하고 이를 보관합니다. 57 | 58 | ```cpp 59 | // ... 60 | vma::Allocator m_allocator{}; // anywhere between m_device and m_shader. 61 | 62 | // ... 63 | void App::create_allocator() { 64 | m_allocator = vma::create_allocator(*m_instance, m_gpu.device, *m_device); 65 | } 66 | ``` 67 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/rendering/README.md: -------------------------------------------------------------------------------- 1 | # 렌더링 2 | 3 | 여기서는 렌더 싱크, 스왑체인 루프의 구현, 스왑체인 이미지의 레이아웃 전환을 수행하고, [동적 렌더링(Dynamic Rendering)](https://docs.vulkan.org/samples/latest/samples/extensions/dynamic_rendering/README.html)을 소개합니다. 초기 Vulkan은 [렌더 패스(Render Passes)](https://docs.vulkan.org/tutorial/latest/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.html)만을 지원했습니다. 렌더 패스는 설정이 장황하고, (subpass 의존성 같은) 다소 혼란스러운 요소들을 요구하며, 아이러니하게도 오히려 명시적이지 않은 부분이 있습니다. 예를 들어 렌더패스는 프레임버퍼 어태치먼트의 레이아웃을 암시적으로 전환할 수 있습니다. 또한 렌더 패스는 그래픽스 파이프라인과 밀접하게 결합되어 있어, 다른 모든 조건이 같더라도 렌더 패스마다 별도의 파이프라인 객체가 필요합니다. 이 렌더 패스/서브 패스 모델은 타일 기반 렌더러를 사용하는 GPU에 주로 유리한 모델이었습니다. 그리고 Vulkan 1.3에서는 동적 렌더링이 렌더 패스를 대체하는 핵심 API로 부상했습니다(이전에는 확장일 뿐이었습니다). 4 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/rendering/dynamic_rendering_red_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/translations/ko-KR/src/rendering/dynamic_rendering_red_clear.png -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/rendering/render_sync.md: -------------------------------------------------------------------------------- 1 | # 렌더 싱크 2 | 3 | 새로운 헤더 `resource_buffering.hpp`를 생성합니다. 4 | 5 | ```cpp 6 | // Number of virtual frames. 7 | inline constexpr std::size_t buffering_v{2}; 8 | 9 | // Alias for N-buffered resources. 10 | template 11 | using Buffered = std::array; 12 | ``` 13 | 14 | `App`에 private 멤버 `struct RenderSync`를 추가합니다. 15 | 16 | ```cpp 17 | struct RenderSync { 18 | // signaled when Swapchain image has been acquired. 19 | vk::UniqueSemaphore draw{}; 20 | // signaled with present Semaphore, waited on before next render. 21 | vk::UniqueFence drawn{}; 22 | // used to record rendering commands. 23 | vk::CommandBuffer command_buffer{}; 24 | }; 25 | ``` 26 | 27 | 스왑체인 루프와 관련있는 새로운 멤버를 추가합니다. 28 | 29 | ```cpp 30 | // command pool for all render Command Buffers. 31 | vk::UniqueCommandPool m_render_cmd_pool{}; 32 | // Sync and Command Buffer for virtual frames. 33 | Buffered m_render_sync{}; 34 | // Current virtual frame index. 35 | std::size_t m_frame_index{}; 36 | ``` 37 | 38 | 생성 함수를 추가하고, 구현한 다음 호출합니다. 39 | 40 | ```cpp 41 | void App::create_render_sync() { 42 | // Command Buffers are 'allocated' from a Command Pool (which is 'created' 43 | // like all other Vulkan objects so far). We can allocate all the buffers 44 | // from a single pool here. 45 | auto command_pool_ci = vk::CommandPoolCreateInfo{}; 46 | // this flag enables resetting the command buffer for re-recording (unlike a 47 | // single-time submit scenario). 48 | command_pool_ci.setFlags(vk::CommandPoolCreateFlagBits::eResetCommandBuffer) 49 | .setQueueFamilyIndex(m_gpu.queue_family); 50 | m_render_cmd_pool = m_device->createCommandPoolUnique(command_pool_ci); 51 | 52 | auto command_buffer_ai = vk::CommandBufferAllocateInfo{}; 53 | command_buffer_ai.setCommandPool(*m_render_cmd_pool) 54 | .setCommandBufferCount(static_cast(resource_buffering_v)) 55 | .setLevel(vk::CommandBufferLevel::ePrimary); 56 | auto const command_buffers = 57 | m_device->allocateCommandBuffers(command_buffer_ai); 58 | assert(command_buffers.size() == m_render_sync.size()); 59 | 60 | // we create Render Fences as pre-signaled so that on the first render for 61 | // each virtual frame we don't wait on their fences (since there's nothing 62 | // to wait for yet). 63 | static constexpr auto fence_create_info_v = 64 | vk::FenceCreateInfo{vk::FenceCreateFlagBits::eSignaled}; 65 | for (auto [sync, command_buffer] : 66 | std::views::zip(m_render_sync, command_buffers)) { 67 | sync.command_buffer = command_buffer; 68 | sync.draw = m_device->createSemaphoreUnique({}); 69 | sync.drawn = m_device->createFenceUnique(fence_create_info_v); 70 | } 71 | } 72 | ``` 73 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/rendering/swapchain_loop.md: -------------------------------------------------------------------------------- 1 | # 스왑체인 루프 2 | 3 | 렌더링 루프의 핵심 요소 중 하나는 스왑체인 루프입니다. 이는 다음과 같은 고수준 단계로 구성됩니다. 4 | 5 | 1. 스왑체인으로부터 이미지를 받아옵니다. 6 | 2. 받아온 이미지에 렌더링합니다. 7 | 3. 렌더링이 끝난 이미지를 표시합니다(이미지를 다시 스왑체인으로 돌려줍니다). 8 | 9 | ![WSI Engine](./wsi_engine.png) 10 | 11 | 여기서 몇 가지 고려해야할 점이 있습니다. 12 | 13 | 1. 이미지를 받아오거나 표시하는 과정은 실패할 수 있습니다(스왑체인을 사용할 수 없는 경우). 이 때 남은 단계들은 생략해야 합니다. 14 | 2. 받아오는 명령은 이미지가 실제로 사용할 준비가 되기 전에 반환될 수 있으며, 렌더링은 해당 이미지를 받아온 이후에 시작하도록 동기화되어야 합니다. 15 | 3. 마찬가지로, 표시하는 작업 또한 렌더링이 끝난 이후에 수행되도록 동기화해야 합니다. 16 | 4. 이미지들은 각 단계에 맞는 적절한 레이아웃으로 전환되어야 합니다. 17 | 18 | 또한, 스왑체인의 이미지의 수는 시스템에 따라 달라질 수 있지만, 엔진은 일반적으로 고정된 개수의 가상 프레임을 사용합니다. 더블 버퍼링에는 2개의 가상 프레임, 트리플 버퍼링에는 3개(보통은 3개로 충분합니다). 자세한 내용은 [여기](https://docs.vulkan.org/samples/latest/samples/performance/swapchain_images/README.html#_double_buffering_or_triple_buffering)서 확인할 수 있습니다. 또한 스왑체인이 (Vsync라 알려진)Mailbox Present 모드를 사용중이라면 메인 루프 중에 이전 렌더링 명령이 끝나기 전 동일한 이미지를 가져오는 것도 가능합니다. 19 | 20 | ## 가상 프레임 21 | 22 | 프레임마다 사용되는 모든 동적 자원들은 가상 프레임에 포함됩니다. 애플리케이션은 고정된 개수의 가상 프레임을 가지고 있으며, 매 렌더 패스마다 이를 순환하며 사용합니다. 동기화를 위해 각 프레임은 이전 프레임의 렌더링이 끝날 때 까지 대기하게 만드는 [`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)를 두어 해당 프레임의 (레이아웃 전환을 포함한) 모든 렌더링 명령을 기록할 것입니다. 23 | 24 | 25 | 화면 표시 작업에도 동기화를 위한 세마포어가 필요하지만, 스왑체인 루프는 각 가상 프레임이 처음 사용될 때 미리 시그널되는 drawn 펜스를 기준으로 대기하기 때문에, 표시용 세마포어는 가상 프레임의 일부가 될 수 없습니다. 아직 시그널되지 않은 표시용 세마포어를 사용하여 이미지를 가져오고 커맨드를 제출하는 것도 가능하지만, 이는 유효하지 않은 동작입니다. 따라서 이러한 세마포어는 스왑체인 이미지(인덱스)와 연결되며, 스왑체인이 재생성될 때 함께 재생성됩니다. 26 | 27 | ## 이미지 레이아웃 28 | 29 | 30 | Vulkan 이미지에는 [이미지 레이아웃](https://docs.vulkan.org/spec/latest/chapters/resources.html#resources-image-layouts)이라 알려진 속성이 있습니다. 대부분의 이미지 작업과 이미지의 서브리소스는 특정 레이아웃에서만 수행될 수 있으므로, 작업 전후에 레이아웃 전환이 필요합니다. 레이아웃 전환은 파이프라인 배리어(GPU의 메모리 배리어를 생각하세요)역할도 수행하며, 전환 전후의 작업을 동기화할수 있게 합니다. 31 | 32 | Vulkan 동기화는 아마도 API의 가장 복잡한 부분 중 하나일 것입니다. 충분한 학습이 권장되며, [이 글](https://gpuopen.com/learn/vulkan-barriers-explained/)에서 배리어에 대해 자세히 설명하고 있습니다. 33 | 34 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/rendering/swapchain_update.md: -------------------------------------------------------------------------------- 1 | # 스왑체인 업데이트 2 | 3 | 세마포어 vector를 추가하고 `recreate()`함수를 통해 이를 할당합니다. 4 | 5 | ```cpp 6 | void create_present_semaphores(); 7 | 8 | // ... 9 | // signaled when image is ready to be presented. 10 | std::vector m_present_semaphores{}; 11 | 12 | // ... 13 | auto Swapchain::recreate(glm::ivec2 size) -> bool { 14 | // ... 15 | populate_images(); 16 | create_image_views(); 17 | // recreate present semaphores as the image count might have changed. 18 | create_present_semaphores(); 19 | // ... 20 | } 21 | 22 | void Swapchain::create_present_semaphores() { 23 | m_present_semaphores.clear(); 24 | m_present_semaphores.resize(m_images.size()); 25 | for (auto& semaphore : m_present_semaphores) { 26 | semaphore = m_device.createSemaphoreUnique({}); 27 | } 28 | } 29 | ``` 30 | 31 | 가져온 이미지에 대응되는 표시용 세마포어를 가져오는 함수를 추가합니다. 이는 렌더링 커맨드 버퍼가 제출될 때 시그널 됩니다. 32 | 33 | ```cpp 34 | auto Swapchain::get_present_semaphore() const -> vk::Semaphore { 35 | return *m_present_semaphores.at(m_image_index.value()); 36 | } 37 | ``` 38 | 39 | 스왑체인에서 이미지를 받아오고 표시하는 작업은 다양한 결과를 반환할 수 있습니다. 우리는 다음과 같은 경우로 한정하여 처리하겠습니다. 40 | 41 | - `eSuccess` : 문제가 없습니다. 42 | - `eSuboptimalKHR` : 역시 문제가 없습니다(에러는 아니며, 데스크탑 환경에서는 드물게 발생합니다). 43 | - `eErrorOutOfDateKHR` : 스왑체인을 재생성해야 합니다. 44 | - 그 외의 모든 `vk::Result` : 치명적이거나 예기치 않은 오류입니다. 45 | 46 | `swapchain.cpp`에 함수를 생성합시다. 47 | 48 | ```cpp 49 | auto needs_recreation(vk::Result const result) -> bool { 50 | switch (result) { 51 | case vk::Result::eSuccess: 52 | case vk::Result::eSuboptimalKHR: return false; 53 | case vk::Result::eErrorOutOfDateKHR: return true; 54 | default: break; 55 | } 56 | throw std::runtime_error{"Swapchain Error"}; 57 | } 58 | ``` 59 | 60 | 스왑체인으로부터 이미지를 성공적으로 받아오면 이미지와 이미지 뷰, 그리고 크기를 반환해야 합니다. 이를 `struct`로 감싸겠습니다. 61 | 62 | ```cpp 63 | struct RenderTarget { 64 | vk::Image image{}; 65 | vk::ImageView image_view{}; 66 | vk::Extent2D extent{}; 67 | }; 68 | ``` 69 | 70 | VulkanHPP의 기본 API는 `vk::Result`가 오류에 해당되면 (사양에 따라) 예외를 던집니다. `eErrorOutOfDateKHR`은 기술적으로는 오류이지만, 프레임버퍼와 스왑체인의 크기가 일치하지 않을 때 일어날 수 있습니다. 이러한 예외 처리를 피하기 위해, 우리는 포인터 인자 또는 출력 인자를 사용하는 오버로드 버전 API를 사용하여 `vk::Result`를 직접 반환받는 방식으로 대체하겠습니다. 71 | 72 | 이미지를 가져오는 함수를 작성하겠습니다. 73 | 74 | ```cpp 75 | auto Swapchain::acquire_next_image(vk::Semaphore const to_signal) 76 | -> std::optional { 77 | assert(!m_image_index); 78 | static constexpr auto timeout_v = std::numeric_limits::max(); 79 | // avoid VulkanHPP ErrorOutOfDateKHR exceptions by using alternate API that 80 | // returns a Result. 81 | auto image_index = std::uint32_t{}; 82 | auto const result = m_device.acquireNextImageKHR( 83 | *m_swapchain, timeout_v, to_signal, {}, &image_index); 84 | if (needs_recreation(result)) { return {}; } 85 | 86 | m_image_index = static_cast(image_index); 87 | return RenderTarget{ 88 | .image = m_images.at(*m_image_index), 89 | .image_view = *m_image_views.at(*m_image_index), 90 | .extent = m_ci.imageExtent, 91 | }; 92 | } 93 | ``` 94 | 95 | 표시하는 함수도 마찬가지입니다. 96 | 97 | ```cpp 98 | auto Swapchain::present(vk::Queue const queue, vk::Semaphore const to_wait) 99 | -> bool { 100 | auto const image_index = static_cast(m_image_index.value()); 101 | auto present_info = vk::PresentInfoKHR{}; 102 | present_info.setSwapchains(*m_swapchain) 103 | .setImageIndices(image_index) 104 | .setWaitSemaphores(to_wait); 105 | // avoid VulkanHPP ErrorOutOfDateKHR exceptions by using alternate API. 106 | auto const result = queue.presentKHR(&present_info); 107 | m_image_index.reset(); 108 | return !needs_recreation(result); 109 | } 110 | ``` 111 | 112 | 각 작업에서 `std::nullopt` 혹은 `false`가 반환될 경우, 스왑체인을 재생성하는 것은 사용자(`class App`)의 책임입니다. 사용자는 또한 받아오는 것과 표시하는 작업 사이에 이미지의 레이아웃을 전환해야 합니다. 이 과정을 돕는 함수를 추가하고 공통 상수로 사용할 수 있도록 ImageSubresourceRange 분리해 정의합니다. 113 | 114 | ```cpp 115 | constexpr auto subresource_range_v = [] { 116 | auto ret = vk::ImageSubresourceRange{}; 117 | // this is a color image with 1 layer and 1 mip-level (the default). 118 | ret.setAspectMask(vk::ImageAspectFlagBits::eColor) 119 | .setLayerCount(1) 120 | .setLevelCount(1); 121 | return ret; 122 | }(); 123 | 124 | // ... 125 | auto Swapchain::base_barrier() const -> vk::ImageMemoryBarrier2 { 126 | // fill up the parts common to all barriers. 127 | auto ret = vk::ImageMemoryBarrier2{}; 128 | ret.setImage(m_images.at(m_image_index.value())) 129 | .setSubresourceRange(subresource_range_v) 130 | .setSrcQueueFamilyIndex(m_gpu.queue_family) 131 | .setDstQueueFamilyIndex(m_gpu.queue_family); 132 | return ret; 133 | } 134 | ``` 135 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/rendering/wsi_engine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/translations/ko-KR/src/rendering/wsi_engine.png -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/shader_objects/README.md: -------------------------------------------------------------------------------- 1 | # 셰이더 오브젝트 2 | 3 | [Vulkan의 그래픽스 파이프라인](https://docs.vulkan.org/spec/latest/chapters/pipelines.html)은 전체 렌더링 과정을 아우르는 거대한 객체로, `draw()` 호출 한 번에 여러 단계를 수행합니다. 하지만 [`VK_EXT_shader_object`](https://www.khronos.org/blog/you-can-use-vulkan-without-pipelines-today)라는 확장을 사용하면, 이러한 그래픽스 파이프라인 자체를 완전히 생략할 수 있습니다. 이 확장을 사용할 경우 대부분의 파이프라인 상태가 동적으로 설정되며, 그리는 시점에 설정됩니다. 이때 개발자가 직접 다뤄야할 Vulkan 핸들은 `ShaderEXT` 객체뿐입니다. 더 자세한 정보는 [여기](https://github.com/KhronosGroup/Vulkan-Samples/tree/main/samples/extensions/shader_object)를 참고하세요. 4 | 5 | Vulkan에서는 셰이더 코드를 SPIR-V 형태로 제공해야 합니다. 우리는 Vulkan SDK에 포함된 `glslc`를 사용해 GLSL 코드를 필요할 때 수동으로 SPIR-V 파일로 컴파일하겠습니다. 6 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/shader_objects/drawing_triangle.md: -------------------------------------------------------------------------------- 1 | # 삼각형 그리기 2 | 3 | `App` 클래스에 `ShaderProgram`과 이를 생성하는 함수를 추가합니다. 4 | 5 | ```cpp 6 | [[nodiscard]] auto asset_path(std::string_view uri) const -> fs::path; 7 | 8 | // ... 9 | void create_shader(); 10 | 11 | // ... 12 | std::optional m_shader{}; 13 | ``` 14 | 15 | `asset_path()`와 `create_shader()`를 구현하고 호출합니다. 16 | 17 | ```cpp 18 | void App::create_shader() { 19 | auto const vertex_spirv = to_spir_v(asset_path("shader.vert")); 20 | auto const fragment_spirv = to_spir_v(asset_path("shader.frag")); 21 | auto const shader_ci = ShaderProgram::CreateInfo{ 22 | .device = *m_device, 23 | .vertex_spirv = vertex_spirv, 24 | .fragment_spirv = fragment_spirv, 25 | .vertex_input = {}, 26 | .set_layouts = {}, 27 | }; 28 | m_shader.emplace(shader_ci); 29 | } 30 | 31 | auto App::asset_path(std::string_view const uri) const -> fs::path { 32 | return m_assets_dir / uri; 33 | } 34 | ``` 35 | 36 | `render()`가 걷잡을 수 없이 커지기 전에, 고수준 로직을 두 멤버 함수로 분리합니다. 37 | 38 | ```cpp 39 | // ImGui code goes here. 40 | void inspect(); 41 | // Issue draw calls here. 42 | void draw(vk::CommandBuffer command_buffer) const; 43 | 44 | // ... 45 | void App::inspect() { 46 | ImGui::ShowDemoWindow(); 47 | // TODO 48 | } 49 | 50 | // ... 51 | command_buffer.beginRendering(rendering_info); 52 | inspect(); 53 | draw(command_buffer); 54 | command_buffer.endRendering(); 55 | ``` 56 | 57 | 이제 셰이더를 바인딩하고 이를 삼각형을 그리는 데 사용할 수 있습니다. `draw()`함수를 `const`로 만들어 `App`을 건드리지 않도록 합니다. 58 | 59 | ```cpp 60 | void App::draw(vk::CommandBuffer const command_buffer) const { 61 | m_shader->bind(command_buffer, m_framebuffer_size); 62 | // current shader has hard-coded logic for 3 vertices. 63 | command_buffer.draw(3, 1, 0, 0); 64 | } 65 | ``` 66 | 67 | ![White Triangle](./white_triangle.png) 68 | 69 | 셰이더를 각 정점에 대해 보간된 RGB를 사용하도록 업데이트합니다. 70 | 71 | ```glsl 72 | // shader.vert 73 | 74 | layout (location = 0) out vec3 out_color; 75 | 76 | // ... 77 | const vec3 colors[] = { 78 | vec3(1.0, 0.0, 0.0), 79 | vec3(0.0, 1.0, 0.0), 80 | vec3(0.0, 0.0, 1.0), 81 | }; 82 | 83 | // ... 84 | out_color = colors[gl_VertexIndex]; 85 | 86 | // shader.frag 87 | 88 | layout (location = 0) in vec3 in_color; 89 | 90 | // ... 91 | out_color = vec4(in_color, 1.0); 92 | ``` 93 | 94 | > `assets/`에 있는 두 SPIR-V 파일을 다시 컴파일하는 것을 잊지 마세요. 95 | 96 | 그리고 초기화 색상을 검은 색으로 설정합니다. 97 | 98 | ```cpp 99 | // ... 100 | .setClearValue(vk::ClearColorValue{0.0f, 0.0f, 0.0f, 1.0f}); 101 | ``` 102 | 103 | 이제 Vulkan에서 sRGB 포맷으로 표현되는 삼각형을 볼 수 있습니다. 104 | 105 | ![sRGB Triangle](./srgb_triangle.png) 106 | 107 | ## 동적 상태 변경하기 108 | 109 | ImGui 창을 사용해 파이프라인 상태를 관찰하거나 일부 설정을 변경할 수 있습니다. 110 | 111 | ```cpp 112 | ImGui::SetNextWindowSize({200.0f, 100.0f}, ImGuiCond_Once); 113 | if (ImGui::Begin("Inspect")) { 114 | if (ImGui::Checkbox("wireframe", &m_wireframe)) { 115 | m_shader->polygon_mode = 116 | m_wireframe ? vk::PolygonMode::eLine : vk::PolygonMode::eFill; 117 | } 118 | if (m_wireframe) { 119 | auto const& line_width_range = 120 | m_gpu.properties.limits.lineWidthRange; 121 | ImGui::SetNextItemWidth(100.0f); 122 | ImGui::DragFloat("line width", &m_shader->line_width, 0.25f, 123 | line_width_range[0], line_width_range[1]); 124 | } 125 | } 126 | ImGui::End(); 127 | ``` 128 | 129 | ![sRGB Triangle (wireframe)](./srgb_triangle_wireframe.png) 130 | 131 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/shader_objects/glsl_to_spir_v.md: -------------------------------------------------------------------------------- 1 | # GLSL 에서 SPIR-V 2 | 3 | 셰이더는 NDC 공간 X축과 Y축에서 -1에서 1까지 작동합니다. 새로운 정점 셰이더의 삼각형 좌표계를 출력하고 이를 `src/glsl/shader.vert`에 저장합니다. 4 | 5 | ```glsl 6 | #version 450 core 7 | 8 | void main() { 9 | const vec2 positions[] = { 10 | vec2(-0.5, -0.5), 11 | vec2(0.5, -0.5), 12 | vec2(0.0, 0.5), 13 | }; 14 | 15 | const vec2 position = positions[gl_VertexIndex]; 16 | 17 | gl_Position = vec4(position, 0.0, 1.0); 18 | } 19 | ``` 20 | 21 | `src/glsl/shader.frag`의 프래그먼트 셰이더는 지금은 흰 색을 출력하기만 할 것입니다. 22 | 23 | ```glsl 24 | #version 450 core 25 | 26 | layout (location = 0) out vec4 out_color; 27 | 28 | void main() { 29 | out_color = vec4(1.0); 30 | } 31 | ``` 32 | 33 | 이 둘을 `assets/`로 컴파일합니다. 34 | 35 | ``` 36 | glslc src/glsl/shader.vert -o assets/shader.vert 37 | glslc src/glsl/shader.frag -o assets/shader.frag 38 | ``` 39 | 40 | > glslc는 Vulkan SDK의 일부입니다. 41 | 42 | ## SPIR-V 불러오기 43 | 44 | SPIR-V 셰이더는 4바이트 단위로 정렬이 되어있는 바이너리 파일입니다. 지금까지 봐왔던 대로, Vulkan API는 `std::uint32_t`의 묶음을 받습니다. 따라서 이러한 종류의 버퍼(단, `std::vector` 혹은 다른 종류의 1바이트 컨테이너는 아닙니다)에 담습니다. 이를 돕는 함수를 `app.cpp`에 추가합니다. 45 | 46 | ```cpp 47 | [[nodiscard]] auto to_spir_v(fs::path const& path) 48 | -> std::vector { 49 | // open the file at the end, to get the total size. 50 | auto file = std::ifstream{path, std::ios::binary | std::ios::ate}; 51 | if (!file.is_open()) { 52 | throw std::runtime_error{ 53 | std::format("Failed to open file: '{}'", path.generic_string())}; 54 | } 55 | 56 | auto const size = file.tellg(); 57 | auto const usize = static_cast(size); 58 | // file data must be uint32 aligned. 59 | if (usize % sizeof(std::uint32_t) != 0) { 60 | throw std::runtime_error{std::format("Invalid SPIR-V size: {}", usize)}; 61 | } 62 | 63 | // seek to the beginning before reading. 64 | file.seekg({}, std::ios::beg); 65 | auto ret = std::vector{}; 66 | ret.resize(usize / sizeof(std::uint32_t)); 67 | void* data = ret.data(); 68 | file.read(static_cast(data), size); 69 | return ret; 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/shader_objects/locating_assets.md: -------------------------------------------------------------------------------- 1 | # 에셋 위치 2 | 3 | 셰이더를 사용하기 전에, 에셋 파일들을 불러와야 합니다. 이를 제대로 수행하려면 우선 에셋들이 위치한 경로를 알아야 합니다. 에셋 경로를 찾는 방법에는 여러 가지가 있지만, 우리는 현재 작업 디렉토리에서 시작하여 상위 디렉토리로 올라가며 특정 하위 폴더(`assets/`)를 찾는 방식을 사용할 것입니다. 이렇게 하면 프로젝트나 빌드 디렉토리 어디에서 `app`이 실행되더라도 `assets/` 디렉토리를 자동으로 찾아 접근할 수 있게 됩니다. 4 | 5 | ``` 6 | . 7 | |-- assets/ 8 | |-- app 9 | |-- build/ 10 | |-- app 11 | |-- out/ 12 | |-- default/Release/ 13 | |-- app 14 | |-- ubsan/Debug/ 15 | |-- app 16 | ``` 17 | 18 | 릴리즈 패키지에서는 일반적으로 실행 파일의 경로를 기준으로 에셋 경로를 설정하며, 상위 경로로 거슬러 올라가는 방식은 사용하지 않는 것이 보통입니다. 작업 경로에 상관없이 패키지에 포함된 에셋은 보통 실행 파일과 같은 위치나 그 주변에 위치하기 때문입니다. 19 | 20 | ## 에셋 경로 21 | : 22 | `App`에 `assets/` 경로를 담을 멤버를 추가합니다. 23 | 24 | ```cpp 25 | namespace fs = std::filesystem; 26 | 27 | // ... 28 | fs::path m_assets_dir{}; 29 | ``` 30 | 31 | 에셋 경로를 찾는 함수를 추가하고, 그 반환값을 `run()` 함수 상단에서 `m_assets_dir`에 저장하세요. 32 | 33 | ```cpp 34 | [[nodiscard]] auto locate_assets_dir() -> fs::path { 35 | // look for '/assets/', starting from the working 36 | // directory and walking up the parent directory tree. 37 | static constexpr std::string_view dir_name_v{"assets"}; 38 | for (auto path = fs::current_path(); 39 | !path.empty() && path.has_parent_path(); path = path.parent_path()) { 40 | auto ret = path / dir_name_v; 41 | if (fs::is_directory(ret)) { return ret; } 42 | } 43 | std::println("[lvk] Warning: could not locate '{}' directory", dir_name_v); 44 | return fs::current_path(); 45 | } 46 | 47 | // ... 48 | m_assets_dir = locate_assets_dir(); 49 | ``` 50 | -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/shader_objects/srgb_triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/translations/ko-KR/src/shader_objects/srgb_triangle.png -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/shader_objects/srgb_triangle_wireframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/translations/ko-KR/src/shader_objects/srgb_triangle_wireframe.png -------------------------------------------------------------------------------- /guide/translations/ko-KR/src/shader_objects/white_triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cpp-gamedev/learn-vulkan/ef5debfe0f5499ebff5ddb635cebb9dfb71e891a/guide/translations/ko-KR/src/shader_objects/white_triangle.png -------------------------------------------------------------------------------- /scripts/format_code.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | [[ ! $(clang-format --version) ]] && exit 1 4 | 5 | [[ ! -d ./src ]] && (echo "Please run script from the project root"; exit 1) 6 | 7 | files=$(find src -name "*.?pp") 8 | 9 | [[ "$files" == "" ]] && (echo "-- No source files found"; exit) 10 | 11 | clang-format -i $files || exit 1 12 | echo -e "-- Formatted Files:\n$files\n" 13 | 14 | exit 15 | -------------------------------------------------------------------------------- /src/app.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace lvk { 17 | namespace fs = std::filesystem; 18 | 19 | class App { 20 | public: 21 | void run(); 22 | 23 | private: 24 | struct RenderSync { 25 | // signaled when Swapchain image has been acquired. 26 | vk::UniqueSemaphore draw{}; 27 | // signaled with present Semaphore, waited on before next render. 28 | vk::UniqueFence drawn{}; 29 | // used to record rendering commands. 30 | vk::CommandBuffer command_buffer{}; 31 | }; 32 | 33 | void create_window(); 34 | void create_instance(); 35 | void create_surface(); 36 | void select_gpu(); 37 | void create_device(); 38 | void create_swapchain(); 39 | void create_render_sync(); 40 | void create_imgui(); 41 | void create_allocator(); 42 | void create_descriptor_pool(); 43 | void create_pipeline_layout(); 44 | void create_shader(); 45 | void create_cmd_block_pool(); 46 | void create_shader_resources(); 47 | void create_descriptor_sets(); 48 | 49 | [[nodiscard]] auto asset_path(std::string_view uri) const -> fs::path; 50 | [[nodiscard]] auto create_command_block() const -> CommandBlock; 51 | [[nodiscard]] auto allocate_sets() const -> std::vector; 52 | 53 | void main_loop(); 54 | 55 | auto acquire_render_target() -> bool; 56 | auto begin_frame() -> vk::CommandBuffer; 57 | void transition_for_render(vk::CommandBuffer command_buffer) const; 58 | void render(vk::CommandBuffer command_buffer); 59 | void transition_for_present(vk::CommandBuffer command_buffer) const; 60 | void submit_and_present(); 61 | 62 | // ImGui code goes here. 63 | void inspect(); 64 | void update_view(); 65 | void update_instances(); 66 | // Issue draw calls here. 67 | void draw(vk::CommandBuffer command_buffer) const; 68 | 69 | void bind_descriptor_sets(vk::CommandBuffer command_buffer) const; 70 | 71 | fs::path m_assets_dir{}; 72 | 73 | // the order of these RAII members is crucially important. 74 | glfw::Window m_window{}; 75 | vk::UniqueInstance m_instance{}; 76 | vk::UniqueSurfaceKHR m_surface{}; 77 | Gpu m_gpu{}; // not an RAII member. 78 | vk::UniqueDevice m_device{}; 79 | vk::Queue m_queue{}; // not an RAII member. 80 | vma::Allocator m_allocator{}; // anywhere between m_device and m_shader. 81 | 82 | std::optional m_swapchain{}; 83 | // command pool for all render Command Buffers. 84 | vk::UniqueCommandPool m_render_cmd_pool{}; 85 | // command pool for all Command Blocks. 86 | vk::UniqueCommandPool m_cmd_block_pool{}; 87 | // Sync and Command Buffer for virtual frames. 88 | Buffered m_render_sync{}; 89 | // Current virtual frame index. 90 | std::size_t m_frame_index{}; 91 | 92 | std::optional m_imgui{}; 93 | 94 | vk::UniqueDescriptorPool m_descriptor_pool{}; 95 | std::vector m_set_layouts{}; 96 | std::vector m_set_layout_views{}; 97 | vk::UniquePipelineLayout m_pipeline_layout{}; 98 | 99 | std::optional m_shader{}; 100 | 101 | vma::Buffer m_vbo{}; 102 | std::optional m_view_ubo{}; 103 | std::optional m_texture{}; 104 | std::vector m_instance_data{}; // model matrices. 105 | std::optional m_instance_ssbo{}; 106 | Buffered> m_descriptor_sets{}; 107 | 108 | glm::ivec2 m_framebuffer_size{}; 109 | std::optional m_render_target{}; 110 | bool m_wireframe{}; 111 | 112 | Transform m_view_transform{}; // generates view matrix. 113 | std::array m_instances{}; // generates model matrices. 114 | 115 | // waiter must be the last member to ensure it blocks until device is idle 116 | // before other members get destroyed. 117 | ScopedWaiter m_waiter{}; 118 | }; 119 | } // namespace lvk 120 | -------------------------------------------------------------------------------- /src/bitmap.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | namespace lvk { 7 | struct Bitmap { 8 | std::span bytes{}; 9 | glm::ivec2 size{}; 10 | }; 11 | } // namespace lvk 12 | -------------------------------------------------------------------------------- /src/command_block.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace lvk { 6 | using namespace std::chrono_literals; 7 | 8 | CommandBlock::CommandBlock(vk::Device const device, vk::Queue const queue, 9 | vk::CommandPool const command_pool) 10 | : m_device(device), m_queue(queue) { 11 | // allocate a UniqueCommandBuffer which will free the underlying command 12 | // buffer from its owning pool on destruction. 13 | auto allocate_info = vk::CommandBufferAllocateInfo{}; 14 | allocate_info.setCommandPool(command_pool) 15 | .setCommandBufferCount(1) 16 | .setLevel(vk::CommandBufferLevel::ePrimary); 17 | // all the current VulkanHPP functions for UniqueCommandBuffer allocation 18 | // return vectors. 19 | auto command_buffers = m_device.allocateCommandBuffersUnique(allocate_info); 20 | m_command_buffer = std::move(command_buffers.front()); 21 | 22 | // start recording commands before returning. 23 | auto begin_info = vk::CommandBufferBeginInfo{}; 24 | begin_info.setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit); 25 | m_command_buffer->begin(begin_info); 26 | } 27 | 28 | void CommandBlock::submit_and_wait() { 29 | if (!m_command_buffer) { return; } 30 | 31 | // end recording and submit. 32 | m_command_buffer->end(); 33 | auto submit_info = vk::SubmitInfo2KHR{}; 34 | auto const command_buffer_info = 35 | vk::CommandBufferSubmitInfo{*m_command_buffer}; 36 | submit_info.setCommandBufferInfos(command_buffer_info); 37 | auto fence = m_device.createFenceUnique({}); 38 | m_queue.submit2(submit_info, *fence); 39 | 40 | // wait for submit fence to be signaled. 41 | static constexpr auto timeout_v = 42 | static_cast(std::chrono::nanoseconds(30s).count()); 43 | auto const result = m_device.waitForFences(*fence, vk::True, timeout_v); 44 | if (result != vk::Result::eSuccess) { 45 | std::println(stderr, "Failed to submit Command Buffer"); 46 | } 47 | // free the command buffer. 48 | m_command_buffer.reset(); 49 | } 50 | } // namespace lvk 51 | -------------------------------------------------------------------------------- /src/command_block.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace lvk { 5 | class CommandBlock { 6 | public: 7 | explicit CommandBlock(vk::Device device, vk::Queue queue, 8 | vk::CommandPool command_pool); 9 | 10 | [[nodiscard]] auto command_buffer() const -> vk::CommandBuffer { 11 | return *m_command_buffer; 12 | } 13 | 14 | void submit_and_wait(); 15 | 16 | private: 17 | vk::Device m_device{}; 18 | vk::Queue m_queue{}; 19 | vk::UniqueCommandBuffer m_command_buffer{}; 20 | }; 21 | } // namespace lvk 22 | -------------------------------------------------------------------------------- /src/dear_imgui.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace lvk { 10 | DearImGui::DearImGui(CreateInfo const& create_info) { 11 | IMGUI_CHECKVERSION(); 12 | ImGui::CreateContext(); 13 | 14 | static auto const load_vk_func = +[](char const* name, void* user_data) { 15 | return VULKAN_HPP_DEFAULT_DISPATCHER.vkGetInstanceProcAddr( 16 | *static_cast(user_data), name); 17 | }; 18 | auto instance = create_info.instance; 19 | ImGui_ImplVulkan_LoadFunctions(create_info.api_version, load_vk_func, 20 | &instance); 21 | 22 | if (!ImGui_ImplGlfw_InitForVulkan(create_info.window, true)) { 23 | throw std::runtime_error{"Failed to initialize Dear ImGui"}; 24 | } 25 | 26 | auto init_info = ImGui_ImplVulkan_InitInfo{}; 27 | init_info.ApiVersion = create_info.api_version; 28 | init_info.Instance = create_info.instance; 29 | init_info.PhysicalDevice = create_info.physical_device; 30 | init_info.Device = create_info.device; 31 | init_info.QueueFamily = create_info.queue_family; 32 | init_info.Queue = create_info.queue; 33 | init_info.MinImageCount = 2; 34 | init_info.ImageCount = static_cast(resource_buffering_v); 35 | init_info.MSAASamples = 36 | static_cast(create_info.samples); 37 | init_info.DescriptorPoolSize = 2; 38 | auto pipline_rendering_ci = vk::PipelineRenderingCreateInfo{}; 39 | pipline_rendering_ci.setColorAttachmentCount(1).setColorAttachmentFormats( 40 | create_info.color_format); 41 | init_info.PipelineRenderingCreateInfo = pipline_rendering_ci; 42 | init_info.UseDynamicRendering = true; 43 | if (!ImGui_ImplVulkan_Init(&init_info)) { 44 | throw std::runtime_error{"Failed to initialize Dear ImGui"}; 45 | } 46 | ImGui_ImplVulkan_CreateFontsTexture(); 47 | 48 | ImGui::StyleColorsDark(); 49 | // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay) 50 | for (auto& colour : ImGui::GetStyle().Colors) { 51 | auto const linear = glm::convertSRGBToLinear( 52 | glm::vec4{colour.x, colour.y, colour.z, colour.w}); 53 | colour = ImVec4{linear.x, linear.y, linear.z, linear.w}; 54 | } 55 | ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = 0.99f; // more opaque 56 | 57 | m_device = Scoped{create_info.device}; 58 | } 59 | 60 | void DearImGui::new_frame() { 61 | if (m_state == State::Begun) { end_frame(); } 62 | ImGui_ImplGlfw_NewFrame(); 63 | ImGui_ImplVulkan_NewFrame(); 64 | ImGui::NewFrame(); 65 | m_state = State::Begun; 66 | } 67 | 68 | void DearImGui::end_frame() { 69 | if (m_state == State::Ended) { return; } 70 | ImGui::Render(); 71 | m_state = State::Ended; 72 | } 73 | 74 | // NOLINTNEXTLINE(readability-convert-member-functions-to-static) 75 | void DearImGui::render(vk::CommandBuffer const command_buffer) const { 76 | auto* data = ImGui::GetDrawData(); 77 | if (data == nullptr) { return; } 78 | ImGui_ImplVulkan_RenderDrawData(data, command_buffer); 79 | } 80 | 81 | void DearImGui::Deleter::operator()(vk::Device const device) const { 82 | device.waitIdle(); 83 | ImGui_ImplVulkan_DestroyFontsTexture(); 84 | ImGui_ImplVulkan_Shutdown(); 85 | ImGui_ImplGlfw_Shutdown(); 86 | ImGui::DestroyContext(); 87 | } 88 | } // namespace lvk 89 | -------------------------------------------------------------------------------- /src/dear_imgui.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace lvk { 9 | struct DearImGuiCreateInfo { 10 | GLFWwindow* window{}; 11 | std::uint32_t api_version{}; 12 | vk::Instance instance{}; 13 | vk::PhysicalDevice physical_device{}; 14 | std::uint32_t queue_family{}; 15 | vk::Device device{}; 16 | vk::Queue queue{}; 17 | vk::Format color_format{}; // single color attachment. 18 | vk::SampleCountFlagBits samples{}; 19 | }; 20 | 21 | class DearImGui { 22 | public: 23 | using CreateInfo = DearImGuiCreateInfo; 24 | 25 | explicit DearImGui(CreateInfo const& create_info); 26 | 27 | void new_frame(); 28 | void end_frame(); 29 | void render(vk::CommandBuffer command_buffer) const; 30 | 31 | private: 32 | enum class State : std::int8_t { Ended, Begun }; 33 | 34 | struct Deleter { 35 | void operator()(vk::Device device) const; 36 | }; 37 | 38 | State m_state{}; 39 | 40 | Scoped m_device{}; 41 | }; 42 | } // namespace lvk 43 | -------------------------------------------------------------------------------- /src/descriptor_buffer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace lvk { 4 | DescriptorBuffer::DescriptorBuffer(VmaAllocator allocator, 5 | std::uint32_t const queue_family, 6 | vk::BufferUsageFlags const usage) 7 | : m_allocator(allocator), m_queue_family(queue_family), m_usage(usage) { 8 | // ensure buffers are created and can be bound after returning. 9 | for (auto& buffer : m_buffers) { write_to(buffer, {}); } 10 | } 11 | 12 | void DescriptorBuffer::write_at(std::size_t const frame_index, 13 | std::span bytes) { 14 | write_to(m_buffers.at(frame_index), bytes); 15 | } 16 | 17 | auto DescriptorBuffer::descriptor_info_at(std::size_t const frame_index) const 18 | -> vk::DescriptorBufferInfo { 19 | auto const& buffer = m_buffers.at(frame_index); 20 | auto ret = vk::DescriptorBufferInfo{}; 21 | ret.setBuffer(buffer.buffer.get().buffer).setRange(buffer.size); 22 | return ret; 23 | } 24 | 25 | void DescriptorBuffer::write_to(Buffer& out, 26 | std::span bytes) const { 27 | static constexpr auto blank_byte_v = std::array{std::byte{}}; 28 | // fallback to an empty byte if bytes is empty. 29 | if (bytes.empty()) { bytes = blank_byte_v; } 30 | out.size = bytes.size(); 31 | if (out.buffer.get().size < bytes.size()) { 32 | // size is too small (or buffer doesn't exist yet), recreate buffer. 33 | auto const buffer_ci = vma::BufferCreateInfo{ 34 | .allocator = m_allocator, 35 | .usage = m_usage, 36 | .queue_family = m_queue_family, 37 | }; 38 | out.buffer = vma::create_buffer(buffer_ci, vma::BufferMemoryType::Host, 39 | out.size); 40 | } 41 | std::memcpy(out.buffer.get().mapped, bytes.data(), bytes.size()); 42 | } 43 | } // namespace lvk 44 | -------------------------------------------------------------------------------- /src/descriptor_buffer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | namespace lvk { 7 | class DescriptorBuffer { 8 | public: 9 | explicit DescriptorBuffer(VmaAllocator allocator, 10 | std::uint32_t queue_family, 11 | vk::BufferUsageFlags usage); 12 | 13 | void write_at(std::size_t frame_index, std::span bytes); 14 | 15 | [[nodiscard]] auto descriptor_info_at(std::size_t frame_index) const 16 | -> vk::DescriptorBufferInfo; 17 | 18 | private: 19 | struct Buffer { 20 | vma::Buffer buffer{}; 21 | vk::DeviceSize size{}; 22 | }; 23 | 24 | void write_to(Buffer& out, std::span bytes) const; 25 | 26 | VmaAllocator m_allocator{}; 27 | std::uint32_t m_queue_family{}; 28 | vk::BufferUsageFlags m_usage{}; 29 | Buffered m_buffers{}; 30 | }; 31 | } // namespace lvk 32 | -------------------------------------------------------------------------------- /src/glsl/shader.frag: -------------------------------------------------------------------------------- 1 | #version 450 core 2 | 3 | layout (set = 1, binding = 0) uniform sampler2D tex; 4 | 5 | layout (location = 0) in vec3 in_color; 6 | layout (location = 1) in vec2 in_uv; 7 | 8 | layout (location = 0) out vec4 out_color; 9 | 10 | void main() { 11 | out_color = vec4(in_color, 1.0) * texture(tex, in_uv); 12 | } 13 | -------------------------------------------------------------------------------- /src/glsl/shader.vert: -------------------------------------------------------------------------------- 1 | #version 450 core 2 | 3 | layout (location = 0) in vec2 a_pos; 4 | layout (location = 1) in vec3 a_color; 5 | layout (location = 2) in vec2 a_uv; 6 | 7 | layout (set = 0, binding = 0) uniform View { 8 | mat4 mat_vp; 9 | }; 10 | 11 | layout (set = 2, binding = 0) readonly buffer Instances { 12 | mat4 mat_ms[]; 13 | }; 14 | 15 | layout (location = 0) out vec3 out_color; 16 | layout (location = 1) out vec2 out_uv; 17 | 18 | void main() { 19 | const mat4 mat_m = mat_ms[gl_InstanceIndex]; 20 | const vec4 world_pos = mat_m * vec4(a_pos, 0.0, 1.0); 21 | 22 | out_color = a_color; 23 | out_uv = a_uv; 24 | gl_Position = mat_vp * world_pos; 25 | } 26 | -------------------------------------------------------------------------------- /src/gpu.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | auto lvk::get_suitable_gpu(vk::Instance const instance, 7 | vk::SurfaceKHR const surface) -> Gpu { 8 | auto const supports_swapchain = [](Gpu const& gpu) { 9 | static constexpr std::string_view name_v = 10 | VK_KHR_SWAPCHAIN_EXTENSION_NAME; 11 | static constexpr auto is_swapchain = 12 | [](vk::ExtensionProperties const& properties) { 13 | return properties.extensionName.data() == name_v; 14 | }; 15 | auto const properties = gpu.device.enumerateDeviceExtensionProperties(); 16 | auto const it = std::ranges::find_if(properties, is_swapchain); 17 | return it != properties.end(); 18 | }; 19 | 20 | auto const set_queue_family = [](Gpu& out_gpu) { 21 | static constexpr auto queue_flags_v = 22 | vk::QueueFlagBits::eGraphics | vk::QueueFlagBits::eTransfer; 23 | for (auto const [index, family] : 24 | std::views::enumerate(out_gpu.device.getQueueFamilyProperties())) { 25 | if ((family.queueFlags & queue_flags_v) == queue_flags_v) { 26 | out_gpu.queue_family = static_cast(index); 27 | return true; 28 | } 29 | } 30 | return false; 31 | }; 32 | 33 | auto const can_present = [surface](Gpu const& gpu) { 34 | return gpu.device.getSurfaceSupportKHR(gpu.queue_family, surface) == 35 | vk::True; 36 | }; 37 | 38 | auto fallback = Gpu{}; 39 | for (auto const& device : instance.enumeratePhysicalDevices()) { 40 | auto gpu = Gpu{.device = device, .properties = device.getProperties()}; 41 | if (gpu.properties.apiVersion < vk_version_v) { continue; } 42 | if (!supports_swapchain(gpu)) { continue; } 43 | if (!set_queue_family(gpu)) { continue; } 44 | if (!can_present(gpu)) { continue; } 45 | gpu.features = gpu.device.getFeatures(); 46 | if (gpu.properties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) { 47 | return gpu; 48 | } 49 | // keep iterating in case we find a Discrete Gpu later. 50 | fallback = gpu; 51 | } 52 | if (fallback.device) { return fallback; } 53 | 54 | throw std::runtime_error{"No suitable Vulkan Physical Devices"}; 55 | } 56 | -------------------------------------------------------------------------------- /src/gpu.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace lvk { 5 | constexpr auto vk_version_v = VK_MAKE_VERSION(1, 3, 0); 6 | 7 | struct Gpu { 8 | vk::PhysicalDevice device{}; 9 | vk::PhysicalDeviceProperties properties{}; 10 | vk::PhysicalDeviceFeatures features{}; 11 | std::uint32_t queue_family{}; 12 | }; 13 | 14 | [[nodiscard]] auto get_suitable_gpu(vk::Instance instance, 15 | vk::SurfaceKHR surface) -> Gpu; 16 | } // namespace lvk 17 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | auto main(int argc, char** argv) -> int { 7 | try { 8 | // skip the first argument. 9 | auto args = std::span{argv, static_cast(argc)}.subspan(1); 10 | while (!args.empty()) { 11 | auto const arg = std::string_view{args.front()}; 12 | if (arg == "-x" || arg == "--force-x11") { 13 | glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11); 14 | } 15 | args = args.subspan(1); 16 | } 17 | lvk::App{}.run(); 18 | } catch (std::exception const& e) { 19 | std::println(stderr, "PANIC: {}", e.what()); 20 | return EXIT_FAILURE; 21 | } catch (...) { 22 | std::println("PANIC!"); 23 | return EXIT_FAILURE; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/render_target.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace lvk { 5 | struct RenderTarget { 6 | vk::Image image{}; 7 | vk::ImageView image_view{}; 8 | vk::Extent2D extent{}; 9 | }; 10 | } // namespace lvk 11 | -------------------------------------------------------------------------------- /src/resource_buffering.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace lvk { 5 | // Number of virtual frames. 6 | inline constexpr std::size_t resource_buffering_v{2}; 7 | 8 | // Alias for N-buffered resources. 9 | template 10 | using Buffered = std::array; 11 | } // namespace lvk 12 | -------------------------------------------------------------------------------- /src/scoped.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace lvk { 6 | template 7 | concept Scopeable = std::equality_comparable; 8 | 9 | template 10 | class Scoped { 11 | public: 12 | Scoped(Scoped const&) = delete; 13 | auto operator=(Scoped const&) = delete; 14 | 15 | Scoped() = default; 16 | 17 | constexpr Scoped(Scoped&& rhs) noexcept 18 | : m_t(std::exchange(rhs.m_t, Type{})) {} 19 | 20 | constexpr auto operator=(Scoped&& rhs) noexcept -> Scoped& { 21 | if (&rhs != this) { std::swap(m_t, rhs.m_t); } 22 | return *this; 23 | } 24 | 25 | explicit(false) constexpr Scoped(Type t) : m_t(std::move(t)) {} 26 | 27 | constexpr ~Scoped() { 28 | if (m_t == Type{}) { return; } 29 | Deleter{}(m_t); 30 | } 31 | 32 | [[nodiscard]] constexpr auto get() const -> Type const& { return m_t; } 33 | [[nodiscard]] constexpr auto get() -> Type& { return m_t; } 34 | 35 | private: 36 | Type m_t{}; 37 | }; 38 | } // namespace lvk 39 | -------------------------------------------------------------------------------- /src/scoped_waiter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace lvk { 6 | struct ScopedWaiterDeleter { 7 | void operator()(vk::Device const device) const noexcept { 8 | device.waitIdle(); 9 | } 10 | }; 11 | 12 | using ScopedWaiter = Scoped; 13 | } // namespace lvk 14 | -------------------------------------------------------------------------------- /src/shader_program.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace lvk { 5 | namespace { 6 | constexpr auto to_vkbool(bool const value) { 7 | return value ? vk::True : vk::False; 8 | } 9 | } // namespace 10 | 11 | ShaderProgram::ShaderProgram(CreateInfo const& create_info) 12 | : m_vertex_input(create_info.vertex_input) { 13 | auto const create_shader_ci = 14 | [&create_info](std::span spirv) { 15 | auto ret = vk::ShaderCreateInfoEXT{}; 16 | ret.setCodeSize(spirv.size_bytes()) 17 | .setPCode(spirv.data()) 18 | // set common parameters. 19 | .setSetLayouts(create_info.set_layouts) 20 | .setCodeType(vk::ShaderCodeTypeEXT::eSpirv) 21 | .setPName("main"); 22 | return ret; 23 | }; 24 | 25 | auto shader_cis = std::array{ 26 | create_shader_ci(create_info.vertex_spirv), 27 | create_shader_ci(create_info.fragment_spirv), 28 | }; 29 | shader_cis[0] 30 | .setStage(vk::ShaderStageFlagBits::eVertex) 31 | .setNextStage(vk::ShaderStageFlagBits::eFragment); 32 | shader_cis[1].setStage(vk::ShaderStageFlagBits::eFragment); 33 | 34 | auto result = create_info.device.createShadersEXTUnique(shader_cis); 35 | if (result.result != vk::Result::eSuccess) { 36 | throw std::runtime_error{"Failed to create Shader Objects"}; 37 | } 38 | m_shaders = std::move(result.value); 39 | m_waiter = create_info.device; 40 | } 41 | 42 | void ShaderProgram::bind(vk::CommandBuffer const command_buffer, 43 | glm::ivec2 const framebuffer_size) const { 44 | set_viewport_scissor(command_buffer, framebuffer_size); 45 | set_static_states(command_buffer); 46 | set_common_states(command_buffer); 47 | set_vertex_states(command_buffer); 48 | set_fragment_states(command_buffer); 49 | bind_shaders(command_buffer); 50 | } 51 | 52 | void ShaderProgram::set_viewport_scissor(vk::CommandBuffer const command_buffer, 53 | glm::ivec2 const framebuffer_size) { 54 | auto const fsize = glm::vec2{framebuffer_size}; 55 | auto viewport = vk::Viewport{}; 56 | // flip the viewport about the X-axis (negative height): 57 | // https://www.saschawillems.de/blog/2019/03/29/flipping-the-vulkan-viewport/ 58 | viewport.setX(0.0f).setY(fsize.y).setWidth(fsize.x).setHeight(-fsize.y); 59 | command_buffer.setViewportWithCount(viewport); 60 | 61 | auto const usize = glm::uvec2{framebuffer_size}; 62 | auto const scissor = 63 | vk::Rect2D{vk::Offset2D{}, vk::Extent2D{usize.x, usize.y}}; 64 | command_buffer.setScissorWithCount(scissor); 65 | } 66 | 67 | void ShaderProgram::set_static_states(vk::CommandBuffer const command_buffer) { 68 | command_buffer.setRasterizerDiscardEnable(vk::False); 69 | command_buffer.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1); 70 | command_buffer.setSampleMaskEXT(vk::SampleCountFlagBits::e1, 0xff); 71 | command_buffer.setAlphaToCoverageEnableEXT(vk::False); 72 | command_buffer.setCullMode(vk::CullModeFlagBits::eNone); 73 | command_buffer.setFrontFace(vk::FrontFace::eCounterClockwise); 74 | command_buffer.setDepthBiasEnable(vk::False); 75 | command_buffer.setStencilTestEnable(vk::False); 76 | command_buffer.setPrimitiveRestartEnable(vk::False); 77 | command_buffer.setColorWriteMaskEXT(0, ~vk::ColorComponentFlags{}); 78 | } 79 | 80 | void ShaderProgram::set_common_states( 81 | vk::CommandBuffer const command_buffer) const { 82 | auto const depth_test = to_vkbool((flags & DepthTest) == DepthTest); 83 | command_buffer.setDepthWriteEnable(depth_test); 84 | command_buffer.setDepthTestEnable(depth_test); 85 | command_buffer.setDepthCompareOp(depth_compare_op); 86 | command_buffer.setPolygonModeEXT(polygon_mode); 87 | command_buffer.setLineWidth(line_width); 88 | } 89 | 90 | void ShaderProgram::set_vertex_states( 91 | vk::CommandBuffer const command_buffer) const { 92 | command_buffer.setVertexInputEXT(m_vertex_input.bindings, 93 | m_vertex_input.attributes); 94 | command_buffer.setPrimitiveTopology(topology); 95 | } 96 | 97 | void ShaderProgram::set_fragment_states( 98 | vk::CommandBuffer const command_buffer) const { 99 | auto const alpha_blend = to_vkbool((flags & AlphaBlend) == AlphaBlend); 100 | command_buffer.setColorBlendEnableEXT(0, alpha_blend); 101 | command_buffer.setColorBlendEquationEXT(0, color_blend_equation); 102 | } 103 | 104 | void ShaderProgram::bind_shaders(vk::CommandBuffer const command_buffer) const { 105 | static constexpr auto stages_v = std::array{ 106 | vk::ShaderStageFlagBits::eVertex, 107 | vk::ShaderStageFlagBits::eFragment, 108 | }; 109 | auto const shaders = std::array{ 110 | *m_shaders[0], 111 | *m_shaders[1], 112 | }; 113 | command_buffer.bindShadersEXT(stages_v, shaders); 114 | } 115 | } // namespace lvk 116 | -------------------------------------------------------------------------------- /src/shader_program.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | namespace lvk { 7 | // vertex attributes and bindings. 8 | struct ShaderVertexInput { 9 | std::span attributes{}; 10 | std::span bindings{}; 11 | }; 12 | 13 | struct ShaderProgramCreateInfo { 14 | vk::Device device; 15 | std::span vertex_spirv; 16 | std::span fragment_spirv; 17 | ShaderVertexInput vertex_input; 18 | std::span set_layouts; 19 | }; 20 | 21 | class ShaderProgram { 22 | public: 23 | // bit flags for various binary states. 24 | enum : std::uint8_t { 25 | None = 0, 26 | AlphaBlend = 1 << 0, // turn on alpha blending. 27 | DepthTest = 1 << 1, // turn on depth write and test. 28 | }; 29 | 30 | static constexpr auto color_blend_equation_v = [] { 31 | auto ret = vk::ColorBlendEquationEXT{}; 32 | ret.setColorBlendOp(vk::BlendOp::eAdd) 33 | // standard alpha blending: 34 | // (alpha * src) + (1 - alpha) * dst 35 | .setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) 36 | .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha); 37 | return ret; 38 | }(); 39 | 40 | static constexpr auto flags_v = AlphaBlend | DepthTest; 41 | 42 | using CreateInfo = ShaderProgramCreateInfo; 43 | 44 | explicit ShaderProgram(CreateInfo const& create_info); 45 | 46 | void bind(vk::CommandBuffer command_buffer, 47 | glm::ivec2 framebuffer_size) const; 48 | 49 | vk::PrimitiveTopology topology{vk::PrimitiveTopology::eTriangleList}; 50 | vk::PolygonMode polygon_mode{vk::PolygonMode::eFill}; 51 | float line_width{1.0f}; 52 | vk::ColorBlendEquationEXT color_blend_equation{color_blend_equation_v}; 53 | vk::CompareOp depth_compare_op{vk::CompareOp::eLessOrEqual}; 54 | std::uint8_t flags{flags_v}; 55 | 56 | private: 57 | static void set_viewport_scissor(vk::CommandBuffer command_buffer, 58 | glm::ivec2 framebuffer); 59 | static void set_static_states(vk::CommandBuffer command_buffer); 60 | void set_common_states(vk::CommandBuffer command_buffer) const; 61 | void set_vertex_states(vk::CommandBuffer command_buffer) const; 62 | void set_fragment_states(vk::CommandBuffer command_buffer) const; 63 | void bind_shaders(vk::CommandBuffer command_buffer) const; 64 | 65 | ShaderVertexInput m_vertex_input{}; 66 | std::vector m_shaders{}; 67 | 68 | ScopedWaiter m_waiter{}; 69 | }; 70 | } // namespace lvk 71 | -------------------------------------------------------------------------------- /src/swapchain.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace lvk { 9 | class Swapchain { 10 | public: 11 | explicit Swapchain(vk::Device device, Gpu const& gpu, 12 | vk::SurfaceKHR surface, glm::ivec2 size); 13 | 14 | auto recreate(glm::ivec2 size) -> bool; 15 | 16 | [[nodiscard]] auto get_size() const -> glm::ivec2 { 17 | return {m_ci.imageExtent.width, m_ci.imageExtent.height}; 18 | } 19 | 20 | [[nodiscard]] auto get_format() const -> vk::Format { 21 | return m_ci.imageFormat; 22 | } 23 | 24 | [[nodiscard]] auto acquire_next_image(vk::Semaphore to_signal) 25 | -> std::optional; 26 | 27 | [[nodiscard]] auto base_barrier() const -> vk::ImageMemoryBarrier2; 28 | 29 | [[nodiscard]] auto get_present_semaphore() const -> vk::Semaphore; 30 | [[nodiscard]] auto present(vk::Queue queue) -> bool; 31 | 32 | private: 33 | void populate_images(); 34 | void create_image_views(); 35 | void create_present_semaphores(); 36 | 37 | vk::Device m_device{}; 38 | Gpu m_gpu{}; 39 | 40 | vk::SwapchainCreateInfoKHR m_ci{}; 41 | vk::UniqueSwapchainKHR m_swapchain{}; 42 | std::vector m_images{}; 43 | std::vector m_image_views{}; 44 | // signaled when image is ready to be presented. 45 | std::vector m_present_semaphores{}; 46 | std::optional m_image_index{}; 47 | }; 48 | } // namespace lvk 49 | -------------------------------------------------------------------------------- /src/texture.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace lvk { 5 | namespace { 6 | // 4-channels. 7 | constexpr auto white_pixel_v = std::array{std::byte{0xff}, std::byte{0xff}, 8 | std::byte{0xff}, std::byte{0xff}}; 9 | // fallback bitmap. 10 | constexpr auto white_bitmap_v = Bitmap{ 11 | .bytes = white_pixel_v, 12 | .size = {1, 1}, 13 | }; 14 | } // namespace 15 | 16 | Texture::Texture(CreateInfo create_info) { 17 | if (create_info.bitmap.bytes.empty() || create_info.bitmap.size.x <= 0 || 18 | create_info.bitmap.size.y <= 0) { 19 | create_info.bitmap = white_bitmap_v; 20 | } 21 | 22 | auto const image_ci = vma::ImageCreateInfo{ 23 | .allocator = create_info.allocator, 24 | .queue_family = create_info.queue_family, 25 | }; 26 | m_image = vma::create_sampled_image( 27 | image_ci, std::move(create_info.command_block), create_info.bitmap); 28 | 29 | auto image_view_ci = vk::ImageViewCreateInfo{}; 30 | auto subresource_range = vk::ImageSubresourceRange{}; 31 | subresource_range.setAspectMask(vk::ImageAspectFlagBits::eColor) 32 | .setLayerCount(1) 33 | .setLevelCount(m_image.get().levels); 34 | 35 | image_view_ci.setImage(m_image.get().image) 36 | .setViewType(vk::ImageViewType::e2D) 37 | .setFormat(m_image.get().format) 38 | .setSubresourceRange(subresource_range); 39 | m_view = create_info.device.createImageViewUnique(image_view_ci); 40 | 41 | m_sampler = create_info.device.createSamplerUnique(create_info.sampler); 42 | } 43 | 44 | auto Texture::descriptor_info() const -> vk::DescriptorImageInfo { 45 | auto ret = vk::DescriptorImageInfo{}; 46 | ret.setImageView(*m_view) 47 | .setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal) 48 | .setSampler(*m_sampler); 49 | return ret; 50 | } 51 | } // namespace lvk 52 | -------------------------------------------------------------------------------- /src/texture.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace lvk { 5 | [[nodiscard]] constexpr auto 6 | create_sampler_ci(vk::SamplerAddressMode const wrap, vk::Filter const filter) { 7 | auto ret = vk::SamplerCreateInfo{}; 8 | ret.setAddressModeU(wrap) 9 | .setAddressModeV(wrap) 10 | .setAddressModeW(wrap) 11 | .setMinFilter(filter) 12 | .setMagFilter(filter) 13 | .setMaxLod(VK_LOD_CLAMP_NONE) 14 | .setBorderColor(vk::BorderColor::eFloatTransparentBlack) 15 | .setMipmapMode(vk::SamplerMipmapMode::eNearest); 16 | return ret; 17 | } 18 | 19 | constexpr auto sampler_ci_v = create_sampler_ci( 20 | vk::SamplerAddressMode::eClampToEdge, vk::Filter::eLinear); 21 | 22 | struct TextureCreateInfo { 23 | vk::Device device; 24 | VmaAllocator allocator; 25 | std::uint32_t queue_family; 26 | CommandBlock command_block; 27 | Bitmap bitmap; 28 | 29 | vk::SamplerCreateInfo sampler{sampler_ci_v}; 30 | }; 31 | 32 | class Texture { 33 | public: 34 | using CreateInfo = TextureCreateInfo; 35 | 36 | explicit Texture(CreateInfo create_info); 37 | 38 | [[nodiscard]] auto descriptor_info() const -> vk::DescriptorImageInfo; 39 | 40 | private: 41 | vma::Image m_image{}; 42 | vk::UniqueImageView m_view{}; 43 | vk::UniqueSampler m_sampler{}; 44 | }; 45 | } // namespace lvk 46 | -------------------------------------------------------------------------------- /src/transform.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace lvk { 5 | namespace { 6 | struct Matrices { 7 | glm::mat4 translation; 8 | glm::mat4 orientation; 9 | glm::mat4 scale; 10 | }; 11 | 12 | [[nodiscard]] auto to_matrices(glm::vec2 const position, float rotation, 13 | glm::vec2 const scale) -> Matrices { 14 | static constexpr auto mat_v = glm::identity(); 15 | static constexpr auto axis_v = glm::vec3{0.0f, 0.0f, 1.0f}; 16 | return Matrices{ 17 | .translation = glm::translate(mat_v, glm::vec3{position, 0.0f}), 18 | .orientation = glm::rotate(mat_v, glm::radians(rotation), axis_v), 19 | .scale = glm::scale(mat_v, glm::vec3{scale, 1.0f}), 20 | }; 21 | } 22 | } // namespace 23 | 24 | auto Transform::model_matrix() const -> glm::mat4 { 25 | auto const [t, r, s] = to_matrices(position, rotation, scale); 26 | // right to left: scale first, then rotate, then translate. 27 | return t * r * s; 28 | } 29 | 30 | auto Transform::view_matrix() const -> glm::mat4 { 31 | // view matrix is the inverse of the model matrix. 32 | // instead, perform translation and rotation in reverse order and with 33 | // negative values. or, use glm::lookAt(). 34 | // scale is kept unchanged as the first transformation for 35 | // "intuitive" scaling on cameras. 36 | auto const [t, r, s] = to_matrices(-position, -rotation, scale); 37 | return r * t * s; 38 | } 39 | } // namespace lvk 40 | -------------------------------------------------------------------------------- /src/transform.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | namespace lvk { 6 | struct Transform { 7 | glm::vec2 position{}; 8 | float rotation{}; 9 | glm::vec2 scale{1.0f}; 10 | 11 | [[nodiscard]] auto model_matrix() const -> glm::mat4; 12 | [[nodiscard]] auto view_matrix() const -> glm::mat4; 13 | }; 14 | } // namespace lvk 15 | -------------------------------------------------------------------------------- /src/vertex.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | 6 | namespace lvk { 7 | struct Vertex { 8 | glm::vec2 position{}; 9 | glm::vec3 color{1.0f}; 10 | glm::vec2 uv{}; 11 | }; 12 | 13 | // two vertex attributes: position at 0, color at 1. 14 | constexpr auto vertex_attributes_v = std::array{ 15 | // the format matches the type and layout of data: vec2 => 2x 32-bit floats. 16 | vk::VertexInputAttributeDescription2EXT{0, 0, vk::Format::eR32G32Sfloat, 17 | offsetof(Vertex, position)}, 18 | // vec3 => 3x 32-bit floats 19 | vk::VertexInputAttributeDescription2EXT{1, 0, vk::Format::eR32G32B32Sfloat, 20 | offsetof(Vertex, color)}, 21 | // vec2 => 2x 32-bit floats 22 | vk::VertexInputAttributeDescription2EXT{2, 0, vk::Format::eR32G32Sfloat, 23 | offsetof(Vertex, uv)}, 24 | }; 25 | 26 | // one vertex binding at location 0. 27 | constexpr auto vertex_bindings_v = std::array{ 28 | // we are using interleaved data with a stride of sizeof(Vertex). 29 | vk::VertexInputBindingDescription2EXT{0, sizeof(Vertex), 30 | vk::VertexInputRate::eVertex, 1}, 31 | }; 32 | } // namespace lvk 33 | -------------------------------------------------------------------------------- /src/vma.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace lvk::vma { 9 | struct Deleter { 10 | void operator()(VmaAllocator allocator) const noexcept; 11 | }; 12 | 13 | using Allocator = Scoped; 14 | 15 | [[nodiscard]] auto create_allocator(vk::Instance instance, 16 | vk::PhysicalDevice physical_device, 17 | vk::Device device) -> Allocator; 18 | 19 | struct RawBuffer { 20 | [[nodiscard]] auto mapped_span() const -> std::span { 21 | return std::span{static_cast(mapped), size}; 22 | } 23 | 24 | auto operator==(RawBuffer const& rhs) const -> bool = default; 25 | 26 | VmaAllocator allocator{}; 27 | VmaAllocation allocation{}; 28 | vk::Buffer buffer{}; 29 | vk::DeviceSize size{}; 30 | void* mapped{}; 31 | }; 32 | 33 | struct BufferDeleter { 34 | void operator()(RawBuffer const& raw_buffer) const noexcept; 35 | }; 36 | 37 | using Buffer = Scoped; 38 | 39 | struct BufferCreateInfo { 40 | VmaAllocator allocator; 41 | vk::BufferUsageFlags usage; 42 | std::uint32_t queue_family; 43 | }; 44 | 45 | enum class BufferMemoryType : std::int8_t { Host, Device }; 46 | 47 | [[nodiscard]] auto create_buffer(BufferCreateInfo const& create_info, 48 | BufferMemoryType memory_type, 49 | vk::DeviceSize size) -> Buffer; 50 | 51 | // disparate byte spans. 52 | using ByteSpans = std::span const>; 53 | 54 | // returns a Device Buffer with each byte span sequentially written. 55 | [[nodiscard]] auto create_device_buffer(BufferCreateInfo const& create_info, 56 | CommandBlock command_block, 57 | ByteSpans const& byte_spans) -> Buffer; 58 | 59 | struct RawImage { 60 | auto operator==(RawImage const& rhs) const -> bool = default; 61 | 62 | VmaAllocator allocator{}; 63 | VmaAllocation allocation{}; 64 | vk::Image image{}; 65 | vk::Extent2D extent{}; 66 | vk::Format format{}; 67 | std::uint32_t levels{}; 68 | }; 69 | 70 | struct ImageDeleter { 71 | void operator()(RawImage const& raw_image) const noexcept; 72 | }; 73 | 74 | using Image = Scoped; 75 | 76 | struct ImageCreateInfo { 77 | VmaAllocator allocator; 78 | std::uint32_t queue_family; 79 | }; 80 | 81 | [[nodiscard]] auto create_image(ImageCreateInfo const& create_info, 82 | vk::ImageUsageFlags usage, std::uint32_t levels, 83 | vk::Format format, vk::Extent2D extent) 84 | -> Image; 85 | 86 | [[nodiscard]] auto create_sampled_image(ImageCreateInfo const& create_info, 87 | CommandBlock command_block, 88 | Bitmap const& bitmap) -> Image; 89 | } // namespace lvk::vma 90 | -------------------------------------------------------------------------------- /src/window.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace lvk { 7 | namespace glfw { 8 | void Deleter::operator()(GLFWwindow* window) const noexcept { 9 | glfwDestroyWindow(window); 10 | glfwTerminate(); 11 | } 12 | } // namespace glfw 13 | 14 | auto glfw::create_window(glm::ivec2 const size, char const* title) -> Window { 15 | static auto const on_error = [](int const code, char const* description) { 16 | std::println(stderr, "[GLFW] Error {}: {}", code, description); 17 | }; 18 | glfwSetErrorCallback(on_error); 19 | if (glfwInit() != GLFW_TRUE) { 20 | throw std::runtime_error{"Failed to initialize GLFW"}; 21 | } 22 | // check for Vulkan support. 23 | if (glfwVulkanSupported() != GLFW_TRUE) { 24 | throw std::runtime_error{"Vulkan not supported"}; 25 | } 26 | auto ret = Window{}; 27 | // tell GLFW that we don't want an OpenGL context. 28 | glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); 29 | ret.reset(glfwCreateWindow(size.x, size.y, title, nullptr, nullptr)); 30 | if (!ret) { throw std::runtime_error{"Failed to create GLFW Window"}; } 31 | return ret; 32 | } 33 | 34 | auto glfw::instance_extensions() -> std::span { 35 | auto count = std::uint32_t{}; 36 | auto const* extensions = glfwGetRequiredInstanceExtensions(&count); 37 | return {extensions, static_cast(count)}; 38 | } 39 | 40 | auto glfw::create_surface(GLFWwindow* window, vk::Instance const instance) 41 | -> vk::UniqueSurfaceKHR { 42 | VkSurfaceKHR ret{}; 43 | auto const result = 44 | glfwCreateWindowSurface(instance, window, nullptr, &ret); 45 | if (result != VK_SUCCESS || ret == VkSurfaceKHR{}) { 46 | throw std::runtime_error{"Failed to create Vulkan Surface"}; 47 | } 48 | return vk::UniqueSurfaceKHR{ret, instance}; 49 | } 50 | 51 | auto glfw::framebuffer_size(GLFWwindow* window) -> glm::ivec2 { 52 | auto ret = glm::ivec2{}; 53 | glfwGetFramebufferSize(window, &ret.x, &ret.y); 54 | return ret; 55 | } 56 | } // namespace lvk 57 | -------------------------------------------------------------------------------- /src/window.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace lvk::glfw { 9 | struct Deleter { 10 | void operator()(GLFWwindow* window) const noexcept; 11 | }; 12 | 13 | using Window = std::unique_ptr; 14 | 15 | // Returns a valid Window if successful, else throws. 16 | [[nodiscard]] auto create_window(glm::ivec2 size, char const* title) -> Window; 17 | 18 | [[nodiscard]] auto instance_extensions() -> std::span; 19 | 20 | [[nodiscard]] auto create_surface(GLFWwindow* window, vk::Instance instance) 21 | -> vk::UniqueSurfaceKHR; 22 | 23 | [[nodiscard]] auto framebuffer_size(GLFWwindow* window) -> glm::ivec2; 24 | } // namespace lvk::glfw 25 | --------------------------------------------------------------------------------