diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c08ba18..23c2655 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -24,7 +24,7 @@ jobs: - name: build book run: | cd guide - mdbook build + cmake -P build.cmake || exit 1 ls book - name: setup pages uses: actions/configure-pages@v4 diff --git a/.github/workflows/guide.yml b/.github/workflows/guide.yml index b527594..0400187 100644 --- a/.github/workflows/guide.yml +++ b/.github/workflows/guide.yml @@ -19,5 +19,5 @@ jobs: - name: build run: | cd guide - mdbook build || exit 1 + cmake -P build.cmake || exit 1 ls book diff --git a/guide/book.toml b/guide/book.toml index 87209f2..db0956a 100644 --- a/guide/book.toml +++ b/guide/book.toml @@ -1,6 +1,10 @@ [book] -authors = ["Karn Kaul"] +authors = ["Karnage"] language = "en" -multilingual = false src = "src" title = "Learn Vulkan" + +[output.html] +theme = "theme" +additional-js = ["theme/lang_toggle.js"] +additional-css = ["theme/lang_toggle.css"] diff --git a/guide/build.cmake b/guide/build.cmake new file mode 100644 index 0000000..a5e61a8 --- /dev/null +++ b/guide/build.cmake @@ -0,0 +1,27 @@ +# Build the target languages +function(BuildBook LANGUAGE SOURCE_DIR TARGET_DIR) + set(LANGUAGE "${LANGUAGE}") + + if(NOT EXISTS "${SOURCE_DIR}/src/SUMMARY.md") + message(WARNING "Skipping '${LANGUAGE}' – SUMMARY.md not found at ${SOURCE_DIR}") + return() + endif() + + if(NOT EXISTS "${SOURCE_DIR}/book.toml") + message(WARNING "Skipping '${LANGUAGE}' – book.toml not found at ${SOURCE_DIR}") + return() + endif() + + message(STATUS "Building book for language: ${LANGUAGE}") + execute_process( + COMMAND mdbook build -d ${TARGET_DIR} + WORKING_DIRECTORY ${SOURCE_DIR} + COMMAND_ERROR_IS_FATAL ANY + ) +endfunction() + +# Copy the theme folder +file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/theme" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}/translations") + +BuildBook("en" "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}/book") +BuildBook("ko-KR" "${CMAKE_CURRENT_SOURCE_DIR}/translations/ko-KR" "${CMAKE_CURRENT_SOURCE_DIR}/book/ko-KR") diff --git a/guide/theme/index.hbs b/guide/theme/index.hbs new file mode 100644 index 0000000..afc3145 --- /dev/null +++ b/guide/theme/index.hbs @@ -0,0 +1,332 @@ + + + + + + {{ title }} + {{#if is_print }} + + {{/if}} + {{#if base_url}} + + {{/if}} + + + + {{> head}} + + + + + + {{#if favicon_svg}} + + {{/if}} + {{#if favicon_png}} + + {{/if}} + + + + {{#if print_enable}} + + {{/if}} + + + + {{#if copy_fonts}} + + {{/if}} + + + + + + + + {{#each additional_css}} + + {{/each}} + + {{#if mathjax_support}} + + + {{/if}} + + + + + + + +
+ + + + + + + + + + + + + +
+ +
+ {{> header}} + + + + {{#if search_enabled}} + + {{/if}} + + + + +
+
+ {{{ content }}} +
+ + +
+
+ + + +
+ + {{#if live_reload_endpoint}} + + + {{/if}} + + {{#if google_analytics}} + + + {{/if}} + + {{#if playground_line_numbers}} + + {{/if}} + + {{#if playground_copyable}} + + {{/if}} + + {{#if playground_js}} + + + + + + {{/if}} + + {{#if search_js}} + + + + {{/if}} + + + + + + + {{#each additional_js}} + + {{/each}} + + {{#if is_print}} + {{#if mathjax_support}} + + {{else}} + + {{/if}} + {{/if}} + +
+ + diff --git a/guide/theme/lang_toggle.css b/guide/theme/lang_toggle.css new file mode 100644 index 0000000..72fc3be --- /dev/null +++ b/guide/theme/lang_toggle.css @@ -0,0 +1,13 @@ +/* 1) Make .left-buttons positioned so its child popups can anchor to it */ +.left-buttons { + position: relative; +} + +/* 2) Specifically override #lang-list to appear at the right edge under the button */ +.left-buttons #lang-list.theme-popup { + left: auto; /* remove the default left: 10px */ + right: 0; /* pin to the right edge of .left-buttons */ + top: calc(var(--menu-bar-height) + 4px); + width: auto; /* don’t stretch across the screen */ + white-space: nowrap; /* keep the list from wrapping into multiple lines */ +} diff --git a/guide/theme/lang_toggle.js b/guide/theme/lang_toggle.js new file mode 100644 index 0000000..d5fcc06 --- /dev/null +++ b/guide/theme/lang_toggle.js @@ -0,0 +1,138 @@ +'use strict'; + +(function languages() { + const langToggleButton = document.getElementById('lang-toggle'); + const langPopup = document.getElementById('lang-list'); + + if (!langToggleButton || !langPopup) { + return; // Safety check: if they're missing from the page, do nothing + } + + function showLangs() { + langPopup.style.display = 'block'; + langToggleButton.setAttribute('aria-expanded', 'true'); + } + + function hideLangs() { + langPopup.style.display = 'none'; + langToggleButton.setAttribute('aria-expanded', 'false'); + langToggleButton.focus(); + } + + langToggleButton.addEventListener('click', function() { + if (langPopup.style.display === 'block') { + hideLangs(); + } else { + showLangs(); + } + }); + + document.addEventListener('click', function (e) { + if (e.target && e.target.matches('button[data-lang]')) { + const chosenLang = e.target.getAttribute('data-lang'); + const supportedLangs = ['en', 'zh-TW', 'ko-KR']; // Add translated languages here + + let currentPath = window.location.pathname; + + // Find "book/" in the path + const bookAnchor = 'book/'; + const idx = currentPath.indexOf(bookAnchor); + + if (idx === -1) { + // Fallback: If "book/" isn’t in the path + // Just go to the top-level file in the chosen language. + if (chosenLang === 'en') + window.location.href = 'index.html'; + else + window.location.href = chosenLang + '/index.html'; + + return; + } + + // Everything up to (and including) "book/" + // e.g. "/C:/Users/.../guide/book/" + const base = currentPath.substring(0, idx + bookAnchor.length); + + // The rest "after book/" part + // e.g. "index.html" or "zh-TW/getting_started/foo.html" + let after = currentPath.substring(idx + bookAnchor.length); + + // Split into segments and remove any leading lang if it’s known + // e.g. ["index.html"] or ["en", "getting_started", "foo.html"] + let segments = after.split('/').filter(s => s.length > 0); + + if (segments.length > 0 && supportedLangs.includes(segments[0])) { + // remove the first segment if it’s a supported lang + // e.g. now ["getting_started", "foo.html"] + segments.shift(); + } + + // Insert the chosen language as the first path segment + // Also, English has no prefix + let newPath; + if (chosenLang === 'en') { + // e.g. /C:/Users/.../guide/book/getting_started/foo.html + newPath = base + segments.join('/'); + } else { + // e.g. /C:/Users/.../guide/book/zh-TW/getting_started/foo.html + newPath = base + chosenLang + '/' + segments.join('/'); + } + + window.location.href = newPath; + } + + if ( + langPopup.style.display === 'block' && + !langToggleButton.contains(e.target) && + !langPopup.contains(e.target) + ) { + hideLangs(); + } + }); + + // Also hide if focus goes elsewhere + langPopup.addEventListener('focusout', function(e) { + // e.relatedTarget can be null in some browsers + if (!!e.relatedTarget && + !langToggleButton.contains(e.relatedTarget) && + !langPopup.contains(e.relatedTarget)) { + hideLangs(); + } + }); + + // Optional: Add keyboard navigation (like theme popup) + document.addEventListener('keydown', function(e) { + if (langPopup.style.display !== 'block') return; + if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; + + let li; + switch (e.key) { + case 'Escape': + e.preventDefault(); + hideLangs(); + break; + case 'ArrowUp': + e.preventDefault(); + li = document.activeElement.parentElement; + if (li && li.previousElementSibling) { + li.previousElementSibling.querySelector('a, button').focus(); + } + break; + case 'ArrowDown': + e.preventDefault(); + li = document.activeElement.parentElement; + if (li && li.nextElementSibling) { + li.nextElementSibling.querySelector('a, button').focus(); + } + break; + case 'Home': + e.preventDefault(); + langPopup.querySelector('li:first-child a, li:first-child button').focus(); + break; + case 'End': + e.preventDefault(); + langPopup.querySelector('li:last-child a, li:last-child button').focus(); + break; + } + }); +})(); diff --git a/guide/translations/.gitignore b/guide/translations/.gitignore new file mode 100644 index 0000000..d1e87fb --- /dev/null +++ b/guide/translations/.gitignore @@ -0,0 +1 @@ +theme \ No newline at end of file diff --git a/guide/translations/ko-KR/book.toml b/guide/translations/ko-KR/book.toml new file mode 100644 index 0000000..6fbf26c --- /dev/null +++ b/guide/translations/ko-KR/book.toml @@ -0,0 +1,10 @@ +[book] +authors = ["Karnage", "DDing"] +language = "ko-KR" +src = "src" +title = "Learn Vulkan" + +[output.html] +theme = "../theme" +additional-js = ["../theme/lang_toggle.js"] +additional-css = ["../theme/lang_toggle.css"] diff --git a/guide/translations/ko-KR/src/README.md b/guide/translations/ko-KR/src/README.md new file mode 100644 index 0000000..f3d6bf2 --- /dev/null +++ b/guide/translations/ko-KR/src/README.md @@ -0,0 +1,34 @@ +# 소개 + +Vulkan은 매우 명시적이고 어려운 API로 알려져 있습니다. 하지만 버전이 거듭될수록 새로운 기능이 추가되고 기존 확장 기능들이 핵심 API에 통합되면서, 필수적인 어려움은 점차 줄어들고 있습니다. RAII는 C++의 핵심 개념 중 하나이지만, 대부분의 Vulkan 가이드에서는 이를 제대로 활용하지 않고, 자원을 수동으로 해제하는 방식으로 오히려 명시성을 강조하는 경우가 많습니다. + +이러한 격차를 메우기 위해 이 가이드는 다음과 같은 목표를 가지고 있습니다. + +- 모던 C++, VulkanHPP, Vulkan 1.3 기능을 적극 활용합니다. +- 성능이 아닌, 단순하고 직관적인 접근에 초점을 맞춥니다. +- 기본적인 렌더링 기능을 갖춘 동적 렌더링 기반을 구축합니다. + +다시 한번 말하자면, 이 가이드의 목적은 성능이 아닙니다. 이 가이드는 현대 패러다임과 도구를을 활용해 현재 표준으로 자리잡은 멀티 플랫폼 그래픽스 API를 빠르게 소개하는 데 중점을 둡니다. 성능을 고려하지 않더라도 Vulkan은 OpenGL보다 현대적이고 우수한 설계를 갖추고 있습니다. 예를 들어, Vulkan에는 전역 상태 기계가 없고, 파라미터는 의미있는 멤버로 구성된 구조체를 통해 전달되며, 멀티쓰레딩 역시 상당히 간단하게 구현할 수 있습니다(실제로 OpenGL보다 Vulkan에서 멀티쓰레딩이 더 쉽습니다). 또한, 애플리케이션 코드를 변경하지 않고도 오용을 가밎할 수 있는 강력한 검증 레이어를 활성화할 수 있습니다. + +더 깊이 Vulkan에 대해 학습하고 싶다면 [공식 튜토리얼](https://docs.vulkan.org/tutorial/latest/00_Introduction.html)이 권장됩니다. [vkguide](https://vkguide.dev/)와 [Vulkan Tutorial](https://vulkan-tutorial.com/) 또한 많이 참고되는 자료로, 내용이 매우 자세하게 정리되어 있습니다. + +## 대상 독자 + +이 가이드는 이런 분들께 추천합니다. + +- 모던 C++의 원리와 사용법을 이해하시는 분 +- 써드파티 라이브러리를 사용해 C++ 프로젝트를 진행해보신 분 +- 그래픽스에 어느 정도 익숙하신 분 + - OpenGL 튜토리얼을 따라 해본 경험이 있다면 이상적입니다 + - SFML / SDL과 같은 프레임워크를 사용해본 경험도 도움이 됩니다 +- 필요한 모든 정보가 이 가이드 하나에 전부 담겨 있지 않아도 괜찮으신 분 + +이 책은 다음 내용을 다루지 않습니다. + +- GPU 기반 렌더링 기법 +- 그래픽스 시스템의 근본적인 구조부터 시작하는 실시간 렌더링 +- 타일 기반 GPU(예: 모바일 기기나 Android)를 위한 고려 사항 + +## 소스코드 + +프로젝트의 소스코드와 본 가이드는 여기에서 확인할 수 있습니다. `section/*` 브랜치는 각 섹션의 끝에서의 코드 상태를 반영하는 것을 목표로 합니다. 버그 수정이나 일부 변경사항은 가능한 한 반영하지만, `main`브랜치의 최신 상태와는 일부 차이가 있을 수 있습니다. 가이드 자체의 소스는 오직 `main` 브랜치에만 최신 상태로 유지되며, 변경사항은 다른 브랜치로 반영되지 않습니다. diff --git a/guide/translations/ko-KR/src/SUMMARY.md b/guide/translations/ko-KR/src/SUMMARY.md new file mode 100644 index 0000000..6fa2f86 --- /dev/null +++ b/guide/translations/ko-KR/src/SUMMARY.md @@ -0,0 +1,51 @@ +# Summary + +[소개](README.md) + +# 기초 + +- [시작하기](getting_started/README.md) + - [프로젝트 레이아웃](getting_started/project_layout.md) + - [검증 레이어](getting_started/validation_layers.md) + - [class App](getting_started/class_app.md) +- [초기화](initialization/README.md) + - [GLFW Window](initialization/glfw_window.md) + - [Vulkan 인스턴스](initialization/instance.md) + - [Vulkan Surface](initialization/surface.md) + - [Vulkan 물리 디바이스](initialization/gpu.md) + - [Vulkan 디바이스](initialization/device.md) + - [Scoped Waiter](initialization/scoped_waiter.md) + - [스왑체인](initialization/swapchain.md) + +# Hello Triangle + +- [렌더링](rendering/README.md) + - [스왑체인 루프](rendering/swapchain_loop.md) + - [렌더 싱크](rendering/render_sync.md) + - [스왑체인 업데이트](rendering/swapchain_update.md) + - [동적 렌더링](rendering/dynamic_rendering.md) +- [Dear ImGui](dear_imgui/README.md) + - [class DearImGui](dear_imgui/dear_imgui.md) + - [ImGui 통합](dear_imgui/imgui_integration.md) +- [셰이더 오브젝트](shader_objects/README.md) + - [에셋 위치](shader_objects/locating_assets.md) + - [셰이더 프로그램](shader_objects/shader_program.md) + - [GLSL 에서 SPIR-V](shader_objects/glsl_to_spir_v.md) + - [삼각형 그리기](shader_objects/drawing_triangle.md) + - [그래픽스 파이프라인](shader_objects/pipelines.md) + +# 셰이더 자원 + +- [메모리 할당](memory/README.md) + - [Vulkan Memory Allocator](memory/vma.md) + - [버퍼](memory/buffers.md) + - [정점 버퍼](memory/vertex_buffer.md) + - [Command Block](memory/command_block.md) + - [디바이스 버퍼](memory/device_buffers.md) + - [이미지](memory/images.md) +- [디스크립터 셋](descriptor_sets/README.md) + - [파이프라인 레이아웃](descriptor_sets/pipeline_layout.md) + - [Descriptor Buffer](descriptor_sets/descriptor_buffer.md) + - [텍스쳐](descriptor_sets/texture.md) + - [뷰 행렬](descriptor_sets/view_matrix.md) + - [인스턴스 렌더링](descriptor_sets/instanced_rendering.md) diff --git a/guide/translations/ko-KR/src/dear_imgui/README.md b/guide/translations/ko-KR/src/dear_imgui/README.md new file mode 100644 index 0000000..61b039f --- /dev/null +++ b/guide/translations/ko-KR/src/dear_imgui/README.md @@ -0,0 +1,3 @@ +# Dear ImGui + +Dear ImGui는 네이티브 CMake를 지원하지 않기 때문에, 소스를 실행 파일에 직접 추가하는 방법도 있지만, 컴파일 경고 등에서 우리 코드와 분리하기 위해 외부 라이브러리 타겟인 `imgui`로 추가할 예정입니다. 이를 위해 `imgui`는 GLFW 및 Vulkan-Headers에 연결되어야 하고, `VK_NO_PROTOTYPES`도 정의되어야 하므로 `ext` 타겟 구조에 약간의 변경이 필요합니다. 이후 `learn-vk-ext`는 `imgui` 및 기타 라이브러리들(현재는 `glm`만 있음)과 연결됩니다. 우리는 동적 렌더링을 지원하는 Dear ImGui v1.91.9 버전을 사용할 예정입니다. diff --git a/guide/translations/ko-KR/src/dear_imgui/dear_imgui.md b/guide/translations/ko-KR/src/dear_imgui/dear_imgui.md new file mode 100644 index 0000000..ef19b91 --- /dev/null +++ b/guide/translations/ko-KR/src/dear_imgui/dear_imgui.md @@ -0,0 +1,137 @@ +# class DearImGui + +Dear ImGui는 자체적인 초기화 과정과 렌더링 루프를 가지고 있으며, 이를 `class DearImGui`로 캡슐화하겠습니다. + +```cpp +struct DearImGuiCreateInfo { + GLFWwindow* window{}; + std::uint32_t api_version{}; + vk::Instance instance{}; + vk::PhysicalDevice physical_device{}; + std::uint32_t queue_family{}; + vk::Device device{}; + vk::Queue queue{}; + vk::Format color_format{}; // single color attachment. + vk::SampleCountFlagBits samples{}; +}; + +class DearImGui { + public: + using CreateInfo = DearImGuiCreateInfo; + + explicit DearImGui(CreateInfo const& create_info); + + void new_frame(); + void end_frame(); + void render(vk::CommandBuffer command_buffer) const; + + private: + enum class State : std::int8_t { Ended, Begun }; + + struct Deleter { + void operator()(vk::Device device) const; + }; + + State m_state{}; + + Scoped m_device{}; +}; +``` + +생성자에서는 ImGui 컨텍스트를 생성하고, Vulkan 함수를 불러와 Vulkan을 위한 GLFW 초기화를 진행합니다 + +```cpp +IMGUI_CHECKVERSION(); +ImGui::CreateContext(); + +static auto const load_vk_func = +[](char const* name, void* user_data) { + return VULKAN_HPP_DEFAULT_DISPATCHER.vkGetInstanceProcAddr( + *static_cast(user_data), name); +}; +auto instance = create_info.instance; +ImGui_ImplVulkan_LoadFunctions(create_info.api_version, load_vk_func, + &instance); + +if (!ImGui_ImplGlfw_InitForVulkan(create_info.window, true)) { + throw std::runtime_error{"Failed to initialize Dear ImGui"}; +} +``` + +그 후 Vulkan용 Dear ImGui를 초기화합니다. + +```cpp +auto init_info = ImGui_ImplVulkan_InitInfo{}; +init_info.ApiVersion = create_info.api_version; +init_info.Instance = create_info.instance; +init_info.PhysicalDevice = create_info.physical_device; +init_info.Device = create_info.device; +init_info.QueueFamily = create_info.queue_family; +init_info.Queue = create_info.queue; +init_info.MinImageCount = 2; +init_info.ImageCount = static_cast(resource_buffering_v); +init_info.MSAASamples = + static_cast(create_info.samples); +init_info.DescriptorPoolSize = 2; +auto pipline_rendering_ci = vk::PipelineRenderingCreateInfo{}; +pipline_rendering_ci.setColorAttachmentCount(1).setColorAttachmentFormats( + create_info.color_format); +init_info.PipelineRenderingCreateInfo = pipline_rendering_ci; +init_info.UseDynamicRendering = true; +if (!ImGui_ImplVulkan_Init(&init_info)) { + throw std::runtime_error{"Failed to initialize Dear ImGui"}; +} +ImGui_ImplVulkan_CreateFontsTexture(); +``` + +sRGB 포맷을 사용하고 있지만 Dear ImGui는 색상 공간에 대한 인식이 없기 때문에, 스타일 색상들을 선형 공간으로 변환해주어야 합니다. 이렇게 하면 감마 보정 과정을 통해 의도한 색상이 출력됩니다. + +```cpp +ImGui::StyleColorsDark(); +// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay) +for (auto& colour : ImGui::GetStyle().Colors) { + auto const linear = glm::convertSRGBToLinear( + glm::vec4{colour.x, colour.y, colour.z, colour.w}); + colour = ImVec4{linear.x, linear.y, linear.z, linear.w}; +} +ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = 0.99f; // more opaque +``` + +마지막으로 삭제자(Deleter)를 생성하고 구현합니다. + +```cpp +m_device = Scoped{create_info.device}; + +// ... +void DearImGui::Deleter::operator()(vk::Device const device) const { + device.waitIdle(); + ImGui_ImplVulkan_DestroyFontsTexture(); + ImGui_ImplVulkan_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); +} +``` + +이 외의 나머지 함수들은 비교적 단순합니다. + +```cpp +void DearImGui::new_frame() { + if (m_state == State::Begun) { end_frame(); } + ImGui_ImplGlfw_NewFrame(); + ImGui_ImplVulkan_NewFrame(); + ImGui::NewFrame(); + m_state = State::Begun; +} + +void DearImGui::end_frame() { + if (m_state == State::Ended) { return; } + ImGui::Render(); + m_state = State::Ended; +} + +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) +void DearImGui::render(vk::CommandBuffer const command_buffer) const { + auto* data = ImGui::GetDrawData(); + if (data == nullptr) { return; } + ImGui_ImplVulkan_RenderDrawData(data, command_buffer); +} +``` diff --git a/guide/translations/ko-KR/src/dear_imgui/imgui_demo.png b/guide/translations/ko-KR/src/dear_imgui/imgui_demo.png new file mode 100644 index 0000000..94eae35 Binary files /dev/null and b/guide/translations/ko-KR/src/dear_imgui/imgui_demo.png differ diff --git a/guide/translations/ko-KR/src/dear_imgui/imgui_integration.md b/guide/translations/ko-KR/src/dear_imgui/imgui_integration.md new file mode 100644 index 0000000..44029c1 --- /dev/null +++ b/guide/translations/ko-KR/src/dear_imgui/imgui_integration.md @@ -0,0 +1,59 @@ +# ImGui 통합 + +`Swapchain`이 이미지 포맷을 외부에 노출하도록 수정하겠습니다. + +```cpp +[[nodiscard]] auto get_format() const -> vk::Format { + return m_ci.imageFormat; +} +``` + +`class App`은 이제 `std::optional` 멤버를 담을 수 있으며, 이를 생성하는 함수를 추가하고 호출할 수 있습니다. + +```cpp +void App::create_imgui() { + auto const imgui_ci = DearImGui::CreateInfo{ + .window = m_window.get(), + .api_version = vk_version_v, + .instance = *m_instance, + .physical_device = m_gpu.device, + .queue_family = m_gpu.queue_family, + .device = *m_device, + .queue = m_queue, + .color_format = m_swapchain->get_format(), + .samples = vk::SampleCountFlagBits::e1, + }; + m_imgui.emplace(imgui_ci); +} +``` + +렌더 패스를 리셋한 이후에 새로운 ImGui 프레임을 시작하고, 데모 창을 띄워봅시다. + +```cpp +m_device->resetFences(*render_sync.drawn); +m_imgui->new_frame(); + +// ... +command_buffer.beginRendering(rendering_info); +ImGui::ShowDemoWindow(); +// draw stuff here. +command_buffer.endRendering(); +``` + +ImGui는 이 시점에서는 아무것도 그리지 않습니다(실제 그리기 명령은 커맨드 버퍼가 필요합니다). 이 부분은 상위 로직을 구성하기 위한 커스터마이징 지점입니다. + +우리는 Dear ImGui를 위한 별도의 렌더 패스를 사용합니다. 이는 코드의 분리를 위한 목적도 있고, 메인 렌더 패스를 나중에 깊이 버퍼를 추가하는 것과 같은 상황에 변경할 수 있도록 하기 위함입니다 `DearImGui`는 하나의 색상 어태치먼트만 사용하는 전용 렌더 패스를 설정한다고 간주합니다. + +```cpp +m_imgui->end_frame(); +// we don't want to clear the image again, instead load it intact after the +// previous pass. +color_attachment.setLoadOp(vk::AttachmentLoadOp::eLoad); +rendering_info.setColorAttachments(color_attachment) + .setPDepthAttachment(nullptr); +command_buffer.beginRendering(rendering_info); +m_imgui->render(command_buffer); +command_buffer.endRendering(); +``` + +![ImGui Demo](./imgui_demo.png) diff --git a/guide/translations/ko-KR/src/descriptor_sets/README.md b/guide/translations/ko-KR/src/descriptor_sets/README.md new file mode 100644 index 0000000..d38a34b --- /dev/null +++ b/guide/translations/ko-KR/src/descriptor_sets/README.md @@ -0,0 +1,5 @@ +# 디스크립터 셋 + +[Vulkan 디스크립터](https://docs.vulkan.org/guide/latest/mapping_data_to_shaders.html#descriptors)는 기본적으로 셰이더가 접근할 수 있는 자원(예 : 유니폼 버퍼, 스토리지 버퍼, 샘플러가 결합된 텍스쳐 등)에 대한 타입이 지정된 포인터입니다. 디스크립터 셋은 이러한 디스크립터들을 다양한 **바인딩**에 모아 하나의 단위로 결합한 집합이며, 셰이더는 특정 셋 번호와 바인딩 번호를 기준으로 입력을 선언합니다. 셰이더에서 사용하는 모든 디스크립터 셋은 드로우 콜 전에 반드시 업데이트, 바인딩되어야 합니다. 디스크립터 셋 레이아웃은 특정 셋 번호에 해당하는 디스크립터 셋의 구성 방식을 나타내며, 일반적으로 셰이더에서 사용하는 모든 디스크립터 셋을 나타냅니다. 디스크립터 셋은 디스크립터 풀과 원하는 디스크립터 셋 레이아웃을 이용해 할당됩니다. + +디스크립터 셋 레이아웃을 구성하고 디스크립터 셋을 관리하는 것은 다양한 접근 방법이 가능하며, 각각 장단점이 있어 꽤 복잡한 주제입니다. [이 페이지](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)에서 다루고 있습니다. diff --git a/guide/translations/ko-KR/src/descriptor_sets/descriptor_buffer.md b/guide/translations/ko-KR/src/descriptor_sets/descriptor_buffer.md new file mode 100644 index 0000000..e4a48b7 --- /dev/null +++ b/guide/translations/ko-KR/src/descriptor_sets/descriptor_buffer.md @@ -0,0 +1,174 @@ +# 디스크립터 버퍼 + +유니폼과 스토리지 버퍼는 생성 이후 내용이 변경되지 않는 GPU 상수가 아닌 이상, N-버퍼링 되어야 합니다. 각 가상 프레임마다의 하나의 `vma::Buffer`를 `DescriptorBuffer`로 캡슐화하겠습니다. + +```cpp +class DescriptorBuffer { + public: + explicit DescriptorBuffer(VmaAllocator allocator, + std::uint32_t queue_family, + vk::BufferUsageFlags usage); + + void write_at(std::size_t frame_index, std::span bytes); + + [[nodiscard]] auto descriptor_info_at(std::size_t frame_index) const + -> vk::DescriptorBufferInfo; + + private: + struct Buffer { + vma::Buffer buffer{}; + vk::DeviceSize size{}; + }; + + void write_to(Buffer& out, std::span bytes) const; + + VmaAllocator m_allocator{}; + std::uint32_t m_queue_family{}; + vk::BufferUsageFlags m_usage{}; + Buffered m_buffers{}; +}; +``` + +구현은 꽤 단순합니다. 기존의 버퍼로 충분하다면 재사용하고, 아니라면 데이터를 복사하기 전에 새로 생성합니다. 이렇게 하면 디스크립터 셋에 바인딩할 버퍼가 항상 유효한 상태임을 보장합니다. + +```cpp +DescriptorBuffer::DescriptorBuffer(VmaAllocator allocator, + std::uint32_t const queue_family, + vk::BufferUsageFlags const usage) + : m_allocator(allocator), m_queue_family(queue_family), m_usage(usage) { + // ensure buffers are created and can be bound after returning. + for (auto& buffer : m_buffers) { write_to(buffer, {}); } +} + +void DescriptorBuffer::write_at(std::size_t const frame_index, + std::span bytes) { + write_to(m_buffers.at(frame_index), bytes); +} + +auto DescriptorBuffer::descriptor_info_at(std::size_t const frame_index) const + -> vk::DescriptorBufferInfo { + auto const& buffer = m_buffers.at(frame_index); + auto ret = vk::DescriptorBufferInfo{}; + ret.setBuffer(buffer.buffer.get().buffer).setRange(buffer.size); + return ret; +} + +void DescriptorBuffer::write_to(Buffer& out, + std::span bytes) const { + static constexpr auto blank_byte_v = std::array{std::byte{}}; + // fallback to an empty byte if bytes is empty. + if (bytes.empty()) { bytes = blank_byte_v; } + out.size = bytes.size(); + if (out.buffer.get().size < bytes.size()) { + // size is too small (or buffer doesn't exist yet), recreate buffer. + auto const buffer_ci = vma::BufferCreateInfo{ + .allocator = m_allocator, + .usage = m_usage, + .queue_family = m_queue_family, + }; + out.buffer = vma::create_buffer(buffer_ci, vma::BufferMemoryType::Host, + out.size); + } + std::memcpy(out.buffer.get().mapped, bytes.data(), bytes.size()); +} +``` + +`App`에 `DescriptorBuffer`를 저장하고 `create_vertex_buffer()`함수의 이름을 `create_shader_resources()`로 변경합니다. + +```cpp +std::optional m_view_ubo{}; + +// ... +m_vbo = vma::create_device_buffer(buffer_ci, create_command_block(), + total_bytes_v); + +m_view_ubo.emplace(m_allocator.get(), m_gpu.queue_family, + vk::BufferUsageFlagBits::eUniformBuffer); +``` + +뷰/프로젝션 행렬을 업데이트하고 프레임별 디스크립터 셋을 바인딩하는 함수를 추가합니다. + +```cpp +void App::update_view() { + auto const half_size = 0.5f * glm::vec2{m_framebuffer_size}; + auto const mat_projection = + glm::ortho(-half_size.x, half_size.x, -half_size.y, half_size.y); + auto const bytes = + std::bit_cast>( + mat_projection); + m_view_ubo->write_at(m_frame_index, bytes); +} + +// ... +void App::bind_descriptor_sets(vk::CommandBuffer const command_buffer) const { + auto writes = std::array{}; + auto const& descriptor_sets = m_descriptor_sets.at(m_frame_index); + auto const set0 = descriptor_sets[0]; + auto write = vk::WriteDescriptorSet{}; + auto const view_ubo_info = m_view_ubo->descriptor_info_at(m_frame_index); + write.setBufferInfo(view_ubo_info) + .setDescriptorType(vk::DescriptorType::eUniformBuffer) + .setDescriptorCount(1) + .setDstSet(set0) + .setDstBinding(0); + writes[0] = write; + m_device->updateDescriptorSets(writes, {}); + + command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, + *m_pipeline_layout, 0, descriptor_sets, + {}); +} +``` + +셰이더에 디스크립터 셋 레이아웃을 추가하고, `draw()` 호출 전에 `update_view()`를 호출하며, `draw()` 내부에서 `bind_descriptor_sets()`를 호출합니다. + +```cpp +auto const shader_ci = ShaderProgram::CreateInfo{ + .device = *m_device, + .vertex_spirv = vertex_spirv, + .fragment_spirv = fragment_spirv, + .vertex_input = vertex_input_v, + .set_layouts = m_set_layout_views, +}; + +// ... +inspect(); +update_view(); +draw(command_buffer); + +// ... +m_shader->bind(command_buffer, m_framebuffer_size); +bind_descriptor_sets(command_buffer); +// ... +``` + +정점 셰이더를 이에 맞춰 변경해줍니다. + +```glsl +layout (set = 0, binding = 0) uniform View { + mat4 mat_vp; +}; + +// ... +void main() { + const vec4 world_pos = vec4(a_pos, 0.0, 1.0); + + out_color = a_color; + gl_Position = mat_vp * world_pos; +} +``` + +투영 공간이 이제 [ -1, 1] 범위가 아닌 프레임버퍼 크기를 기준으로 하기 때문에 정점 위치를 1픽셀보다 크게 위치하도록 업데이트합니다. + +```cpp +static constexpr auto vertices_v = std::array{ + Vertex{.position = {-200.0f, -200.0f}, .color = {1.0f, 0.0f, 0.0f}}, + Vertex{.position = {200.0f, -200.0f}, .color = {0.0f, 1.0f, 0.0f}}, + Vertex{.position = {200.0f, 200.0f}, .color = {0.0f, 0.0f, 1.0f}}, + Vertex{.position = {-200.0f, 200.0f}, .color = {1.0f, 1.0f, 0.0f}}, +}; +``` + +![View UBO](./view_ubo.png) + +이러한 디스크립터 버퍼가 동적으로 생성되고 파괴될 때, 관련 디스크립터 셋을 사용하는 렌더링이 모두 완료된 후에 파괴되도록 `ScopedWaiter`를 사용해 보장합니다. 또는, 프레임마다 임시 버퍼 풀을 유지하여(작거나 동적인 정점 버퍼와 유사하게) 버퍼를 개별이 아닌 일괄적으로 파괴하는 방식도 사용할 수 있습니다. diff --git a/guide/translations/ko-KR/src/descriptor_sets/instanced_rendering.md b/guide/translations/ko-KR/src/descriptor_sets/instanced_rendering.md new file mode 100644 index 0000000..68406af --- /dev/null +++ b/guide/translations/ko-KR/src/descriptor_sets/instanced_rendering.md @@ -0,0 +1,139 @@ +# 인스턴스 렌더링 + +하나의 객체를 여러 번 그려야 할 때 사용할 수 있는 방법 중 하나는 인스턴스 렌더링입니다. 기본 아이디어는 인스턴스별 데이터를 유니폼 버퍼 또는 스토리지 버퍼에 담고, 이를 정점 셰이더에서 참조하는 것입니다. 우리는 인스턴스마다 하나의 모델 행렬을 표현하겠습니다. 필요하다면 색상과 같은 정보를 포함해 프래그먼트 셰이더에서 기존 출력 색상에 곱하는 방식으로 사용할 수도 있습니다. 이러한 데이터는 스토리지 버퍼(SSBO)에 바인딩되며, 버퍼의 크기는 호출 시점에 결정됩니다. + +SSBO와 인스턴스 행렬을 저장할 버퍼를 추가합니다. + +```cpp +std::vector m_instance_data{}; // model matrices. +std::optional m_instance_ssbo{}; +``` + +렌더링할 인스턴스에 사용할 `Transform`을 추가하고 이를 기반으로 행렬을 업데이트하는 함수를 추가합니다. + +```cpp +void update_instances(); + +// ... +std::array m_instances{}; // generates model matrices. + +// ... +void App::update_instances() { + m_instance_data.clear(); + m_instance_data.reserve(m_instances.size()); + for (auto const& transform : m_instances) { + m_instance_data.push_back(transform.model_matrix()); + } + // can't use bit_cast anymore, reinterpret data as a byte array instead. + auto const span = std::span{m_instance_data}; + void* data = span.data(); + auto const bytes = + std::span{static_cast(data), span.size_bytes()}; + m_instance_ssbo->write_at(m_frame_index, bytes); +} +``` + +디스크립터 풀을 업데이트하여 스토리지 버퍼를 지원하도록 합니다. + +```cpp +// ... +vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2}, +vk::DescriptorPoolSize{vk::DescriptorType::eStorageBuffer, 2}, +``` + +디스크립터 셋을 2번과 해당 바인딩을 추가합니다. 이처럼 각 디스크립터 셋을 명확하게 역할별로 분리하는 것이 좋습니다. + +* 디스크립터 셋 0 - : 뷰 / 카메라 +* 디스크립터 셋 1 - 텍스쳐 / 머테리얼 +* 디스크립터 셋 2 : 인스턴싱 + +```cpp +static constexpr auto set_2_bindings_v = std::array{ + layout_binding(1, vk::DescriptorType::eStorageBuffer), +}; +auto set_layout_cis = std::array{}; +// ... +set_layout_cis[2].setBindings(set_2_bindings_v); +``` + +뷰 UBO를 생성한 이후 인스턴스용 SSBO를 생성합니다. + +```cpp +m_instance_ssbo.emplace(m_allocator.get(), m_gpu.queue_family, + vk::BufferUsageFlagBits::eStorageBuffer); +``` + +`update_view()`를 호출한 다음 `update_instances()`를 호출합니다. + +```cpp +// ... +update_view(); +update_instances(); +``` + +트랜스폼 확인 로직을 람다로 분리해 각 인스턴스의 트랜스폼을 검사합니다. + +```cpp +static auto const inspect_transform = [](Transform& out) { + ImGui::DragFloat2("position", &out.position.x); + ImGui::DragFloat("rotation", &out.rotation); + ImGui::DragFloat2("scale", &out.scale.x, 0.1f); +}; + +ImGui::Separator(); +if (ImGui::TreeNode("View")) { + inspect_transform(m_view_transform); + ImGui::TreePop(); +} + +ImGui::Separator(); +if (ImGui::TreeNode("Instances")) { + for (std::size_t i = 0; i < m_instances.size(); ++i) { + auto const label = std::to_string(i); + if (ImGui::TreeNode(label.c_str())) { + inspect_transform(m_instances.at(i)); + ImGui::TreePop(); + } + } + ImGui::TreePop(); +} +``` + +SSBO를 위한 descriptorWrite도 추가합니다. + +```cpp +auto writes = std::array{}; +// ... +auto const set2 = descriptor_sets[2]; +auto const instance_ssbo_info = + m_instance_ssbo->descriptor_info_at(m_frame_index); +write.setBufferInfo(instance_ssbo_info) + .setDescriptorType(vk::DescriptorType::eStorageBuffer) + .setDescriptorCount(1) + .setDstSet(set2) + .setDstBinding(0); +writes[2] = write; +``` + +마지막으로, 드로우 콜의 인스턴스 수를 변경합니다. + +```cpp +auto const instances = static_cast(m_instances.size()); +// m_vbo has 6 indices. +command_buffer.drawIndexed(6, instances, 0, 0, 0); +``` + +정점 셰이더를 수정하여 인스턴스별 모델 행렬을 적용합니다. + +```glsl +// ... +layout (set = 1, binding = 1) readonly buffer Instances { + mat4 mat_ms[]; +}; + +// ... +const mat4 mat_m = mat_ms[gl_InstanceIndex]; +const vec4 world_pos = mat_m * vec4(a_pos, 0.0, 1.0); +``` + +![Instanced Rendering](./instanced_rendering.png) diff --git a/guide/translations/ko-KR/src/descriptor_sets/instanced_rendering.png b/guide/translations/ko-KR/src/descriptor_sets/instanced_rendering.png new file mode 100644 index 0000000..766a49c Binary files /dev/null and b/guide/translations/ko-KR/src/descriptor_sets/instanced_rendering.png differ diff --git a/guide/translations/ko-KR/src/descriptor_sets/pipeline_layout.md b/guide/translations/ko-KR/src/descriptor_sets/pipeline_layout.md new file mode 100644 index 0000000..683e66d --- /dev/null +++ b/guide/translations/ko-KR/src/descriptor_sets/pipeline_layout.md @@ -0,0 +1,81 @@ +# 파이프라인 레이아웃 + +[Vulkan 파이프라인 레이아웃](https://registry.khronos.org/vulkan/specs/latest/man/html/VkPipelineLayout.html)은 셰이더 프로그램과 연결된 디스크립터 셋과 푸시 상수를 나타냅니다. 셰이더 오브젝트를 사용할 경우에도 디스크립터 셋을 활용하기 위해 파이프라인 레이아웃이 필요합니다. + +뷰/프로젝션 행렬을 담기 위한 유니폼 버퍼를 포함하는 단일 디스크립터 셋 레이아웃부터 시작합니다. 디스크립터 풀을 `App`에 추가하고 셰이더보다 먼저 생성합니다. + +```cpp +vk::UniqueDescriptorPool m_descriptor_pool{}; + +// ... +void App::create_descriptor_pool() { + static constexpr auto pool_sizes_v = std::array{ + // 2 uniform buffers, can be more if desired. + vk::DescriptorPoolSize{vk::DescriptorType::eUniformBuffer, 2}, + }; + auto pool_ci = vk::DescriptorPoolCreateInfo{}; + // allow 16 sets to be allocated from this pool. + pool_ci.setPoolSizes(pool_sizes_v).setMaxSets(16); + m_descriptor_pool = m_device->createDescriptorPoolUnique(pool_ci); +} +``` + +새로운 멤버를 `App`에 추가해 디스크립터 셋 레이아웃과 파이프라인 레이아웃을 담도록 합니다. `m_set_layout_views`는 디스크립터 셋 레이아웃의 핸들을 연속된 vector로 복사한 것입니다. + +```cpp +std::vector m_set_layouts{}; +std::vector m_set_layout_views{}; +vk::UniquePipelineLayout m_pipeline_layout{}; + +// ... +constexpr auto layout_binding(std::uint32_t binding, + vk::DescriptorType const type) { + return vk::DescriptorSetLayoutBinding{ + binding, type, 1, vk::ShaderStageFlagBits::eAllGraphics}; +} + +// ... +void App::create_pipeline_layout() { + static constexpr auto set_0_bindings_v = std::array{ + layout_binding(0, vk::DescriptorType::eUniformBuffer), + }; + auto set_layout_cis = std::array{}; + set_layout_cis[0].setBindings(set_0_bindings_v); + + for (auto const& set_layout_ci : set_layout_cis) { + m_set_layouts.push_back( + m_device->createDescriptorSetLayoutUnique(set_layout_ci)); + m_set_layout_views.push_back(*m_set_layouts.back()); + } + + auto pipeline_layout_ci = vk::PipelineLayoutCreateInfo{}; + pipeline_layout_ci.setSetLayouts(m_set_layout_views); + m_pipeline_layout = + m_device->createPipelineLayoutUnique(pipeline_layout_ci); +} +``` + +레이아웃 전체에 해당하는 디스크립터 셋을 할당하는 함수를 추가합니다. + +```cpp +auto App::allocate_sets() const -> std::vector { + auto allocate_info = vk::DescriptorSetAllocateInfo{}; + allocate_info.setDescriptorPool(*m_descriptor_pool) + .setSetLayouts(m_set_layout_views); + return m_device->allocateDescriptorSets(allocate_info); +} +``` + +그릴 객체에 쓰일 디스크립터 셋을 저장합니다. + +```cpp +Buffered> m_descriptor_sets{}; + +// ... + +void App::create_descriptor_sets() { + for (auto& descriptor_sets : m_descriptor_sets) { + descriptor_sets = allocate_sets(); + } +} +``` diff --git a/guide/translations/ko-KR/src/descriptor_sets/rgby_texture.png b/guide/translations/ko-KR/src/descriptor_sets/rgby_texture.png new file mode 100644 index 0000000..25b0c6d Binary files /dev/null and b/guide/translations/ko-KR/src/descriptor_sets/rgby_texture.png differ diff --git a/guide/translations/ko-KR/src/descriptor_sets/texture.md b/guide/translations/ko-KR/src/descriptor_sets/texture.md new file mode 100644 index 0000000..d94e889 --- /dev/null +++ b/guide/translations/ko-KR/src/descriptor_sets/texture.md @@ -0,0 +1,248 @@ +# 텍스쳐 + +대부분의 복잡한 작업은 `vma`에서 처리하기 때문에, `Texture`는 다음 세 가지로 구성됩니다. + +1. 샘플링될 이미지 +2. 해당 이미지의 이미지 뷰 +3. 고유한 샘플러 + +`texture.hpp`에 기본 샘플러를 생성하겠습니다. + +```cpp +[[nodiscard]] constexpr auto +create_sampler_ci(vk::SamplerAddressMode const wrap, vk::Filter const filter) { + auto ret = vk::SamplerCreateInfo{}; + ret.setAddressModeU(wrap) + .setAddressModeV(wrap) + .setAddressModeW(wrap) + .setMinFilter(filter) + .setMagFilter(filter) + .setMaxLod(VK_LOD_CLAMP_NONE) + .setBorderColor(vk::BorderColor::eFloatTransparentBlack) + .setMipmapMode(vk::SamplerMipmapMode::eNearest); + return ret; +} + +constexpr auto sampler_ci_v = create_sampler_ci( + vk::SamplerAddressMode::eClampToEdge, vk::Filter::eLinear); +``` + +CreateInfo와 텍스쳐 타입을 정의합니다. + + +```cpp +struct TextureCreateInfo { + vk::Device device; + VmaAllocator allocator; + std::uint32_t queue_family; + CommandBlock command_block; + Bitmap bitmap; + + vk::SamplerCreateInfo sampler{sampler_ci_v}; +}; + +class Texture { + public: + using CreateInfo = TextureCreateInfo; + + explicit Texture(CreateInfo create_info); + + [[nodiscard]] auto descriptor_info() const -> vk::DescriptorImageInfo; + + private: + vma::Image m_image{}; + vk::UniqueImageView m_view{}; + vk::UniqueSampler m_sampler{}; +}; +``` + +에러가 발생 시 사용할(fallback) 비트맵 상수도 추가하겠습니다. + +```cpp +// 4-channels. +constexpr auto white_pixel_v = std::array{std::byte{0xff}, std::byte{0xff}, + std::byte{0xff}, std::byte{0xff}}; +// fallback bitmap. +constexpr auto white_bitmap_v = Bitmap{ + .bytes = white_pixel_v, + .size = {1, 1}, +}; + +// ... +Texture::Texture(CreateInfo create_info) { + if (create_info.bitmap.bytes.empty() || create_info.bitmap.size.x <= 0 || + create_info.bitmap.size.y <= 0) { + create_info.bitmap = white_bitmap_v; + } + + auto const image_ci = vma::ImageCreateInfo{ + .allocator = create_info.allocator, + .queue_family = create_info.queue_family, + }; + m_image = vma::create_sampled_image( + image_ci, std::move(create_info.command_block), create_info.bitmap); + + auto image_view_ci = vk::ImageViewCreateInfo{}; + auto subresource_range = vk::ImageSubresourceRange{}; + subresource_range.setAspectMask(vk::ImageAspectFlagBits::eColor) + .setLayerCount(1) + .setLevelCount(m_image.get().levels); + + image_view_ci.setImage(m_image.get().image) + .setViewType(vk::ImageViewType::e2D) + .setFormat(m_image.get().format) + .setSubresourceRange(subresource_range); + m_view = create_info.device.createImageViewUnique(image_view_ci); + + m_sampler = create_info.device.createSamplerUnique(create_info.sampler); +} + +auto Texture::descriptor_info() const -> vk::DescriptorImageInfo { + auto ret = vk::DescriptorImageInfo{}; + ret.setImageView(*m_view) + .setImageLayout(vk::ImageLayout::eShaderReadOnlyOptimal) + .setSampler(*m_sampler); + return ret; +} +``` + +텍스쳐를 샘플링하려면 `Vertex`에 UV 좌표를 추가해야 합니다. + +```cpp +struct Vertex { + glm::vec2 position{}; + glm::vec3 color{1.0f}; + glm::vec2 uv{}; +}; + +// two vertex attributes: position at 0, color at 1. +constexpr auto vertex_attributes_v = std::array{ + // the format matches the type and layout of data: vec2 => 2x 32-bit floats. + vk::VertexInputAttributeDescription2EXT{0, 0, vk::Format::eR32G32Sfloat, + offsetof(Vertex, position)}, + // vec3 => 3x 32-bit floats + vk::VertexInputAttributeDescription2EXT{1, 0, vk::Format::eR32G32B32Sfloat, + offsetof(Vertex, color)}, + // vec2 => 2x 32-bit floats + vk::VertexInputAttributeDescription2EXT{2, 0, vk::Format::eR32G32Sfloat, + offsetof(Vertex, uv)}, +}; +``` + +`App`에 텍스쳐를 담고 다른 셰이더 자원도 함께 생성하겠습니다. + +```cpp +std::optional m_texture{}; + +// ... +using Pixel = std::array; +static constexpr auto rgby_pixels_v = std::array{ + Pixel{std::byte{0xff}, {}, {}, std::byte{0xff}}, + Pixel{std::byte{}, std::byte{0xff}, {}, std::byte{0xff}}, + Pixel{std::byte{}, {}, std::byte{0xff}, std::byte{0xff}}, + Pixel{std::byte{0xff}, std::byte{0xff}, {}, std::byte{0xff}}, +}; +static constexpr auto rgby_bytes_v = + std::bit_cast>( + rgby_pixels_v); +static constexpr auto rgby_bitmap_v = Bitmap{ + .bytes = rgby_bytes_v, + .size = {2, 2}, +}; +auto texture_ci = Texture::CreateInfo{ + .device = *m_device, + .allocator = m_allocator.get(), + .queue_family = m_gpu.queue_family, + .command_block = create_command_block(), + .bitmap = rgby_bitmap_v, +}; +// use Nearest filtering instead of Linear (interpolation). +texture_ci.sampler.setMagFilter(vk::Filter::eNearest); +m_texture.emplace(std::move(texture_ci)); +``` + +DescriptorPoolSize를 업데이트해 CombinedImageSampler도 포함하도록 수정합니다. + +```cpp +/// ... +vk::DescriptorPoolSize{vk::DescriptorType::eUniformBuffer, 2}, +vk::DescriptorPoolSize{vk::DescriptorType::eCombinedImageSampler, 2}, +``` + +새로운 디스크립터 셋(1번)의 바인딩 0번에 CombinedImageSampler를 설정합니다. 바인딩 호출 최적화를 하지 않을 경우, set 0의 바인딩 1번에 추가해도 괜찮습니다. + +```cpp +static constexpr auto set_1_bindings_v = std::array{ + layout_binding(0, vk::DescriptorType::eCombinedImageSampler), +}; +auto set_layout_cis = std::array{}; +set_layout_cis[0].setBindings(set_0_bindings_v); +set_layout_cis[1].setBindings(set_1_bindings_v); +``` + +정점 색상 값을 지우고 사각형에 UV를 설정합니다. Vulkan에서의 UV 공간은 GLFW 윈도우 공간과 동일합니다. 왼쪽 위가 원점이고, 오른쪽이 +X, 아래쪽이 +Y입니다. + +```cpp +static constexpr auto vertices_v = std::array{ + Vertex{.position = {-200.0f, -200.0f}, .uv = {0.0f, 1.0f}}, + Vertex{.position = {200.0f, -200.0f}, .uv = {1.0f, 1.0f}}, + Vertex{.position = {200.0f, 200.0f}, .uv = {1.0f, 0.0f}}, + Vertex{.position = {-200.0f, 200.0f}, .uv = {0.0f, 0.0f}}, +}; +``` + +마지막으로, DescriptorWrite를 업데이트 합니다. + +```cpp +auto writes = std::array{}; +// ... +auto const set1 = descriptor_sets[1]; +auto const image_info = m_texture->descriptor_info(); +write.setImageInfo(image_info) + .setDescriptorType(vk::DescriptorType::eCombinedImageSampler) + .setDescriptorCount(1) + .setDstSet(set1) + .setDstBinding(0); +writes[1] = write; +``` + +텍스쳐는 N-버퍼링되지 않기 때문에(이는 GPU 상수입니다), 디스크립터 셋도 텍스쳐 생성 이후 한번만 업데이트하면 됩니다. + +UV 정점 속성을 정점 셰이더에 추가하고 이를 프래그먼트 셰이더로 전달합니다. + +```glsl +layout (location = 2) in vec2 a_uv; + +// ... +layout (location = 1) out vec2 out_uv; + +// ... +out_color = a_color; +out_uv = a_uv; +``` + +셋 1번과 그에 맞는 UV 좌표도 프래그먼트 셰이더에 추가하고, 샘플링한 텍스쳐 색상과 정점 색상을 섞어봅시다. + +```glsl +layout (set = 1, binding = 0) uniform sampler2D tex; + +// ... +layout (location = 1) in vec2 in_uv; + +// ... +out_color = vec4(in_color, 1.0) * texture(tex, in_uv); +``` + +![RGBY Texture](./rgby_texture.png) + +밉맵을 생성하기 위해서는 [Vulkan 공식 예제](https://docs.vulkan.org/samples/latest/samples/api/hpp_texture_mipmap_generation/README.html#_generating_the_mip_chain)를 참고하세요. 고수준 절차는 다음과 같습니다. + +1. 이미지 크기에 기반하여 밉 레벨을 계산합니다. +2. 원하는 밉 레벨 수를 갖는 이미지를 생성합니다. +3. 첫 번째 밉 레벨에 원본 데이터를 복사합니다. +4. 첫 번째 밉 레벨의 레이아웃을 TransferSrc로 변경합니다. +5. 모든 밉 레벨을 순회하며 다음을 수행합니다. + 1. 현재 밉 레벨의 레이아웃을 TransferDst로 변경합니다. + 2. 이전 밉 레벨에서 현재 밉 레벨로 ImageBlit 작업을 수행합니다. + 3. 현재 밉 레벨의 레이아웃을 TransferSrc로 변경합니다. +6. 모든 레벨의 레이아웃을 ShaderRead로 변경합니다. diff --git a/guide/translations/ko-KR/src/descriptor_sets/view_matrix.md b/guide/translations/ko-KR/src/descriptor_sets/view_matrix.md new file mode 100644 index 0000000..8e18515 --- /dev/null +++ b/guide/translations/ko-KR/src/descriptor_sets/view_matrix.md @@ -0,0 +1,79 @@ +# 뷰 행렬 + +뷰 행렬을 통합하는 작업은 꽤 간단합니다. 먼저, 오브젝트와 카메라/뷰의 변환 정보를 하나의 구조체로 캡슐화합니다. + +```cpp +struct Transform { + glm::vec2 position{}; + float rotation{}; + glm::vec2 scale{1.0f}; + + [[nodiscard]] auto model_matrix() const -> glm::mat4; + [[nodiscard]] auto view_matrix() const -> glm::mat4; +}; +``` + +공통된 로직을 함수로 사용하도록 두 가지 멤버 함수를 추가하겠습니다. + +```cpp +namespace { +struct Matrices { + glm::mat4 translation; + glm::mat4 orientation; + glm::mat4 scale; +}; + +[[nodiscard]] auto to_matrices(glm::vec2 const position, float rotation, + glm::vec2 const scale) -> Matrices { + static constexpr auto mat_v = glm::identity(); + static constexpr auto axis_v = glm::vec3{0.0f, 0.0f, 1.0f}; + return Matrices{ + .translation = glm::translate(mat_v, glm::vec3{position, 0.0f}), + .orientation = glm::rotate(mat_v, glm::radians(rotation), axis_v), + .scale = glm::scale(mat_v, glm::vec3{scale, 1.0f}), + }; +} +} // namespace + +auto Transform::model_matrix() const -> glm::mat4 { + auto const [t, r, s] = to_matrices(position, rotation, scale); + // right to left: scale first, then rotate, then translate. + return t * r * s; +} + +auto Transform::view_matrix() const -> glm::mat4 { + // view matrix is the inverse of the model matrix. + // instead, perform translation and rotation in reverse order and with + // negative values. or, use glm::lookAt(). + // scale is kept unchanged as the first transformation for + // "intuitive" scaling on cameras. + auto const [t, r, s] = to_matrices(-position, -rotation, scale); + return r * t * s; +} +``` + +`App`에 `Transform` 멤버를 추가하여 뷰/카메라를 나타내고, 해당 멤버를 확인하여 기존의 프로젝션 행렬과 결합합니다. + +```cpp +Transform m_view_transform{}; // generates view matrix. + +// ... +ImGui::Separator(); +if (ImGui::TreeNode("View")) { + ImGui::DragFloat2("position", &m_view_transform.position.x); + ImGui::DragFloat("rotation", &m_view_transform.rotation); + ImGui::DragFloat2("scale", &m_view_transform.scale.x); + ImGui::TreePop(); +} + +// ... +auto const mat_view = m_view_transform.view_matrix(); +auto const mat_vp = mat_projection * mat_view; +auto const bytes = + std::bit_cast>(mat_vp); +m_view_ubo->write_at(m_frame_index, bytes); +``` + +자연스럽게 뷰를 왼쪽으로 이동하면 현재는 사각형 하나뿐이지만 이것이 오른쪽으로 이동한 것으로 보일 것입니다. + +![View Matrix](./view_matrix.png) diff --git a/guide/translations/ko-KR/src/descriptor_sets/view_matrix.png b/guide/translations/ko-KR/src/descriptor_sets/view_matrix.png new file mode 100644 index 0000000..5070b91 Binary files /dev/null and b/guide/translations/ko-KR/src/descriptor_sets/view_matrix.png differ diff --git a/guide/translations/ko-KR/src/descriptor_sets/view_ubo.png b/guide/translations/ko-KR/src/descriptor_sets/view_ubo.png new file mode 100644 index 0000000..0a56b53 Binary files /dev/null and b/guide/translations/ko-KR/src/descriptor_sets/view_ubo.png differ diff --git a/guide/translations/ko-KR/src/getting_started/README.md b/guide/translations/ko-KR/src/getting_started/README.md new file mode 100644 index 0000000..8f8ab63 --- /dev/null +++ b/guide/translations/ko-KR/src/getting_started/README.md @@ -0,0 +1,34 @@ +# 시작하기 + +Vulkan은 플랫폼에 독립적인 API입니다. 이로 인해 다양한 구현을 포괄해야 하므로, 다소 장황해질 수 밖에 없습니다. 이 가이드에서는 Windows와 Linux(x64 혹은 aarch64)에 집중하고, 외장 GPU를 중심으로 설명할 것입니다. 이를 통해 Vulkan의 복잡함을 어느 정도 완화할 수 있습니다. Vulkan 1.3은 우리가 다룰 데스크탑 플랫폼과 대부분의 최신 그래픽 카드에서 널리 지원됩니다. + +> 이것이 내장 GPU를 지원하지 않는다는 뜻은 아닙니다. 다만 특별히 그에 맞게 설계하거나 최적화하지는 않습니다. + +## 요구사항 + +1. Vulkan 1.3 버전 이상을 지원하는 GPU와 로더 +1. [Vulkan 1.3 이상의 SDK](https://vulkan.lunarg.com/sdk/home) + 1. 이는 검증 레이어에 필요하며, Vulkan 애플리케이션 개발 시 중요한 구성요소입니다. 단, 프로젝트 자체는 SDK에 직접 의존하지 않습니다. + 1. 항상 최신 SDK 사용이 권장됩니다. (이 문서를 작성하는 시점 기준으로는 1.4) +1. Vulkan을 기본적으로 지원하는 데스크탑 운영체제 + 1. Windows 혹은 최신 패키지를 제공하는 Linux 배포판 사용이 권장됩니다. + 2. MacOS는 Vulkan을 기본적으로 지원하지 않습니다. MoltenVk를 통해 사용할 수는 있으나, 작성 시점 기준 MoltenVk는 Vulkan 1.3을 완전히 지원하지 않기 때문에, 이 환경에서는 제약이 있을 수 있습니다. +2. C++23을 지원하는 컴파일러 및 표준 라이브러리 + 1. GCC14+, Clang18+, 최신 MSVC가 권장되며, MinGW/MSYS는 권장되지 않습니다. + 2. C++20을 사용하고 C++23의 특정 기능을 대체하는 것도 가능합니다.(예: `std::print()` 대신 `fmt::print()` 사용, 람다식에 `()` 추가 등) +3. CMake 3.24 이상 + +## 개요 + +C++ 모듈에 대한 지원은 점차 확대되고 있지만, 우리가 목표로 하는 모든 플랫폼과 IDE에서는 아직 완전히 지원되지 않습니다. 따라서 당분간은 헤더 파일을 사용할 수 밖에 없습니다. 이는 가까운 미래에 도구들이 개선되면, 가이드를 리팩토링하면서 변경될 수 있습니다. + +이 프로젝트는 "Build the World" 접근 방식을 사용합니다. 이는 Sanitizer 사용을 가능하게 하고, 모든 지원 플랫폼에서 재현 가능한 빌드를 제공하며, 대상 시스템에서의 사전 설치 요구사항을 최소화합니다. 물론, 미리 빌드된 바이너리를 사용하는 것도 가능하며, Vulkan을 사용하는 방식에는 영향을 주지 않습니다. + +## 라이브러리 + +1. 창과 입력, Surface를 위한 [GLFW](https://github.com/glfw/glfw) +1. Vulkan과 상호작용하기 위한 [VulkanHPP](https://github.com/KhronosGroup/Vulkan-Hpp), [Vulkan-Headers](https://github.com/KhronosGroup/Vulkan-Headers)를 통해 사용. + 1. Vulkan은 C API이지만, 공식 C++ 래핑 라이브러리가 제공되어 많은 편리한 기능을 제공합니다. 이 가이드는 C API를 사용하는 다른 라이브러리들(예: GLFW,VMA)을 사용할 때를 제외하고는 거의 C++만 사용합니다. +2. Vulkan 메모리 힙을 다루기 위한 [Vulkan Memory Allocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator/) +3. C++에서 GLSL과 유사한 선형대수학을 위한 [GLM](https://github.com/g-truc/glm) +4. UI를 위한 [Dear ImGui](https://github.com/ocornut/imgui) diff --git a/guide/translations/ko-KR/src/getting_started/class_app.md b/guide/translations/ko-KR/src/getting_started/class_app.md new file mode 100644 index 0000000..25924ca --- /dev/null +++ b/guide/translations/ko-KR/src/getting_started/class_app.md @@ -0,0 +1,39 @@ +# Application + +`class App`은 전체 애플리케이션의 소유자이자 실행 주체 역할을 합니다. 인스턴스는 하나뿐이지만, 클래스를 사용함으로써 RAII의 이점을 활용해 자원을 올바른 순서로 자동 해제할 수 있으며, 전역 변수를 사용할 필요도 없습니다. + +```cpp +// app.hpp +namespace lvk { +class App { + public: + void run(); +}; +} // namespace lvk + +// app.cpp +namespace lvk { +void App::run() { + // TODO +} +} // namespace lvk +``` + +## Main + +`main.cpp`는 많은 역할을 하지 않습니다. 주로 실제 진입점으로 제어를 넘기고, 치명적인 예외를 처리하는 역할을 합니다. + +```cpp +// main.cpp +auto main() -> int { + try { + lvk::App{}.run(); + } catch (std::exception const& e) { + std::println(stderr, "PANIC: {}", e.what()); + return EXIT_FAILURE; + } catch (...) { + std::println("PANIC!"); + return EXIT_FAILURE; + } +} +``` diff --git a/guide/translations/ko-KR/src/getting_started/high_level_loader.png b/guide/translations/ko-KR/src/getting_started/high_level_loader.png new file mode 100644 index 0000000..cbfd3b6 Binary files /dev/null and b/guide/translations/ko-KR/src/getting_started/high_level_loader.png differ diff --git a/guide/translations/ko-KR/src/getting_started/project_layout.md b/guide/translations/ko-KR/src/getting_started/project_layout.md new file mode 100644 index 0000000..3e61b2d --- /dev/null +++ b/guide/translations/ko-KR/src/getting_started/project_layout.md @@ -0,0 +1,22 @@ +# 프로젝트 레이아웃 + +이 페이지는 이 가이드에서 사용하는 코드 레이아웃에 대해 설명합니다. 여기서 설명하는 내용은 이 가이드에서의 참고사항일 뿐이며, Vulkan 사용과는 관련이 없습니다. + +외부 의존성은 zip파일로 묶여있으며, CMake가 구성(configure) 단계에서 이를 압축 해제합니다. FetchContent를 사용하는 것도 유효한 대안입니다. + +`Ninja Multi-Config`는 OS나 컴파일러에 관계없이 사용되는 기본 생성기로 가정합니다. 이는 프로젝트 루트 디렉토리에 있는 `CMakePresets.json`파일에 설정되어 있으며, 사용자 정의 프리셋은 `CMakeUserPresets.json`을 통해 추가할 수 있습니다. + +> Windows에서는 Visual Studio의 CMake Mode가 이 생성기를 사용하며, 자동으로 프리셋을 불러옵니다. Visual Studio Code에서는 CMake Tools 확장이 자동으로 프리셋을 사용합니다. 그 외의 IDE에서는 CMake 프리셋 사용 방법에 대한 해당 IDE의 문서를 참고하세요. + +**Filesystem** + +``` +. +|-- CMakeLists.txt <== executable target +|-- CMakePresets.json +|-- [other project files] +|-- ext/ +│ |-- CMakeLists.txt <== external dependencies target +|-- src/ + |-- [sources and headers] +``` \ No newline at end of file diff --git a/guide/translations/ko-KR/src/getting_started/validation_layers.md b/guide/translations/ko-KR/src/getting_started/validation_layers.md new file mode 100644 index 0000000..dbbc529 --- /dev/null +++ b/guide/translations/ko-KR/src/getting_started/validation_layers.md @@ -0,0 +1,11 @@ +# 검증 레이어 + +애플리케이션이 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) 입니다. + +![Vulkan Loader](high_level_loader.png) + +크로노스 그룹의 [권장사항](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가 디버거를 중단시킴) 같은 기능도 포함됩니다. 이와 더불어, 애플리케이션 내부에 별도의 검증 레이어 관련 코드를 작성할 필요가 없어집니다. + +> 주의 : 개발(또는 데스크탑) 환경의 `PATH` 환경 변수를 수정하거나, 지원되는 시스템에서는 `LD_LIBRARY_PATH`를 사용하여 SDK의 바이너리 경로가 우선적으로 인식되도록 설정해야 합니다. + +![Vulkan Configurator](./vkconfig_gui.png) diff --git a/guide/translations/ko-KR/src/getting_started/vkconfig_gui.png b/guide/translations/ko-KR/src/getting_started/vkconfig_gui.png new file mode 100644 index 0000000..4ebe0fa Binary files /dev/null and b/guide/translations/ko-KR/src/getting_started/vkconfig_gui.png differ diff --git a/guide/translations/ko-KR/src/initialization/README.md b/guide/translations/ko-KR/src/initialization/README.md new file mode 100644 index 0000000..839ee01 --- /dev/null +++ b/guide/translations/ko-KR/src/initialization/README.md @@ -0,0 +1,12 @@ +# 초기화 + +여기서는 다음을 포함하여, 애플리케이션 실행에 필요한 모든 시스템의 초기화 과정을 다룹니다. + +- GLFW를 초기화하고 창 생성하기 +- Vulkan Instance 생성하기 +- Vulkan Surface 생성하기 +- Vulkan Physical Device 선택하기 +- Vulkan logical Device 생성하기 +- Vulkan Swapchain 생성하기 + +여기서 어느 한 단계라도 실패하면, 그 이후에는 의미 있는 작업을 진행할 수 없기 때문에 치명적인 오류로 간주됩니다. diff --git a/guide/translations/ko-KR/src/initialization/device.md b/guide/translations/ko-KR/src/initialization/device.md new file mode 100644 index 0000000..1165677 --- /dev/null +++ b/guide/translations/ko-KR/src/initialization/device.md @@ -0,0 +1,66 @@ +# Vulkan 디바이스 + +[디바이스](https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#devsandqueues-devices)는 Physical Device의 논리적 인스턴스이며, 이후의 모든 Vulkan 작업에서 주요 인터페이스 역할을 하게 됩니다. [큐](https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#devsandqueues-queues)는 디바이스가 소유하는 것으로, `Gpu` 구조체에 저장된 큐 패밀리에서 하나를 가져와 기록된 커맨드 버퍼를 제출하는 데 사용할 것입니다. 또한 사용하기를 원하는 [Dynamic Rendering](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_dynamic_rendering.html) 과 [Synchronization2](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_synchronization2.html)같은 기능들을 명시적으로 선언해야 합니다. + +`vk::QueueCreateInfo`객체를 설정합시다. + +```cpp +auto queue_ci = vk::DeviceQueueCreateInfo{}; +// since we use only one queue, it has the entire priority range, ie, 1.0 +static constexpr auto queue_priorities_v = std::array{1.0f}; +queue_ci.setQueueFamilyIndex(m_gpu.queue_family) + .setQueueCount(1) + .setQueuePriorities(queue_priorities_v); +``` + +핵심 디바이스 기능을 설정합니다. + +```cpp +// nice-to-have optional core features, enable if GPU supports them. +auto enabled_features = vk::PhysicalDeviceFeatures{}; +enabled_features.fillModeNonSolid = m_gpu.features.fillModeNonSolid; +enabled_features.wideLines = m_gpu.features.wideLines; +enabled_features.samplerAnisotropy = m_gpu.features.samplerAnisotropy; +enabled_features.sampleRateShading = m_gpu.features.sampleRateShading; +``` + +추가 기능을 설정하기 위해 `setPNext()`를 사용해 묶습니다. + +```cpp +// extra features that need to be explicitly enabled. +auto sync_feature = vk::PhysicalDeviceSynchronization2Features{vk::True}; +auto dynamic_rendering_feature = + vk::PhysicalDeviceDynamicRenderingFeatures{vk::True}; +// sync_feature.pNext => dynamic_rendering_feature, +// and later device_ci.pNext => sync_feature. +// this is 'pNext chaining'. +sync_feature.setPNext(&dynamic_rendering_feature); +``` + +`vk::DeviceCreateInfo` 구조체를 설정합니다. + +```cpp +auto device_ci = vk::DeviceCreateInfo{}; +// we only need one device extension: Swapchain. +static constexpr auto extensions_v = + std::array{VK_KHR_SWAPCHAIN_EXTENSION_NAME}; +device_ci.setPEnabledExtensionNames(extensions_v) + .setQueueCreateInfos(queue_ci) + .setPEnabledFeatures(&enabled_features) + .setPNext(&sync_feature); +``` + +`vk::UniqueDevice` 멤버를 `m_gpu` 이후에 선언하고, 이를 생성한 다음 디스패쳐를 해당 디바이스로 다시 초기화합니다. + +```cpp +m_device = m_gpu.device.createDeviceUnique(device_ci); +// initialize the dispatcher against the created Device. +VULKAN_HPP_DEFAULT_DISPATCHER.init(*m_device); +``` + +`vk::Queue` 멤버도 선언하고 초기화합니다(순서는 중요하지 않습니다. 이는 단순한 핸들이며 실제 큐는 디바이스가 관리하기 때문입니다). + +```cpp +static constexpr std::uint32_t queue_index_v{0}; +m_queue = m_device->getQueue(m_gpu.queue_family, queue_index_v); +``` diff --git a/guide/translations/ko-KR/src/initialization/glfw_window.md b/guide/translations/ko-KR/src/initialization/glfw_window.md new file mode 100644 index 0000000..5dc7261 --- /dev/null +++ b/guide/translations/ko-KR/src/initialization/glfw_window.md @@ -0,0 +1,88 @@ +# GLFW Window + +창 생성과 이벤트 처리를 위해 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`을 통해 지정할 수 있습니다. + +Vulkan-GLFW 애플리케이션에서 다중 창을 사용할 수는 있지만, 이 가이드에서는 다루지 않습니다. 여기서는 GLFW 라이브러리와 단일 창을 하나의 단위로 보고 함께 초기화하고 해제하는 방식으로 구성합니다. GLFW는 구조를 알 수 없는(Opaque) 포인터 `GLFWwindow*`를 반환하므로, 이를 `std::unique_ptr`과 커스텀 파괴자를 사용해 캡슐화하는 것이 적절합니다. + +```cpp +// window.hpp +namespace lvk::glfw { +struct Deleter { + void operator()(GLFWwindow* window) const noexcept; +}; + +using Window = std::unique_ptr; + +// Returns a valid Window if successful, else throws. +[[nodiscard]] auto create_window(glm::ivec2 size, char const* title) -> Window; +} // namespace lvk::glfw + +// window.cpp +void Deleter::operator()(GLFWwindow* window) const noexcept { + glfwDestroyWindow(window); + glfwTerminate(); +} +``` + +GLFW는 전체 화면이나 테두리 없는 창을 만들 수 있지만, 이 가이드에서는 일반 창을 사용합니다. 창을 생성하지 못하면 이후 작업이 불가능하기 때문에, 실패한 경우에는 모두 치명적인 예외를 발생시키도록 되어 있습니다. + +```cpp +auto glfw::create_window(glm::ivec2 const size, char const* title) -> Window { + static auto const on_error = [](int const code, char const* description) { + std::println(stderr, "[GLFW] Error {}: {}", code, description); + }; + glfwSetErrorCallback(on_error); + if (glfwInit() != GLFW_TRUE) { + throw std::runtime_error{"Failed to initialize GLFW"}; + } + // check for Vulkan support. + if (glfwVulkanSupported() != GLFW_TRUE) { + throw std::runtime_error{"Vulkan not supported"}; + } + auto ret = Window{}; + // tell GLFW that we don't want an OpenGL context. + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + ret.reset(glfwCreateWindow(size.x, size.y, title, nullptr, nullptr)); + if (!ret) { throw std::runtime_error{"Failed to create GLFW Window"}; } + return ret; +} +``` + +`App`은 이제 `glfw::Window`를 멤버로 저장해 사용자가 창을 닫을 때 까지 `run()`에서 이를 사용할 수 있습니다. 아직은 창에 아무것도 그릴 수 없지만, 이는 앞으로의 과정을 시작하는 첫 단계입니다. + +이를 private 멤버로 선언합니다. + +```cpp +private: + glfw::Window m_window{}; +``` + +각 작업을 캡슐화하는 몇 가지 private 멤버 함수를 추가합니다. + +```cpp +void create_window(); + +void main_loop(); +``` + +이를 구현하고 `run()`에서 호출합니다. + +```cpp +void App::run() { + create_window(); + + main_loop(); +} + +void App::create_window() { + m_window = glfw::create_window({1280, 720}, "Learn Vulkan"); +} + +void App::main_loop() { + while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) { + glfwPollEvents(); + } +} +``` + +> Wayland에서는 아직 창이 보이지 않을 수 있습니다. 창은 애플리케이션이 프레임버퍼를 렌더링한 후에야 화면에 표시됩니다. diff --git a/guide/translations/ko-KR/src/initialization/gpu.md b/guide/translations/ko-KR/src/initialization/gpu.md new file mode 100644 index 0000000..4b5e36c --- /dev/null +++ b/guide/translations/ko-KR/src/initialization/gpu.md @@ -0,0 +1,94 @@ +# Vulkan 물리 디바이스 + +[물리 디바이스](https://docs.vulkan.org/spec/latest/chapters/devsandqueues.html#devsandqueues-physical-device-enumeration)는 Vulkan의 완전한 구현체를 나타내며, 일반적으로 하나의 GPU를 의미합니다(단, Mesa/lavapipe와 같은 소프트웨어 렌더러일 수도 있습니다). 여러 GPU가 장착된 랩탑과 같은 일부 기기에서는 여러 개의 물리 디바이스가 존재할 수 있습니다. 이 중에서 다음 조건을 만족하는 것을 하나 선택해야 합니다. + +1. Vulkan 1.3을 지원해야 합니다. +2. 스왑체인을 지원해야 합니다. +3. Graphics와 Transfer작업을 지원하는 Vulkan Queue가 존재해야 합니다. +4. 이전에 생성한 Vulkan Surface로 출력(present)할 수 있어야 합니다. +5. (선택 사항) 외장 GPU를 우선적으로 고려합니다. + +실제 물리 디바이스와 몇 가지 유용한 객체들을 `struct Gpu`로 묶겠습니다. 많은 유틸리티 함수가 함께 정의될 것이므로 이를 별도의 hpp, cpp파일에 구현하고 기존의 `vk_version_v` 상수를 이 새로운 헤더로 옮기겠습니다. + +```cpp +constexpr auto vk_version_v = VK_MAKE_VERSION(1, 3, 0); + +struct Gpu { + vk::PhysicalDevice device{}; + vk::PhysicalDeviceProperties properties{}; + vk::PhysicalDeviceFeatures features{}; + std::uint32_t queue_family{}; +}; + +[[nodiscard]] auto get_suitable_gpu(vk::Instance instance, + vk::SurfaceKHR surface) -> Gpu; +``` + +아래는 구현부입니다. + +```cpp +auto lvk::get_suitable_gpu(vk::Instance const instance, + vk::SurfaceKHR const surface) -> Gpu { + auto const supports_swapchain = [](Gpu const& gpu) { + static constexpr std::string_view name_v = + VK_KHR_SWAPCHAIN_EXTENSION_NAME; + static constexpr auto is_swapchain = + [](vk::ExtensionProperties const& properties) { + return properties.extensionName.data() == name_v; + }; + auto const properties = gpu.device.enumerateDeviceExtensionProperties(); + auto const it = std::ranges::find_if(properties, is_swapchain); + return it != properties.end(); + }; + + auto const set_queue_family = [](Gpu& out_gpu) { + static constexpr auto queue_flags_v = + vk::QueueFlagBits::eGraphics | vk::QueueFlagBits::eTransfer; + for (auto const [index, family] : + std::views::enumerate(out_gpu.device.getQueueFamilyProperties())) { + if ((family.queueFlags & queue_flags_v) == queue_flags_v) { + out_gpu.queue_family = static_cast(index); + return true; + } + } + return false; + }; + + auto const can_present = [surface](Gpu const& gpu) { + return gpu.device.getSurfaceSupportKHR(gpu.queue_family, surface) == + vk::True; + }; + + auto fallback = Gpu{}; + for (auto const& device : instance.enumeratePhysicalDevices()) { + auto gpu = Gpu{.device = device, .properties = device.getProperties()}; + if (gpu.properties.apiVersion < vk_version_v) { continue; } + if (!supports_swapchain(gpu)) { continue; } + if (!set_queue_family(gpu)) { continue; } + if (!can_present(gpu)) { continue; } + gpu.features = gpu.device.getFeatures(); + if (gpu.properties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) { + return gpu; + } + // keep iterating in case we find a Discrete Gpu later. + fallback = gpu; + } + if (fallback.device) { return fallback; } + + throw std::runtime_error{"No suitable Vulkan Physical Devices"}; +} +``` + +마지막으로 `Gpu` 멤버를 `App`에 추가하고 `create_surface()`이후에 초기화합니다. + +```cpp +create_surface(); +select_gpu(); + +// ... +void App::select_gpu() { + m_gpu = get_suitable_gpu(*m_instance, *m_surface); + std::println("Using GPU: {}", + std::string_view{m_gpu.properties.deviceName}); +} +``` diff --git a/guide/translations/ko-KR/src/initialization/instance.md b/guide/translations/ko-KR/src/initialization/instance.md new file mode 100644 index 0000000..7382c72 --- /dev/null +++ b/guide/translations/ko-KR/src/initialization/instance.md @@ -0,0 +1,87 @@ +# Vulkan 인스턴스 + +Vulkan을 SDK를 통해 빌드 시점에 링킹하는 대신, 런타임에 동적으로 로드할 것입니다. 이를 위해 몇 가지 조정이 필요합니다. + +1. CMake의 ext target에서 `VK_NO_PROTOTYPES`를 정의해, Vulkan API 함수 선언이 함수 포인터로 변환되도록 합니다. +2. `app.cpp`에서 전역에 `VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE`를 추가합니다. +3. 초기화 이전과 초기화 과정 중에 `VULKAN_HPP_DEFAULT_DISPATCHER.init()`을 호출합니다. + +Vulkan에서 가장 먼저 해야할 것은 [인스턴스](https://docs.vulkan.org/spec/latest/chapters/initialization.html#initialization-instances)를 생성하는 것입니다. 이는 물리 디바이스(GPU)의 목록을 가져오거나, 논리 디바이스를 생성할 수 있습니다. + +Vulkan 1.3 버전을 필요로 하므로, 이를 상수로 정의해 쉽게 참조할 수 있도록 합니다. + +```cpp +namespace { +constexpr auto vk_version_v = VK_MAKE_VERSION(1, 3, 0); +} // namespace +``` + +`App`클래스에 새로운 멤버 함수 `create_instance()`를 추가하고, `run()`함수 내에서 `create_window()`호출 직후에 이를 호출하세요. 디스패쳐를 초기화한 후에는, 로더가 요구하는 Vulkan 버전을 충족하는지 확인합니다. + +```cpp +void App::create_instance() { + // initialize the dispatcher without any arguments. + VULKAN_HPP_DEFAULT_DISPATCHER.init(); + auto const loader_version = vk::enumerateInstanceVersion(); + if (loader_version < vk_version_v) { + throw std::runtime_error{"Loader does not support Vulkan 1.3"}; + } +} +``` + +WSI 관련 인스턴스 확장이 필요하며, 이는 GLFW가 제공해줍니다. 관련 확장을 받아오는 함수를 `window.hpp/cpp`에 추가합니다. + +```cpp +auto glfw::instance_extensions() -> std::span { + auto count = std::uint32_t{}; + auto const* extensions = glfwGetRequiredInstanceExtensions(&count); + return {extensions, static_cast(count)}; +} +``` + +인스턴스 생성에 이어서, `vk::ApplicationInfo`객체를 생성하고 필요한 정보를 채워 넣습니다. + +```cpp +auto app_info = vk::ApplicationInfo{}; +app_info.setPApplicationName("Learn Vulkan").setApiVersion(vk_version_v); +``` + +`vk::InstanceCreateInfo` 구조체를 생성하고 초기화합니다. + +```cpp +auto instance_ci = vk::InstanceCreateInfo{}; +// need WSI instance extensions here (platform-specific Swapchains). +auto const extensions = glfw::instance_extensions(); +instance_ci.setPApplicationInfo(&app_info).setPEnabledExtensionNames( + extensions); +``` + +`m_window`멤버 다음에 `vk::UniqueInstance` 멤버를 추가하세요. 이는 GLFW 종료 전에 반드시 파괴되어야 하므로 순서를 지키는 것이 중요합니다. 인스턴스를 생성한 뒤, 해당 인스턴스를 기반으로 디스패쳐를 다시 초기화합니다. + +```cpp +glfw::Window m_window{}; +vk::UniqueInstance m_instance{}; + +// ... +// initialize the dispatcher against the created Instance. +m_instance = vk::createInstanceUnique(instance_ci); +VULKAN_HPP_DEFAULT_DISPATCHER.init(*m_instance); +``` + +VkConfig가 검증 레이어가 활성화된 상태인지 확인한 후, 애플리케이션을 디버그 혹은 실행하세요. 만약 로더 메시지의 "Information" 레벨 로그가 활성화되어 있다면, 이 시점에서 로드된 레이어 정보, 물리 디바이스 및 해당 ICD의 열거 등 다양한 콘솔 출력이 보일 것입니다. + +해당 메시지 또는 유사한 로그가 보이지 않는다면, Vulkan Configurator 설정과 환경변수 `PATH`를 다시 확인해보세요. + +``` +INFO | LAYER: Insert instance layer "VK_LAYER_KHRONOS_validation" +``` + +예를 들어, `libVkLayer_khronos_validation.so` / `VkLayer_khronos_validation.dll`이 애플리케이션 / 로더에 보이지 않는다면 다음과 같은 메시지가 출력될 수 있습니다 + +``` +INFO | LAYER: Requested layer "VK_LAYER_KHRONOS_validation" failed to load. +``` + +축하합니다! 성공적으로 Vulkan Instance를 초기화하였습니다. + +> Wayland 사용자의 경우 아직 창이 보이지 않을 것이기 때문에 현재로서는 VkConfig/Validation 유일한 확인 수단입니다. \ No newline at end of file diff --git a/guide/translations/ko-KR/src/initialization/scoped_waiter.md b/guide/translations/ko-KR/src/initialization/scoped_waiter.md new file mode 100644 index 0000000..9548a17 --- /dev/null +++ b/guide/translations/ko-KR/src/initialization/scoped_waiter.md @@ -0,0 +1,64 @@ +# Scoped Waiter + +소멸자에서 디바이스가 Idle한 상태가 될 때까지 기다리거나 블록하는 객체는 매우 유용한 추상화입니다. GPU가 Vulkan 객체를 사용 중일 때 해당 객체를 파괴하는 것은 잘못된 사용입니다. 이 객체는 의존성이 있는 자원이 파괴되기 전에 디바이스가 idle 상태임을 보장하는 데 도움이 됩니다. + +스코프가 끝날 때 임의의 작업을 수행할 수 있는 기능은 다른 곳에서도 유용할 수 있기 때문에, 이를 기본 템플릿 클래스 `Scoped`로 캡슐화합니다. 이 클래스는 포인터 타입 `Type*` 대신에 값 `Type`을 담는 `unique_ptr`와 유사하지만, 다음과 같은 제약이 있습니다. + +1. `Type`은 기본 생성자가 있어야 합니다. +2. 기본 생성자를 통한 `Type`은 null과 동일하다고 가정하며, 이 경우 `Deleter`를 호출하지 않습니다. + +```cpp +template +concept Scopeable = + std::equality_comparable && std::is_default_constructible_v; + +template +class Scoped { + public: + Scoped(Scoped const&) = delete; + auto operator=(Scoped const&) = delete; + + Scoped() = default; + + constexpr Scoped(Scoped&& rhs) noexcept + : m_t(std::exchange(rhs.m_t, Type{})) {} + + constexpr auto operator=(Scoped&& rhs) noexcept -> Scoped& { + if (&rhs != this) { std::swap(m_t, rhs.m_t); } + return *this; + } + + explicit(false) constexpr Scoped(Type t) : m_t(std::move(t)) {} + + constexpr ~Scoped() { + if (m_t == Type{}) { return; } + Deleter{}(m_t); + } + + [[nodiscard]] constexpr auto get() const -> Type const& { return m_t; } + [[nodiscard]] constexpr auto get() -> Type& { return m_t; } + + private: + Type m_t{}; +}; +``` + +이 내용이 이해가 되지 않더라도 걱정하지 마세요. 구현 자체는 중요하지 않고, 이 객체가 무엇을 하는지, 그리고 어떻게 사용하는지가 중요합니다. + +`ScopeWaiter`는 이제 비교적 간단하게 구현할 수 있습니다. + +```cpp +struct ScopedWaiterDeleter { + void operator()(vk::Device const device) const noexcept { + device.waitIdle(); + } +}; + +using ScopedWaiter = Scoped; +``` + +`ScopeWaiter` 멤버를 `App`의 멤버 리스트 맨 마지막에 추가하세요. 이는 반드시 마지막에 선언되어야 하며, 그렇게 해야 이 멤버가 가장 먼저 파괴되기 때문에 다른 멤버들이 파괴되기 전에 idle 상태가 되는 것을 보장할 수 있습니다. 이를 디바이스 생성 후에 초기화합니다. + +```cpp +m_waiter = *m_device; +``` diff --git a/guide/translations/ko-KR/src/initialization/surface.md b/guide/translations/ko-KR/src/initialization/surface.md new file mode 100644 index 0000000..cde3f23 --- /dev/null +++ b/guide/translations/ko-KR/src/initialization/surface.md @@ -0,0 +1,26 @@ +# Vulkan Surface + +Vulkan은 플랫폼과 독립적으로 작동하기 위해 [`VK_KHR_surface` 확장](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_surface.html)을 통해 WSI와 상호작용합니다. [Surface](https://docs.vulkan.org/guide/latest/wsi.html#_surface)는 프레젠테이션 엔진을 통해 창에 이미지를 표시할 수 있게 해줍니다. + +`window.hpp/cpp`에 또 다른 함수를 추가합시다. + +```cpp +auto glfw::create_surface(GLFWwindow* window, vk::Instance const instance) + -> vk::UniqueSurfaceKHR { + VkSurfaceKHR ret{}; + auto const result = + glfwCreateWindowSurface(instance, window, nullptr, &ret); + if (result != VK_SUCCESS || ret == VkSurfaceKHR{}) { + throw std::runtime_error{"Failed to create Vulkan Surface"}; + } + return vk::UniqueSurfaceKHR{ret, instance}; +} +``` + +`App`에 `vk::UniqueSurfaceKHR`이라는 멤버를 `m_instance` 이후에 추가하고 Surface를 생성합니다. + +```cpp +void App::create_surface() { + m_surface = glfw::create_surface(m_window.get(), *m_instance); +} +``` diff --git a/guide/translations/ko-KR/src/initialization/swapchain.md b/guide/translations/ko-KR/src/initialization/swapchain.md new file mode 100644 index 0000000..98a3d08 --- /dev/null +++ b/guide/translations/ko-KR/src/initialization/swapchain.md @@ -0,0 +1,226 @@ +# 스왑체인 + +[스왑체인](https://docs.vulkan.org/guide/latest/wsi.html#_swapchain)은 Surface와 연결된, 화면에 표시 가능한 이미지들의 배열입니다. 이는 애플리케이션과 플랫폼의 프레젠테이션 엔진 사이를 이어주는 다리 역할을 합니다. 스왑체인은 메인 루프에서 이미지를 받아오고 화면에 표시하기 위해 지속적으로 사용됩니다. 스왑체인 생성에 실패하는 것은 치명적인 오류이므로, 그 생성 과정은 초기화 단계에 포함됩니다. + +스왑체인을 우리가 정의한 `class Swapchain`으로 감쌀 것입니다. 이 클래스는 스왑체인이 소유한 이미지의 복사본을 저장하고, 각 이미지에 맞는 이미지 뷰를 생성하고 소유합니다. 스왑체인은 프레임 버퍼 크기가 변경되거나, acquire/present 작업이 `vk::ErrorOutOfDataKHR`를 반환하는 경우처럼, 메인 루프 중에 재생성이 필요할 수 있습니다. 이를 `recreate()` 함수로 캡슐화하여 초기화 시점에도 간단히 호출할 수 있도록 하겠습니다. + +```cpp +// swapchain.hpp +class Swapchain { + public: + explicit Swapchain(vk::Device device, Gpu const& gpu, + vk::SurfaceKHR surface, glm::ivec2 size); + + auto recreate(glm::ivec2 size) -> bool; + + [[nodiscard]] auto get_size() const -> glm::ivec2 { + return {m_ci.imageExtent.width, m_ci.imageExtent.height}; + } + + private: + void populate_images(); + void create_image_views(); + + vk::Device m_device{}; + Gpu m_gpu{}; + + vk::SwapchainCreateInfoKHR m_ci{}; + vk::UniqueSwapchainKHR m_swapchain{}; + std::vector m_images{}; + std::vector m_image_views{}; +}; + +// swapchain.cpp +Swapchain::Swapchain(vk::Device const device, Gpu const& gpu, + vk::SurfaceKHR const surface, glm::ivec2 const size) + : m_device(device), m_gpu(gpu) {} +``` + +## 정적 스왑체인 속성 + +이미지 크기나 개수와 같은 몇몇 스왑체인 생성 파라미터는 surface capabilities에 따라 결정되며, 이는 런타임 중 변경될 수 있습니다. 나머지 설정은 생성자에서 처리할 수 있으며, 이때 필요한 [Surface Format](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSurfaceFormatKHR.html)을 얻기 위한 함수가 필요합니다. + +```cpp +constexpr auto srgb_formats_v = std::array{ + vk::Format::eR8G8B8A8Srgb, + vk::Format::eB8G8R8A8Srgb, +}; + +// returns a SurfaceFormat with SrgbNonLinear color space and an sRGB format. +[[nodiscard]] constexpr auto +get_surface_format(std::span supported) + -> vk::SurfaceFormatKHR { + for (auto const desired : srgb_formats_v) { + auto const is_match = [desired](vk::SurfaceFormatKHR const& in) { + return in.format == desired && + in.colorSpace == + vk::ColorSpaceKHR::eVkColorspaceSrgbNonlinear; + }; + auto const it = std::ranges::find_if(supported, is_match); + if (it == supported.end()) { continue; } + return *it; + } + return supported.front(); +} +``` + +sRGB 포맷이 선호되는 이유는 화면의 색상 공간이 sRGB이기 때문입니다. 이는 Vulkan의 기본 [색상 영역](https://registry.khronos.org/vulkan/specs/latest/man/html/VkColorSpaceKHR.html)이 `vk::ColorSpaceKHR::eVkColorspaceSrgbNonlinear` 하나 뿐이라는 사실에서 알 수 있습니다. 이 값은 sRGB 색상 공간의 이미지를 지원함을 나타냅니다. + +이제 생성자를 구현할 수 있습니다. + +```cpp +auto const surface_format = + get_surface_format(m_gpu.device.getSurfaceFormatsKHR(surface)); +m_ci.setSurface(surface) + .setImageFormat(surface_format.format) + .setImageColorSpace(surface_format.colorSpace) + .setImageArrayLayers(1) + // Swapchain images will be used as color attachments (render targets). + .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment) + // eFifo is guaranteed to be supported. + .setPresentMode(vk::PresentModeKHR::eFifo); +if (!recreate(size)) { + throw std::runtime_error{"Failed to create Vulkan Swapchain"}; +} +``` + +## 스왑체인 재생성 + +스왑체인 생성 파라미터의 제약은 [Surface Capabilities](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSurfaceCapabilitiesKHR.html)에 지정됩니다. 사양에 따라 함수 두 개와 상수 하나를 추가하겠습니다. + +```cpp +constexpr std::uint32_t min_images_v{3}; + +// returns currentExtent if specified, else clamped size. +[[nodiscard]] constexpr auto +get_image_extent(vk::SurfaceCapabilitiesKHR const& capabilities, + glm::uvec2 const size) -> vk::Extent2D { + constexpr auto limitless_v = 0xffffffff; + if (capabilities.currentExtent.width < limitless_v && + capabilities.currentExtent.height < limitless_v) { + return capabilities.currentExtent; + } + auto const x = std::clamp(size.x, capabilities.minImageExtent.width, + capabilities.maxImageExtent.width); + auto const y = std::clamp(size.y, capabilities.minImageExtent.height, + capabilities.maxImageExtent.height); + return vk::Extent2D{x, y}; +} + +[[nodiscard]] constexpr auto +get_image_count(vk::SurfaceCapabilitiesKHR const& capabilities) + -> std::uint32_t { + if (capabilities.maxImageCount < capabilities.minImageCount) { + return std::max(min_images_v, capabilities.minImageCount); + } + return std::clamp(min_images_v, capabilities.minImageCount, + capabilities.maxImageCount); +} +``` + +트리플 버퍼링을 설정할 수 있도록 최소한 세 개의 이미지가 필요합니다. Surface의 `maxImageCount < 3`일 가능성도 있지만, 그럴 일은 거의 없습니다. 오히려 `minImageCount > 3`인 경우가 더 흔합니다. + +Vulkan 이미지의 차원은 양수여야만 하므로, 전달받은 프레임 버퍼 크기가 유효하지 않다면 재생성을 생략합니다. 예를 들어 윈도우가 최소화된 경우 이런 상황이 발생할 수 있습니다(이때는 창이 복원될 때까지 렌더링이 일시 중지됩니다). + +```cpp +auto Swapchain::recreate(glm::ivec2 size) -> bool { + // Image sizes must be positive. + if (size.x <= 0 || size.y <= 0) { return false; } + + auto const capabilities = + m_gpu.device.getSurfaceCapabilitiesKHR(m_ci.surface); + m_ci.setImageExtent(get_image_extent(capabilities, size)) + .setMinImageCount(get_image_count(capabilities)) + .setOldSwapchain(m_swapchain ? *m_swapchain : vk::SwapchainKHR{}) + .setQueueFamilyIndices(m_gpu.queue_family); + assert(m_ci.imageExtent.width > 0 && m_ci.imageExtent.height > 0 && + m_ci.minImageCount >= min_images_v); + + // wait for the device to be idle before destroying the current swapchain. + m_device.waitIdle(); + m_swapchain = m_device.createSwapchainKHRUnique(m_ci); + + return true; +} +``` + +재생성에 성공한 후에는 이미지와 이미지 뷰 벡터를 채워야 합니다. 이미지의 경우, 매번 새로 반환된 벡터를 멤버 변수에 할당하지 않기 위해 약간 더 복잡한 방식으로 접근합니다. + +```cpp +void require_success(vk::Result const result, char const* error_msg) { + if (result != vk::Result::eSuccess) { throw std::runtime_error{error_msg}; } +} + +// ... +void Swapchain::populate_images() { + // we use the more verbose two-call API to avoid assigning m_images to a new + // vector on every call. + auto image_count = std::uint32_t{}; + auto result = + m_device.getSwapchainImagesKHR(*m_swapchain, &image_count, nullptr); + require_success(result, "Failed to get Swapchain Images"); + + m_images.resize(image_count); + result = m_device.getSwapchainImagesKHR(*m_swapchain, &image_count, + m_images.data()); + require_success(result, "Failed to get Swapchain Images"); +} +``` + +이미지 뷰 생성은 비교적 간단합니다. + +```cpp +void Swapchain::create_image_views() { + auto subresource_range = vk::ImageSubresourceRange{}; + // this is a color image with 1 layer and 1 mip-level (the default). + subresource_range.setAspectMask(vk::ImageAspectFlagBits::eColor) + .setLayerCount(1) + .setLevelCount(1); + auto image_view_ci = vk::ImageViewCreateInfo{}; + // set common parameters here (everything except the Image). + image_view_ci.setViewType(vk::ImageViewType::e2D) + .setFormat(m_ci.imageFormat) + .setSubresourceRange(subresource_range); + m_image_views.clear(); + m_image_views.reserve(m_images.size()); + for (auto const image : m_images) { + image_view_ci.setImage(image); + m_image_views.push_back(m_device.createImageViewUnique(image_view_ci)); + } +} +``` + +이제 이 함수를 `recreate()`에서 `return true` 직전에 호출하고 로그를 찍어봅시다. + +```cpp +populate_images(); +create_image_views(); + +size = get_size(); +std::println("[lvk] Swapchain [{}x{}]", size.x, size.y); +return true; +``` + +> 창 크기를 계속해서 바꿀 경우 로그가 많이 찍힐 수 있습니다(특히 Linux에서). + +프레임 버퍼 크기를 얻기 위해 `window.hpp/cpp`에 함수를 추가합니다. + +```cpp +auto glfw::framebuffer_size(GLFWwindow* window) -> glm::ivec2 { + auto ret = glm::ivec2{}; + glfwGetFramebufferSize(window, &ret.x, &ret.y); + return ret; +} +``` + +마지막으로, `std::optional` 멤버를 `App`의 `m_device` 이후에 추가하고, 생성 함수를 추가하여 `create_device()` 이후에 이를 호출합니다. + +```cpp +std::optional m_swapchain{}; + +// ... +void App::create_swapchain() { + auto const size = glfw::framebuffer_size(m_window.get()); + m_swapchain.emplace(*m_device, m_gpu, *m_surface, size); +} +``` diff --git a/guide/translations/ko-KR/src/memory/README.md b/guide/translations/ko-KR/src/memory/README.md new file mode 100644 index 0000000..bb442d5 --- /dev/null +++ b/guide/translations/ko-KR/src/memory/README.md @@ -0,0 +1,5 @@ +# 메모리 할당 + +명시적인 API인 Vulkan에서는 디바이스가 사용할 메모리를 애플리케이션이 직접 [메모리 할당](https://docs.vulkan.org/guide/latest/memory_allocation.html)을 해야 합니다. 이 과정은 다소 복잡할 수 있기 때문에, Vulkan 사양에서 권장하듯이 이 모든 세부 사항을 [Vulkan Memory Allocator (VMA)](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator)에 맡겨 간단하게 처리할 것입니다. + +Vulkan은 할당된 메모리를 사용하는 두 가지 객체 유형을 제공합니다. 버퍼와 이미지입니다. VMA는 이 둘에 대해 투명한(transparent) 지원을 제공합니다. 우리는 그저 버퍼와 이미지를 디바이스를 통해 직접 할당하는 대신, VMA를 통해 할당 및 해제하면 됩니다. CPU에서의 메모리 할당과 객체 생성과는 달리, Vulkan에는 버퍼와 이미지 생성에는 정렬(alignment)이나 크기(size) 외에도 훨씬 더 많은 파라미터가 필요합니다. 예상하셨겠지만, 여기서는 정점 버퍼, 유니폼/스토리지 버퍼, 그리고 텍스쳐 이미지와 같은 셰이더 자원과 관련된 하위 집합만을 다룰 예정입니다. diff --git a/guide/translations/ko-KR/src/memory/buffers.md b/guide/translations/ko-KR/src/memory/buffers.md new file mode 100644 index 0000000..d43fbc2 --- /dev/null +++ b/guide/translations/ko-KR/src/memory/buffers.md @@ -0,0 +1,94 @@ +# 버퍼 + +먼저 VMA 버퍼를 위한 RAII 래퍼 컴포넌트를 추가합니다. + +```cpp +struct RawBuffer { + [[nodiscard]] auto mapped_span() const -> std::span { + return std::span{static_cast(mapped), size}; + } + + auto operator==(RawBuffer const& rhs) const -> bool = default; + + VmaAllocator allocator{}; + VmaAllocation allocation{}; + vk::Buffer buffer{}; + vk::DeviceSize size{}; + void* mapped{}; +}; + +struct BufferDeleter { + void operator()(RawBuffer const& raw_buffer) const noexcept; +}; + +// ... +void BufferDeleter::operator()(RawBuffer const& raw_buffer) const noexcept { + vmaDestroyBuffer(raw_buffer.allocator, raw_buffer.buffer, + raw_buffer.allocation); +} +``` + +버퍼는 호스트(RAM)와 디바이스(VRAM) 메모리를 기반으로 할당될 수 있습니다. 호스트 메모리는 매핑이 가능하므로 매 프레임마다 바뀌는 정보를 담기에 적합하며, 디바이스 메모리는 GPU에서 접근하기에 빠르지만 데이터를 복사하는 데 더 복잡한 절차가 필요합니다. 이를 고려하여 관련된 타입과 생성 함수를 추가하겠습니다. + +```cpp +struct BufferCreateInfo { + VmaAllocator allocator; + vk::BufferUsageFlags usage; + std::uint32_t queue_family; +}; + +enum class BufferMemoryType : std::int8_t { Host, Device }; + +[[nodiscard]] auto create_buffer(BufferCreateInfo const& create_info, + BufferMemoryType memory_type, + vk::DeviceSize size) -> Buffer; + +// ... +auto vma::create_buffer(BufferCreateInfo const& create_info, + BufferMemoryType const memory_type, + vk::DeviceSize const size) -> Buffer { + if (size == 0) { + std::println(stderr, "Buffer cannot be 0-sized"); + return {}; + } + + auto allocation_ci = VmaAllocationCreateInfo{}; + allocation_ci.flags = + VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + auto usage = create_info.usage; + if (memory_type == BufferMemoryType::Device) { + allocation_ci.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + // device buffers need to support TransferDst. + usage |= vk::BufferUsageFlagBits::eTransferDst; + } else { + allocation_ci.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST; + // host buffers can provide mapped memory. + allocation_ci.flags |= VMA_ALLOCATION_CREATE_MAPPED_BIT; + } + + auto buffer_ci = vk::BufferCreateInfo{}; + buffer_ci.setQueueFamilyIndices(create_info.queue_family) + .setSize(size) + .setUsage(usage); + auto vma_buffer_ci = static_cast(buffer_ci); + + VmaAllocation allocation{}; + VkBuffer buffer{}; + auto allocation_info = VmaAllocationInfo{}; + auto const result = + vmaCreateBuffer(create_info.allocator, &vma_buffer_ci, &allocation_ci, + &buffer, &allocation, &allocation_info); + if (result != VK_SUCCESS) { + std::println(stderr, "Failed to create VMA Buffer"); + return {}; + } + + return RawBuffer{ + .allocator = create_info.allocator, + .allocation = allocation, + .buffer = buffer, + .size = size, + .mapped = allocation_info.pMappedData, + }; +} +``` diff --git a/guide/translations/ko-KR/src/memory/command_block.md b/guide/translations/ko-KR/src/memory/command_block.md new file mode 100644 index 0000000..6ed20ab --- /dev/null +++ b/guide/translations/ko-KR/src/memory/command_block.md @@ -0,0 +1,84 @@ +# Command Block + +오래 유지되는 정점 버퍼의 경우 디바이스 메모리에 위치하는 편이 더 나은 성능을 보입니다. 특히 3D 메시와 같은 경우에 그렇습니다. 데이터를 디바이스 버퍼로 전송하려면 두 단계가 필요합니다. + +1. 호스트 버퍼를 할당하고 데이터를 매핑된 메모리로 복사합니다. +2. 디바이스 버퍼를 할당하고 메모리 복사 명령을 기록한 뒤, 이를 제출합니다. + +두 번째 단계는 커맨드 버퍼와 큐 제출이 필요하며, 제출된 작업이 완료될 때까지 기다려야 합니다. 이 동작을 클래스로 캡슐화하겠습니다. 이 구조는 이미지를 생성할때에도 쓰일 수 있습니다. + +```cpp +class CommandBlock { + public: + explicit CommandBlock(vk::Device device, vk::Queue queue, + vk::CommandPool command_pool); + + [[nodiscard]] auto command_buffer() const -> vk::CommandBuffer { + return *m_command_buffer; + } + + void submit_and_wait(); + + private: + vk::Device m_device{}; + vk::Queue m_queue{}; + vk::UniqueCommandBuffer m_command_buffer{}; +}; +``` + +이 클래스의 생성자는 임시 할당용으로 미리 생성된 커맨드 풀과, 나중에 명령을 제출할 큐를 인자로 받습니다. 이렇게 하면 해당 객체를 생성 후 다른 코드로 전달해 재사용할 수 있습니다. + +```cpp +CommandBlock::CommandBlock(vk::Device const device, vk::Queue const queue, + vk::CommandPool const command_pool) + : m_device(device), m_queue(queue) { + // allocate a UniqueCommandBuffer which will free the underlying command + // buffer from its owning pool on destruction. + auto allocate_info = vk::CommandBufferAllocateInfo{}; + allocate_info.setCommandPool(command_pool) + .setCommandBufferCount(1) + .setLevel(vk::CommandBufferLevel::ePrimary); + // all the current VulkanHPP functions for UniqueCommandBuffer allocation + // return vectors. + auto command_buffers = m_device.allocateCommandBuffersUnique(allocate_info); + m_command_buffer = std::move(command_buffers.front()); + + // start recording commands before returning. + auto begin_info = vk::CommandBufferBeginInfo{}; + begin_info.setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit); + m_command_buffer->begin(begin_info); +} +``` + +`submit_and_wait()`은 커맨드 버퍼의 작업이 끝난 뒤 리셋하여 커맨드 풀로 반환합니다. + +```cpp +void CommandBlock::submit_and_wait() { + if (!m_command_buffer) { return; } + + // end recording and submit. + m_command_buffer->end(); + auto submit_info = vk::SubmitInfo2KHR{}; + auto const command_buffer_info = + vk::CommandBufferSubmitInfo{*m_command_buffer}; + submit_info.setCommandBufferInfos(command_buffer_info); + auto fence = m_device.createFenceUnique({}); + m_queue.submit2(submit_info, *fence); + + // wait for submit fence to be signaled. + static constexpr auto timeout_v = + static_cast(std::chrono::nanoseconds(30s).count()); + auto const result = m_device.waitForFences(*fence, vk::True, timeout_v); + if (result != vk::Result::eSuccess) { + std::println(stderr, "Failed to submit Command Buffer"); + } + // free the command buffer. + m_command_buffer.reset(); +} +``` + +## 멀티쓰레딩 시 고려사항 + +"`submit_and_wait()`를 호출할 때마다 메인 쓰레드를 블록하는 대신, 멀티쓰레드로 CommandBlock을 구현하는 편이 낫지 않을까?" 라고 생각하실 수 있습니다. 맞습니다! 하지만 멀티 쓰레딩을 위해서는 몇 가지 추가 작업이 필요합니다. 각 쓰레드가 고유한 커맨드 풀이 필요합니다. CommandBlock마다 하나의 고유한 커맨드 풀을 사용하며, 임계 영역을 뮤텍스로 보호하는 것, 스왑체인으로부터 이미지를 가져오고 표시하는 작업 등 큐에 대한 모든 작업은 동기화되어야 합니다. 이를 위해 `vk::Queue`객체와 해당 큐를 보호하는 `std::mutex` 포인터 혹은 참조를 보관하는 `class Queue`를 설계할 수 있으며, 큐 제출은 이 클래스를 통해 수행할 것입니다. 이렇게 하면 각 에셋을 불러오는 쓰레드가 고유한 커맨드 풀을 사용하면서도 큐 제출은 안전하게 이루어질 수 있습니다. `VmaAllocator`는 내부적으로 동기화되어 있으며, 빌드 시 동기화를 끌 수도 있지만 기본적으로는 멀티쓰레드 환경에서 안전하게 사용할 수 있습니다. + +멀티쓰레드 렌더링을 구현하려면 각 쓰레드에서 Secondary 커맨드 버퍼에 렌더링 명령을 기록한 후, 이를 Primary 커맨드 버퍼로 모아 `RenderSync`에서 실행할 수 있습니다. 다만 수백개의 드로우콜 정도는 하나의 쓰레드에서 처리하는 편이 더 빠르기 때문에, 수천개의 비싼 드로우콜과 수십개의 렌더패스를 사용하지 않는 한 사용할 일이 없습니다. diff --git a/guide/translations/ko-KR/src/memory/device_buffers.md b/guide/translations/ko-KR/src/memory/device_buffers.md new file mode 100644 index 0000000..5f3a49f --- /dev/null +++ b/guide/translations/ko-KR/src/memory/device_buffers.md @@ -0,0 +1,133 @@ +# 디바이스 버퍼 + +여기서는 정점 버퍼에 디바이스 버퍼만을 사용합니다. 정점과 인덱스 정보를 하나의 VBO로 묶어 사용할 것입니다. 따라서 생성 함수는 데이터를 받아 버퍼 복사를 수행한 후 반환합니다. 기본적으로 반환값은 "GPU 상수" 버퍼일 것입니다. 정점과 인덱스 데이터를 하나의 연속된 바이트로 강제로 묶는 대신, 각 데이터를 별도의 범위로 사용할 수 있도록 생성 함수는 span을 인자로 받습니다. + +```cpp +// disparate byte spans. +using ByteSpans = std::span const>; + +// returns a Device Buffer with each byte span sequentially written. +[[nodiscard]] auto create_device_buffer(BufferCreateInfo const& create_info, + CommandBlock command_block, + ByteSpans const& byte_spans) -> Buffer; +``` + +`create_device_buffer()`는 다음과 같습니다. + +```cpp +auto vma::create_device_buffer(BufferCreateInfo const& create_info, + CommandBlock command_block, + ByteSpans const& byte_spans) -> Buffer { + auto const total_size = std::accumulate( + byte_spans.begin(), byte_spans.end(), 0uz, + [](std::size_t const n, std::span bytes) { + return n + bytes.size(); + }); + + auto staging_ci = create_info; + staging_ci.usage = vk::BufferUsageFlagBits::eTransferSrc; + + // create staging Host Buffer with TransferSrc usage. + auto staging_buffer = + create_buffer(staging_ci, BufferMemoryType::Host, total_size); + // create the Device Buffer. + auto ret = create_buffer(create_info, BufferMemoryType::Device, total_size); + // can't do anything if either buffer creation failed. + if (!staging_buffer.get().buffer || !ret.get().buffer) { return {}; } + + // copy byte spans into staging buffer. + auto dst = staging_buffer.get().mapped_span(); + for (auto const bytes : byte_spans) { + std::memcpy(dst.data(), bytes.data(), bytes.size()); + dst = dst.subspan(bytes.size()); + } + + // record buffer copy operation. + auto buffer_copy = vk::BufferCopy2{}; + buffer_copy.setSize(total_size); + auto copy_buffer_info = vk::CopyBufferInfo2{}; + copy_buffer_info.setSrcBuffer(staging_buffer.get().buffer) + .setDstBuffer(ret.get().buffer) + .setRegions(buffer_copy); + command_block.command_buffer().copyBuffer2(copy_buffer_info); + + // submit and wait. + // waiting here is necessary to keep the staging buffer alive while the GPU + // accesses it through the recorded commands. + // this is also why the function takes ownership of the passed CommandBlock + // instead of just referencing it / taking a vk::CommandBuffer. + command_block.submit_and_wait(); + + return ret; +} +``` + +`App`에 command block pool을 추가하고 commandblock을 생성하는 함수를 추가합니다. + +```cpp +void App::create_cmd_block_pool() { + auto command_pool_ci = vk::CommandPoolCreateInfo{}; + command_pool_ci + .setQueueFamilyIndex(m_gpu.queue_family) + // this flag indicates that the allocated Command Buffers will be + // short-lived. + .setFlags(vk::CommandPoolCreateFlagBits::eTransient); + m_cmd_block_pool = m_device->createCommandPoolUnique(command_pool_ci); +} + +auto App::create_command_block() const -> CommandBlock { + return CommandBlock{*m_device, m_queue, *m_cmd_block_pool}; +} +``` + +`create_vertex_buffer()`를 업데이트하여 사각형을 생성하도록 합니다. + +```cpp +template +[[nodiscard]] constexpr auto to_byte_array(T const& t) { + return std::bit_cast>(t); +} + +// ... +void App::create_vertex_buffer() { + // vertices of a quad. + static constexpr auto vertices_v = std::array{ + Vertex{.position = {-0.5f, -0.5f}, .color = {1.0f, 0.0f, 0.0f}}, + Vertex{.position = {0.5f, -0.5f}, .color = {0.0f, 1.0f, 0.0f}}, + Vertex{.position = {0.5f, 0.5f}, .color = {0.0f, 0.0f, 1.0f}}, + Vertex{.position = {-0.5f, 0.5f}, .color = {1.0f, 1.0f, 0.0f}}, + }; + static constexpr auto indices_v = std::array{ + 0u, 1u, 2u, 2u, 3u, 0u, + }; + static constexpr auto vertices_bytes_v = to_byte_array(vertices_v); + static constexpr auto indices_bytes_v = to_byte_array(indices_v); + static constexpr auto total_bytes_v = + std::array, 2>{ + vertices_bytes_v, + indices_bytes_v, + }; + // we want to write total_bytes_v to a Device VertexBuffer | IndexBuffer. + m_vbo = vma::create_device_buffer(m_allocator.get(), + vk::BufferUsageFlagBits::eVertexBuffer | + vk::BufferUsageFlagBits::eIndexBuffer, + create_command_block(), total_bytes_v); +} +``` + +`draw()`함수를 이에 맞춰 변경합니다. + +```cpp +void App::draw(vk::CommandBuffer const command_buffer) const { + m_shader->bind(command_buffer, m_framebuffer_size); + // single VBO at binding 0 at no offset. + command_buffer.bindVertexBuffers(0, m_vbo.get().buffer, vk::DeviceSize{}); + // u32 indices after offset of 4 vertices. + command_buffer.bindIndexBuffer(m_vbo.get().buffer, 4 * sizeof(Vertex), + vk::IndexType::eUint32); + // m_vbo has 6 indices. + command_buffer.drawIndexed(6, 1, 0, 0, 0); +} +``` + +![VBO Quad](./vbo_quad.png) diff --git a/guide/translations/ko-KR/src/memory/images.md b/guide/translations/ko-KR/src/memory/images.md new file mode 100644 index 0000000..f6b53a5 --- /dev/null +++ b/guide/translations/ko-KR/src/memory/images.md @@ -0,0 +1,184 @@ +# 이미지 + +이미지는 버퍼보다 훨씬 더 많은 속성과 생성 파라미터를 가지고 있습니다. 여기서는 두 종류로 나누겠습니다. 셰이더에서 샘플링될 이미지(텍스쳐), 그리고 렌더링에 사용할 깊이 이미지입니다. 지금은 이러한 이미지를 위한 기본 타입과 함수만 추가하겠습니다. + +```cpp +struct RawImage { + auto operator==(RawImage const& rhs) const -> bool = default; + + VmaAllocator allocator{}; + VmaAllocation allocation{}; + vk::Image image{}; + vk::Extent2D extent{}; + vk::Format format{}; + std::uint32_t levels{}; +}; + +struct ImageDeleter { + void operator()(RawImage const& raw_image) const noexcept; +}; + +using Image = Scoped; + +struct ImageCreateInfo { + VmaAllocator allocator; + std::uint32_t queue_family; +}; + +[[nodiscard]] auto create_image(ImageCreateInfo const& create_info, + vk::ImageUsageFlags usage, std::uint32_t levels, + vk::Format format, vk::Extent2D extent) + -> Image; +``` + +구현은 다음과 같습니다. + +```cpp +void ImageDeleter::operator()(RawImage const& raw_image) const noexcept { + vmaDestroyImage(raw_image.allocator, raw_image.image, raw_image.allocation); +} + +// ... +auto vma::create_image(ImageCreateInfo const& create_info, + vk::ImageUsageFlags const usage, + std::uint32_t const levels, vk::Format const format, + vk::Extent2D const extent) -> Image { + if (extent.width == 0 || extent.height == 0) { + std::println(stderr, "Images cannot have 0 width or height"); + return {}; + } + auto image_ci = vk::ImageCreateInfo{}; + image_ci.setImageType(vk::ImageType::e2D) + .setExtent({extent.width, extent.height, 1}) + .setFormat(format) + .setUsage(usage) + .setArrayLayers(1) + .setMipLevels(levels) + .setSamples(vk::SampleCountFlagBits::e1) + .setTiling(vk::ImageTiling::eOptimal) + .setInitialLayout(vk::ImageLayout::eUndefined) + .setQueueFamilyIndices(create_info.queue_family); + auto const vk_image_ci = static_cast(image_ci); + + auto allocation_ci = VmaAllocationCreateInfo{}; + allocation_ci.usage = VMA_MEMORY_USAGE_AUTO; + VkImage image{}; + VmaAllocation allocation{}; + auto const result = vmaCreateImage(create_info.allocator, &vk_image_ci, + &allocation_ci, &image, &allocation, {}); + if (result != VK_SUCCESS) { + std::println(stderr, "Failed to create VMA Image"); + return {}; + } + + return RawImage{ + .allocator = create_info.allocator, + .allocation = allocation, + .image = image, + .extent = extent, + .format = format, + .levels = levels, + }; +} +``` + +샘플링할 이미지(텍스쳐)를 생성하기 위해 이미지 바이트 데이터와 크기(extent)가 필요합니다. 이를 구조체로 감싸 사용하겠습니다. + +```cpp +struct Bitmap { + std::span bytes{}; + glm::ivec2 size{}; +}; +``` + +생성 과정은 디바이스 버퍼와 유사합니다. 스테이징 버퍼를 복사하고, 레이아웃 전환을 수행해야 합니다. 요약하면 다음과 같습니다. + +1. 이미지와 스테이징 버퍼를 생성합니다. +2. 이미지의 레이아웃을 Undefined에서 TransferDst로 전환합니다. +3. 버퍼에서 이미지로 복사하는 명령을 기록합니다. +4. 이미지의 레이아웃을 TransferDst에서 ShaderReadOnlyOptimal로 변경합니다. + +```cpp +auto vma::create_sampled_image(ImageCreateInfo const& create_info, + CommandBlock command_block, Bitmap const& bitmap) + -> Image { + // create image. + // no mip-mapping right now: 1 level. + auto const mip_levels = 1u; + auto const usize = glm::uvec2{bitmap.size}; + auto const extent = vk::Extent2D{usize.x, usize.y}; + auto const usage = + vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled; + auto ret = create_image(create_info, usage, mip_levels, + vk::Format::eR8G8B8A8Srgb, extent); + + // create staging buffer. + auto const buffer_ci = BufferCreateInfo{ + .allocator = create_info.allocator, + .usage = vk::BufferUsageFlagBits::eTransferSrc, + .queue_family = create_info.queue_family, + }; + auto const staging_buffer = create_buffer(buffer_ci, BufferMemoryType::Host, + bitmap.bytes.size_bytes()); + + // can't do anything if either creation failed. + if (!ret.get().image || !staging_buffer.get().buffer) { return {}; } + + // copy bytes into staging buffer. + std::memcpy(staging_buffer.get().mapped, bitmap.bytes.data(), + bitmap.bytes.size_bytes()); + + // transition image for transfer. + auto dependency_info = vk::DependencyInfo{}; + auto subresource_range = vk::ImageSubresourceRange{}; + subresource_range.setAspectMask(vk::ImageAspectFlagBits::eColor) + .setLayerCount(1) + .setLevelCount(mip_levels); + auto barrier = vk::ImageMemoryBarrier2{}; + barrier.setImage(ret.get().image) + .setSrcQueueFamilyIndex(create_info.queue_family) + .setDstQueueFamilyIndex(create_info.queue_family) + .setOldLayout(vk::ImageLayout::eUndefined) + .setNewLayout(vk::ImageLayout::eTransferDstOptimal) + .setSubresourceRange(subresource_range) + .setSrcStageMask(vk::PipelineStageFlagBits2::eTopOfPipe) + .setSrcAccessMask(vk::AccessFlagBits2::eNone) + .setDstStageMask(vk::PipelineStageFlagBits2::eTransfer) + .setDstAccessMask(vk::AccessFlagBits2::eMemoryRead | + vk::AccessFlagBits2::eMemoryWrite); + dependency_info.setImageMemoryBarriers(barrier); + command_block.command_buffer().pipelineBarrier2(dependency_info); + + // record buffer image copy. + auto buffer_image_copy = vk::BufferImageCopy2{}; + auto subresource_layers = vk::ImageSubresourceLayers{}; + subresource_layers.setAspectMask(vk::ImageAspectFlagBits::eColor) + .setLayerCount(1) + .setLayerCount(mip_levels); + buffer_image_copy.setImageSubresource(subresource_layers) + .setImageExtent(vk::Extent3D{extent.width, extent.height, 1}); + auto copy_info = vk::CopyBufferToImageInfo2{}; + copy_info.setDstImage(ret.get().image) + .setDstImageLayout(vk::ImageLayout::eTransferDstOptimal) + .setSrcBuffer(staging_buffer.get().buffer) + .setRegions(buffer_image_copy); + command_block.command_buffer().copyBufferToImage2(copy_info); + + // transition image for sampling. + barrier.setOldLayout(barrier.newLayout) + .setNewLayout(vk::ImageLayout::eShaderReadOnlyOptimal) + .setSrcStageMask(barrier.dstStageMask) + .setSrcAccessMask(barrier.dstAccessMask) + .setDstStageMask(vk::PipelineStageFlagBits2::eAllGraphics) + .setDstAccessMask(vk::AccessFlagBits2::eMemoryRead | + vk::AccessFlagBits2::eMemoryWrite); + dependency_info.setImageMemoryBarriers(barrier); + command_block.command_buffer().pipelineBarrier2(dependency_info); + + command_block.submit_and_wait(); + + return ret; +} +``` + +이미지를 텍스쳐로 사용하기 전에 디스크립터 셋을 구성해야 합니다. diff --git a/guide/translations/ko-KR/src/memory/vbo_quad.png b/guide/translations/ko-KR/src/memory/vbo_quad.png new file mode 100644 index 0000000..554785b Binary files /dev/null and b/guide/translations/ko-KR/src/memory/vbo_quad.png differ diff --git a/guide/translations/ko-KR/src/memory/vertex_buffer.md b/guide/translations/ko-KR/src/memory/vertex_buffer.md new file mode 100644 index 0000000..35a60c4 --- /dev/null +++ b/guide/translations/ko-KR/src/memory/vertex_buffer.md @@ -0,0 +1,104 @@ +# 정점 버퍼 + +여기서의 목표는 셰이더에 하드코딩되어 있던 정점 정보를 애플리케이션 코드로 옮기는 것입니다. 당분간은 임시로 호스트 메모리에 위치한 `vma::Buffer`를 사용하고, 정점 속성과 같은 나머지 구조에 더 집중하겠습니다. + +먼저 새로운 헤더 `vertex.hpp`를 추가합니다. + +```cpp +struct Vertex { + glm::vec2 position{}; + glm::vec3 color{1.0f}; +}; + +// two vertex attributes: position at 0, color at 1. +constexpr auto vertex_attributes_v = std::array{ + // the format matches the type and layout of data: vec2 => 2x 32-bit floats. + vk::VertexInputAttributeDescription2EXT{0, 0, vk::Format::eR32G32Sfloat, + offsetof(Vertex, position)}, + // vec3 => 3x 32-bit floats + vk::VertexInputAttributeDescription2EXT{1, 0, vk::Format::eR32G32B32Sfloat, + offsetof(Vertex, color)}, +}; + +// one vertex binding at location 0. +constexpr auto vertex_bindings_v = std::array{ + // we are using interleaved data with a stride of sizeof(Vertex). + vk::VertexInputBindingDescription2EXT{0, sizeof(Vertex), + vk::VertexInputRate::eVertex, 1}, +}; +``` + +ShaderCreateInfo에 정점 속성과 바인딩 정보를 추가합니다. + +```cpp +// ... +static constexpr auto vertex_input_v = ShaderVertexInput{ + .attributes = vertex_attributes_v, + .bindings = vertex_bindings_v, +}; +auto const shader_ci = ShaderProgram::CreateInfo{ + .device = *m_device, + .vertex_spirv = vertex_spirv, + .fragment_spirv = fragment_spirv, + .vertex_input = vertex_input_v, + .set_layouts = {}, +}; +// ... +``` + +정점 입력이 정의되었으므로 정점 셰이더를 업데이트하고 다시 컴파일합니다. + +```glsl +#version 450 core + +layout (location = 0) in vec2 a_pos; +layout (location = 1) in vec3 a_color; + +layout (location = 0) out vec3 out_color; + +void main() { + const vec2 position = a_pos; + + out_color = a_color; + gl_Position = vec4(position, 0.0, 1.0); +} +``` + +VBO(Vertex Buffer Object) 멤버를 추가하고, 해당 버퍼를 생성합니다. + +```cpp +void App::create_vertex_buffer() { + // vertices moved from the shader. + static constexpr auto vertices_v = std::array{ + Vertex{.position = {-0.5f, -0.5f}, .color = {1.0f, 0.0f, 0.0f}}, + Vertex{.position = {0.5f, -0.5f}, .color = {0.0f, 1.0f, 0.0f}}, + Vertex{.position = {0.0f, 0.5f}, .color = {0.0f, 0.0f, 1.0f}}, + }; + + // we want to write vertices_v to a Host VertexBuffer. + auto const buffer_ci = vma::BufferCreateInfo{ + .allocator = m_allocator.get(), + .usage = vk::BufferUsageFlagBits::eVertexBuffer, + .queue_family = m_gpu.queue_family, + }; + m_vbo = vma::create_buffer(buffer_ci, vma::BufferMemoryType::Host, + sizeof(vertices_v)); + + // host buffers have a memory-mapped pointer available to memcpy data to. + std::memcpy(m_vbo.get().mapped, vertices_v.data(), sizeof(vertices_v)); +} +``` + +드로우 콜을 기록하기 전에 VBO를 바인딩합니다. + +```cpp +// single VBO at binding 0 at no offset. +command_buffer.bindVertexBuffers(0, m_vbo->get_raw().buffer, + vk::DeviceSize{}); +// m_vbo has 3 vertices. +command_buffer.draw(3, 1, 0, 0); +``` + +아마 이전과 동일한 삼각형을 볼 수 있을 것입니다. 하지만 이제는 원하는 정점 데이터를 자유롭게 사용할 수 있습니다. 프리미티브 토폴로지는 기본적으로 Triangle List로 설정하며, 정점 배열에서 매 3개의 정점이 삼각형으로 그려질 것입니다. 예를 들어 정점 9개가 `[[0, 1, 2], [3, 4, 5], [6, 7, 8]]` 있다면, 각 3개의 정점이 하나의 삼각형을 형성하게 됩니다. 정점 데이터와 토폴로지를 다양하게 바꿔보며 실험해 보세요. 예상치 못한 출력이나 버그가 발생한다면 RenderDoc을 사용해 디버깅할 수 있습니다. + +호스트 정점 버퍼는 UI 객체처럼 임시로 쓰이거나 자주 변경되는 프리미티브에 유용합니다. 2D 프레임워크에서는 이러한 VBO를 독점적으로 사용하는 것도 가능합니다. 예를 들어, 가상 프레임마다 별도의 버퍼 풀을 두고, 각 드로우마다 현재 프레임의 풀에서 버퍼를 하나 가져와 정점을 복사하는 방식이 단순하면서도 효과적입니다. diff --git a/guide/translations/ko-KR/src/memory/vma.md b/guide/translations/ko-KR/src/memory/vma.md new file mode 100644 index 0000000..ef70520 --- /dev/null +++ b/guide/translations/ko-KR/src/memory/vma.md @@ -0,0 +1,66 @@ +# Vulkan Memory Allocator + +VMA는 CMake를 완벽히 지원하지만, 단일 번역 단위에서 정의(instantiate)되어야 하는 단일 헤더 라이브러리이기도 합니다. 이를 관리하기 위해 고유한 `vma::vma` 타겟을 직접 생성하여 소스 파일을 컴파일하겠습니다. + +```cpp +// vk_mem_alloc.cpp +#define VMA_IMPLEMENTATION + +#include +``` + +VulkanHPP와는 달리 VMA는 C만을 지원합니다. 따라서 RAII 방식으로 관리하기 위해 `Scope`클래스 템플릿을 사용할 것입니다. 가장 먼저 필요한 것은 `VmaAllocator`으로, 이는 `vk::Device` 혹은 `GLFWwindow*`와 유사한 역할을 합니다. + +```cpp +// vma.hpp +namespace lvk::vma { +struct Deleter { + void operator()(VmaAllocator allocator) const noexcept; +}; + +using Allocator = Scoped; + +[[nodiscard]] auto create_allocator(vk::Instance instance, + vk::PhysicalDevice physical_device, + vk::Device device) -> Allocator; +} // namespace lvk::vma + +// vma.cpp +void Deleter::operator()(VmaAllocator allocator) const noexcept { + vmaDestroyAllocator(allocator); +} + +// ... +auto vma::create_allocator(vk::Instance const instance, + vk::PhysicalDevice const physical_device, + vk::Device const device) -> Allocator { + auto const& dispatcher = VULKAN_HPP_DEFAULT_DISPATCHER; + // need to zero initialize C structs, unlike VulkanHPP. + auto vma_vk_funcs = VmaVulkanFunctions{}; + vma_vk_funcs.vkGetInstanceProcAddr = dispatcher.vkGetInstanceProcAddr; + vma_vk_funcs.vkGetDeviceProcAddr = dispatcher.vkGetDeviceProcAddr; + + auto allocator_ci = VmaAllocatorCreateInfo{}; + allocator_ci.physicalDevice = physical_device; + allocator_ci.device = device; + allocator_ci.pVulkanFunctions = &vma_vk_funcs; + allocator_ci.instance = instance; + VmaAllocator ret{}; + auto const result = vmaCreateAllocator(&allocator_ci, &ret); + if (result == VK_SUCCESS) { return ret; } + + throw std::runtime_error{"Failed to create Vulkan Memory Allocator"}; +} +``` + +`App`은 `vma::Allocator` 객체를 생성하고 이를 보관합니다. + +```cpp +// ... +vma::Allocator m_allocator{}; // anywhere between m_device and m_shader. + +// ... +void App::create_allocator() { + m_allocator = vma::create_allocator(*m_instance, m_gpu.device, *m_device); +} +``` diff --git a/guide/translations/ko-KR/src/rendering/README.md b/guide/translations/ko-KR/src/rendering/README.md new file mode 100644 index 0000000..326e43c --- /dev/null +++ b/guide/translations/ko-KR/src/rendering/README.md @@ -0,0 +1,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로 부상했습니다(이전에는 확장일 뿐이었습니다). diff --git a/guide/translations/ko-KR/src/rendering/dynamic_rendering.md b/guide/translations/ko-KR/src/rendering/dynamic_rendering.md new file mode 100644 index 0000000..e7e5be3 --- /dev/null +++ b/guide/translations/ko-KR/src/rendering/dynamic_rendering.md @@ -0,0 +1,212 @@ +# 동적 렌더링 + +동적 렌더링을 활성화하면 비교적 복잡한 렌더 패스를 사용하지 않아도 됩니다. 다만 렌더 패스는 타일 기반 GPU에서 조금 더 이점을 갖습니다. 여기서는 스왑체인, 렌더 싱크, 그리고 렌더링 자체를 하나로 묶는 작업을 진행하겠습니다. 아직 실제로 화면에 무언가를 렌더링할 준비가 된 것은 아니지만, 특정 색상으로 이미지를 초기화할 수는 있습니다. + +아래와 같은 멤버를 `App`에 추가하겠습니다. + +```cpp +auto acquire_render_target() -> bool; +auto begin_frame() -> vk::CommandBuffer; +void transition_for_render(vk::CommandBuffer command_buffer) const; +void render(vk::CommandBuffer command_buffer); +void transition_for_present(vk::CommandBuffer command_buffer) const; +void submit_and_present(); + +// ... +glm::ivec2 m_framebuffer_size{}; +std::optional m_render_target{}; +``` + +이제 메인 루프는 이 멤버들을 활용하여 스왑체인과 렌더링 루프를 구현할 수 있습니다. + +```cpp +while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) { + glfwPollEvents(); + if (!acquire_render_target()) { continue; } + auto const command_buffer = begin_frame(); + transition_for_render(command_buffer); + render(command_buffer); + transition_for_present(command_buffer); + submit_and_present(); +} +``` + +스왑체인 이미지를 받아오기 전에, 현재 프레임의 펜스를 대기해야 합니다. 이미지를 받아오는 것이 성공하면 해당 펜스를 리셋(unsignal)합니다. + +```cpp +auto App::acquire_render_target() -> bool { + m_framebuffer_size = glfw::framebuffer_size(m_window.get()); + // minimized? skip loop. + if (m_framebuffer_size.x <= 0 || m_framebuffer_size.y <= 0) { + return false; + } + + auto& render_sync = m_render_sync.at(m_frame_index); + + // wait for the fence to be signaled. + static constexpr auto fence_timeout_v = + static_cast(std::chrono::nanoseconds{3s}.count()); + auto result = + m_device->waitForFences(*render_sync.drawn, vk::True, fence_timeout_v); + if (result != vk::Result::eSuccess) { + throw std::runtime_error{"Failed to wait for Render Fence"}; + } + + m_render_target = m_swapchain->acquire_next_image(*render_sync.draw); + if (!m_render_target) { + // acquire failure => ErrorOutOfDate. Recreate Swapchain. + m_swapchain->recreate(m_framebuffer_size); + return false; + } + + // reset fence _after_ acquisition of image: if it fails, the + // fence remains signaled. + m_device->resetFences(*render_sync.drawn); + + return true; +} +``` + +펜스가 리셋되었기 때문에, 다음 동작을 진행하기 전에 해당 펜스를 signal하도록 반드시 큐에 커맨드 버퍼가 제출되어야 합니다. 그렇지 않으면 다음 루프에서 펜스를 기다리는 과정에서 교착 상태에 빠지고, 결국 3초 후 예외가 발생하게 됩니다. 이제 커맨드 버퍼 기록을 시작하겠습니다. + +```cpp +auto App::begin_frame() -> vk::CommandBuffer { + auto const& render_sync = m_render_sync.at(m_frame_index); + + auto command_buffer_bi = vk::CommandBufferBeginInfo{}; + // this flag means recorded commands will not be reused. + command_buffer_bi.setFlags(vk::CommandBufferUsageFlagBits::eOneTimeSubmit); + render_sync.command_buffer.begin(command_buffer_bi); + return render_sync.command_buffer; +} +``` + +렌더링에 사용할 이미지의 레이아웃을 AttachmentOptimal 레이아웃으로 전환합니다. 이를 위해 이미지 배리어를 설정하고 커맨드 버퍼에 기록합니다. + +```cpp +void App::transition_for_render(vk::CommandBuffer const command_buffer) const { + auto dependency_info = vk::DependencyInfo{}; + auto barrier = m_swapchain->base_barrier(); + // Undefined => AttachmentOptimal + // the barrier must wait for prior color attachment operations to complete, + // and block subsequent ones. + barrier.setOldLayout(vk::ImageLayout::eUndefined) + .setNewLayout(vk::ImageLayout::eAttachmentOptimal) + .setSrcAccessMask(vk::AccessFlagBits2::eColorAttachmentRead | + vk::AccessFlagBits2::eColorAttachmentWrite) + .setSrcStageMask(vk::PipelineStageFlagBits2::eColorAttachmentOutput) + .setDstAccessMask(barrier.srcAccessMask) + .setDstStageMask(barrier.srcStageMask); + dependency_info.setImageMemoryBarriers(barrier); + command_buffer.pipelineBarrier2(dependency_info); +} +``` + +받아온 이미지를 색상 타겟으로 사용하는 RenderingAttachmentInfo를 생성합니다. 빨간 색을 초기화 색상으로 사용하고, LoadOp가 이미지를 초기화하며, StoreOp는 결과를 담도록 해야 합니다(현재는 초기화된 이미지만 저장합니다). 이 colorAttachment와 전체 이미지를 렌더링 영역으로 설정한 RenderingInfo 구조체를 생성합니다. 마지막으로 렌더링을 실행합니다. + +```cpp +void App::render(vk::CommandBuffer const command_buffer) { + auto color_attachment = vk::RenderingAttachmentInfo{}; + color_attachment.setImageView(m_render_target->image_view) + .setImageLayout(vk::ImageLayout::eAttachmentOptimal) + .setLoadOp(vk::AttachmentLoadOp::eClear) + .setStoreOp(vk::AttachmentStoreOp::eStore) + // temporarily red. + .setClearValue(vk::ClearColorValue{1.0f, 0.0f, 0.0f, 1.0f}); + auto rendering_info = vk::RenderingInfo{}; + auto const render_area = + vk::Rect2D{vk::Offset2D{}, m_render_target->extent}; + rendering_info.setRenderArea(render_area) + .setColorAttachments(color_attachment) + .setLayerCount(1); + + command_buffer.beginRendering(rendering_info); + // draw stuff here. + command_buffer.endRendering(); +} +``` + +렌더링이 끝나면 이미지를 표시하기 위해 레이아웃을 전환합니다. + +```cpp +void App::transition_for_present(vk::CommandBuffer const command_buffer) const { + auto dependency_info = vk::DependencyInfo{}; + auto barrier = m_swapchain->base_barrier(); + // AttachmentOptimal => PresentSrc + // the barrier must wait for prior color attachment operations to complete, + // and block subsequent ones. + barrier.setOldLayout(vk::ImageLayout::eAttachmentOptimal) + .setNewLayout(vk::ImageLayout::ePresentSrcKHR) + .setSrcAccessMask(vk::AccessFlagBits2::eColorAttachmentRead | + vk::AccessFlagBits2::eColorAttachmentWrite) + .setSrcStageMask(vk::PipelineStageFlagBits2::eColorAttachmentOutput) + .setDstAccessMask(barrier.srcAccessMask) + .setDstStageMask(barrier.srcStageMask); + dependency_info.setImageMemoryBarriers(barrier); + command_buffer.pipelineBarrier2(dependency_info); +} +``` + +커맨드 버퍼를 끝내고(End) 이를 제출합니다. 스왑체인 이미지를 사용할 준비가 되면 `draw`세마포어가 시그널 되고, 이로 인해 커맨드 버퍼가 실행됩니다. 렌더링이 완료되면 `present` 세마포어와 `drawn` 펜스가 시그널됩니다. 이후 동일한 가상 프레임이 다시 처리될 때 이 펜스를 기다리게 됩니다. 마지막으로 프레임 인덱스를 증가시키고, 다음 표시 작업이 대기할 수 있도록 `present` 세마포어를 전달합니다. + +```cpp +void App::submit_and_present() { + auto const& render_sync = m_render_sync.at(m_frame_index); + render_sync.command_buffer.end(); + + auto submit_info = vk::SubmitInfo2{}; + auto const command_buffer_info = + vk::CommandBufferSubmitInfo{render_sync.command_buffer}; + auto wait_semaphore_info = vk::SemaphoreSubmitInfo{}; + wait_semaphore_info.setSemaphore(*render_sync.draw) + .setStageMask(vk::PipelineStageFlagBits2::eColorAttachmentOutput); + auto signal_semaphore_info = vk::SemaphoreSubmitInfo{}; + signal_semaphore_info.setSemaphore(*render_sync.present) + .setStageMask(vk::PipelineStageFlagBits2::eColorAttachmentOutput); + submit_info.setCommandBufferInfos(command_buffer_info) + .setWaitSemaphoreInfos(wait_semaphore_info) + .setSignalSemaphoreInfos(signal_semaphore_info); + m_queue.submit2(submit_info, *render_sync.drawn); + + m_frame_index = (m_frame_index + 1) % m_render_sync.size(); + m_render_target.reset(); + + // an eErrorOutOfDateKHR result is not guaranteed if the + // framebuffer size does not match the Swapchain image size, check it + // explicitly. + auto const fb_size_changed = m_framebuffer_size != m_swapchain->get_size(); + auto const out_of_date = + !m_swapchain->present(m_queue, *render_sync.present); + if (fb_size_changed || out_of_date) { + m_swapchain->recreate(m_framebuffer_size); + } +} +``` + +> Wayland 사용자라면 이제 마침내 창과 상호작용할 수 있습니다! + +![Cleared Image](./dynamic_rendering_red_clear.png) + +## Wayland에서의 RenderDoc + +이 글을 작성하는 시점에서는 RenderDoc이 Wayland 애플리케이션을 지원하지 않습니다. `glfwInit()` 이전에 `glfwInitHint()`를 호출하여 임시로 X11(XWayland)를 사용하도록 강제할 수 있습니다. + +```cpp +glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11); +``` + +커맨드라인 옵션을 활용해 이 설정을 조건적으로 적용하는 것이 간단하고 유연한 방식입니다. RenderDoc에서 해당 인자를 설정하거나, X11 백엔드를 사용하고자 할 때마다 넘겨주는 식으로 처리하면 됩니다. + +```cpp +// main.cpp +// skip the first argument. +auto args = std::span{argv, static_cast(argc)}.subspan(1); +while (!args.empty()) { + auto const arg = std::string_view{args.front()}; + if (arg == "-x" || arg == "--force-x11") { + glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11); + } + args = args.subspan(1); +} +lvk::App{}.run(); +``` diff --git a/guide/translations/ko-KR/src/rendering/dynamic_rendering_red_clear.png b/guide/translations/ko-KR/src/rendering/dynamic_rendering_red_clear.png new file mode 100644 index 0000000..b041e29 Binary files /dev/null and b/guide/translations/ko-KR/src/rendering/dynamic_rendering_red_clear.png differ diff --git a/guide/translations/ko-KR/src/rendering/render_sync.md b/guide/translations/ko-KR/src/rendering/render_sync.md new file mode 100644 index 0000000..46520ef --- /dev/null +++ b/guide/translations/ko-KR/src/rendering/render_sync.md @@ -0,0 +1,75 @@ +# 렌더 싱크 + +새로운 헤더 `resource_buffering.hpp`를 생성합니다. + +```cpp +// Number of virtual frames. +inline constexpr std::size_t buffering_v{2}; + +// Alias for N-buffered resources. +template +using Buffered = std::array; +``` + +`App`에 private 멤버 `struct RenderSync`를 추가합니다. + +```cpp +struct RenderSync { + // signaled when Swapchain image has been acquired. + vk::UniqueSemaphore draw{}; + // signaled when image is ready to be presented. + vk::UniqueSemaphore present{}; + // signaled with present Semaphore, waited on before next render. + vk::UniqueFence drawn{}; + // used to record rendering commands. + vk::CommandBuffer command_buffer{}; +}; +``` + +스왑체인 루프와 관련있는 새로운 멤버를 추가합니다. + +```cpp +// command pool for all render Command Buffers. +vk::UniqueCommandPool m_render_cmd_pool{}; +// Sync and Command Buffer for virtual frames. +Buffered m_render_sync{}; +// Current virtual frame index. +std::size_t m_frame_index{}; +``` + +생성 함수를 추가하고, 구현한 다음 호출합니다. + +```cpp +void App::create_render_sync() { + // Command Buffers are 'allocated' from a Command Pool (which is 'created' + // like all other Vulkan objects so far). We can allocate all the buffers + // from a single pool here. + auto command_pool_ci = vk::CommandPoolCreateInfo{}; + // this flag enables resetting the command buffer for re-recording (unlike a + // single-time submit scenario). + command_pool_ci.setFlags(vk::CommandPoolCreateFlagBits::eResetCommandBuffer) + .setQueueFamilyIndex(m_gpu.queue_family); + m_render_cmd_pool = m_device->createCommandPoolUnique(command_pool_ci); + + auto command_buffer_ai = vk::CommandBufferAllocateInfo{}; + command_buffer_ai.setCommandPool(*m_render_cmd_pool) + .setCommandBufferCount(static_cast(resource_buffering_v)) + .setLevel(vk::CommandBufferLevel::ePrimary); + auto const command_buffers = + m_device->allocateCommandBuffers(command_buffer_ai); + assert(command_buffers.size() == m_render_sync.size()); + + // we create Render Fences as pre-signaled so that on the first render for + // each virtual frame we don't wait on their fences (since there's nothing + // to wait for yet). + static constexpr auto fence_create_info_v = + vk::FenceCreateInfo{vk::FenceCreateFlagBits::eSignaled}; + for (auto [sync, command_buffer] : + std::views::zip(m_render_sync, command_buffers)) { + sync.command_buffer = command_buffer; + sync.draw = m_device->createSemaphoreUnique({}); + sync.present = m_device->createSemaphoreUnique({}); + sync.drawn = m_device->createFenceUnique(fence_create_info_v); + } +} +``` diff --git a/guide/translations/ko-KR/src/rendering/swapchain_loop.md b/guide/translations/ko-KR/src/rendering/swapchain_loop.md new file mode 100644 index 0000000..c506322 --- /dev/null +++ b/guide/translations/ko-KR/src/rendering/swapchain_loop.md @@ -0,0 +1,30 @@ +# 스왑체인 루프 + +렌더링 루프의 핵심 요소 중 하나는 스왑체인 루프입니다. 이는 다음과 같은 고수준 단계로 구성됩니다. + +1. 스왑체인으로부터 이미지를 받아옵니다. +2. 받아온 이미지에 렌더링합니다. +3. 렌더링이 끝난 이미지를 표시합니다(이미지를 다시 스왑체인으로 돌려줍니다). + +![WSI Engine](./wsi_engine.png) + +여기서 몇 가지 고려해야할 점이 있습니다. + +1. 이미지를 받아오거나 표시하는 과정은 실패할 수 있습니다(스왑체인을 사용할 수 없는 경우). 이 때 남은 단계들은 생략해야 합니다. +2. 받아오는 명령은 이미지가 실제로 사용할 준비가 되기 전에 반환될 수 있으며, 렌더링은 해당 이미지를 받아온 이후에 시작하도록 동기화되어야 합니다. +3. 마찬가지로, 표시하는 작업 또한 렌더링이 끝난 이후에 수행되도록 동기화해야 합니다. +4. 이미지들은 각 단계에 맞는 적절한 레이아웃으로 전환되어야 합니다. + +또한, 스왑체인의 이미지의 수는 시스템에 따라 달라질 수 있지만, 엔진은 일반적으로 고정된 개수의 가상 프레임을 사용합니다. 더블 버퍼링에는 2개의 가상 프레임, 트리플 버퍼링에는 3개(보통은 3개로 충분합니다). 자세한 내용은 [여기](https://docs.vulkan.org/samples/latest/samples/performance/swapchain_images/README.html#_double_buffering_or_triple_buffering)서 확인할 수 있습니다. 또한 스왑체인이 (Vsync라 알려진)Mailbox Present 모드를 사용중이라면 메인 루프 중에 이전 렌더링 명령이 끝나기 전 동일한 이미지를 가져오는 것도 가능합니다. + +## 가상 프레임 + +프레임마다 사용되는 모든 동적 자원들은 가상 프레임에 포함됩니다. 애플리케이션은 고정된 개수의 가상 프레임을 가지고 있으며, 매 렌더 패스마다 이를 순환하며 사용합니다. 동기화를 위해 각 프레임은 이전 프레임의 렌더링이 끝날 때 까지 대기하게 만드는 [`vk::Fence`](https://docs.vulkan.org/spec/latest/chapters/synchronization.html#synchronization-fences)가 있어야 합니다. 또한 GPU에서의 이미지를 받아오고, 렌더링, 화면에 나타내는 작업을 동기화하기 위한 2개의[`vk::Semaphore`](https://docs.vulkan.org/spec/latest/chapters/synchronization.html#synchronization-semaphores)가 필요합니다(이 작업들은 CPU측에서 대기할 필요는 없습니다). 명령을 기록하기 위해 가상 프레임마다 [`vk::CommandBuffer`](https://docs.vulkan.org/spec/latest/chapters/cmdbuffers.html)를 두어 해당 프레임의 (레이아웃 전환을 포함한) 모든 렌더링 명령을 기록할 것입니다. + +## 이미지 레이아웃 + + +Vulkan 이미지에는 [이미지 레이아웃](https://docs.vulkan.org/spec/latest/chapters/resources.html#resources-image-layouts)이라 알려진 속성이 있습니다. 대부분의 이미지 작업과 이미지의 서브리소스는 특정 레이아웃에서만 수행될 수 있으므로, 작업 전후에 레이아웃 전환이 필요합니다. 레이아웃 전환은 파이프라인 배리어(GPU의 메모리 배리어를 생각하세요)역할도 수행하며, 전환 전후의 작업을 동기화할수 있게 합니다. + +Vulkan 동기화는 아마도 API의 가장 복잡한 부분 중 하나일 것입니다. 충분한 학습이 권장되며, [이 글](https://gpuopen.com/learn/vulkan-barriers-explained/)에서 배리어에 대해 자세히 설명하고 있습니다. + diff --git a/guide/translations/ko-KR/src/rendering/swapchain_update.md b/guide/translations/ko-KR/src/rendering/swapchain_update.md new file mode 100644 index 0000000..8455293 --- /dev/null +++ b/guide/translations/ko-KR/src/rendering/swapchain_update.md @@ -0,0 +1,98 @@ +# 스왑체인 업데이트 + +스왑체인에서 이미지를 받아오고 표시하는 작업은 다양한 결과를 반환할 수 있습니다. 우리는 다음과 같은 경우에 한정하여 처리합니다. + +- `eSuccess` : 문제가 없습니다. +- `eSuboptimalKHR` : 역시 문제가 없습니다(에러는 아니며, 데스크탑 환경에서는 드물게 발생합니다). +- `eErrorOutOfDateKHR` : 스왑체인을 재생성해야 합니다. +- 그 외의 모든 `vk::Result` : 치명적이거나 예기치 않은 오류입니다. + +`swapchain.cpp`에 함수를 생성합시다. + +```cpp +auto needs_recreation(vk::Result const result) -> bool { + switch (result) { + case vk::Result::eSuccess: + case vk::Result::eSuboptimalKHR: return false; + case vk::Result::eErrorOutOfDateKHR: return true; + default: break; + } + throw std::runtime_error{"Swapchain Error"}; +} +``` + +스왑체인으로부터 이미지를 성공적으로 받아오면 이미지와 이미지 뷰, 그리고 크기를 반환해야 합니다. 이를 `struct`로 감싸겠습니다. + +```cpp +struct RenderTarget { + vk::Image image{}; + vk::ImageView image_view{}; + vk::Extent2D extent{}; +}; +``` + +VulkanHPP의 기본 API는 `vk::Result`가 오류에 해당되면 (사양에 따라) 예외를 던집니다. `eErrorOutOfDateKHR`은 기술적으로는 오류이지만, 프레임버퍼와 스왑체인의 크기가 일치하지 않을 때 일어날 수 있습니다. 이러한 예외 처리를 피하기 위해, 우리는 포인터 인자 또는 출력 인자를 사용하는 오버로드 버전 API를 사용하여 `vk::Result`를 직접 반환받는 방식으로 대체하겠습니다. + +이미지를 가져오는 함수를 작성하겠습니다. + +```cpp +auto Swapchain::acquire_next_image(vk::Semaphore const to_signal) + -> std::optional { + assert(!m_image_index); + static constexpr auto timeout_v = std::numeric_limits::max(); + // avoid VulkanHPP ErrorOutOfDateKHR exceptions by using alternate API that + // returns a Result. + auto image_index = std::uint32_t{}; + auto const result = m_device.acquireNextImageKHR( + *m_swapchain, timeout_v, to_signal, {}, &image_index); + if (needs_recreation(result)) { return {}; } + + m_image_index = static_cast(image_index); + return RenderTarget{ + .image = m_images.at(*m_image_index), + .image_view = *m_image_views.at(*m_image_index), + .extent = m_ci.imageExtent, + }; +} +``` + +표시하는 함수도 마찬가지입니다. + +```cpp +auto Swapchain::present(vk::Queue const queue, vk::Semaphore const to_wait) + -> bool { + auto const image_index = static_cast(m_image_index.value()); + auto present_info = vk::PresentInfoKHR{}; + present_info.setSwapchains(*m_swapchain) + .setImageIndices(image_index) + .setWaitSemaphores(to_wait); + // avoid VulkanHPP ErrorOutOfDateKHR exceptions by using alternate API. + auto const result = queue.presentKHR(&present_info); + m_image_index.reset(); + return !needs_recreation(result); +} +``` + +각 작업에서 `std::nullopt` 혹은 `false`가 반환될 경우, 스왑체인을 재생성하는 것은 사용자(`class App`)의 책임입니다. 사용자는 또한 받아오는 것과 표시하는 작업 사이에 이미지의 레이아웃을 전환해야 합니다. 이 과정을 돕는 함수를 추가하고 공통 상수로 사용할 수 있도록 ImageSubresourceRange 분리해 정의합니다. + +```cpp +constexpr auto subresource_range_v = [] { + auto ret = vk::ImageSubresourceRange{}; + // this is a color image with 1 layer and 1 mip-level (the default). + ret.setAspectMask(vk::ImageAspectFlagBits::eColor) + .setLayerCount(1) + .setLevelCount(1); + return ret; +}(); + +// ... +auto Swapchain::base_barrier() const -> vk::ImageMemoryBarrier2 { + // fill up the parts common to all barriers. + auto ret = vk::ImageMemoryBarrier2{}; + ret.setImage(m_images.at(m_image_index.value())) + .setSubresourceRange(subresource_range_v) + .setSrcQueueFamilyIndex(m_gpu.queue_family) + .setDstQueueFamilyIndex(m_gpu.queue_family); + return ret; +} +``` diff --git a/guide/translations/ko-KR/src/rendering/wsi_engine.png b/guide/translations/ko-KR/src/rendering/wsi_engine.png new file mode 100644 index 0000000..d5d3756 Binary files /dev/null and b/guide/translations/ko-KR/src/rendering/wsi_engine.png differ diff --git a/guide/translations/ko-KR/src/shader_objects/README.md b/guide/translations/ko-KR/src/shader_objects/README.md new file mode 100644 index 0000000..019c250 --- /dev/null +++ b/guide/translations/ko-KR/src/shader_objects/README.md @@ -0,0 +1,5 @@ +# 셰이더 오브젝트 + +[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)를 참고하세요. + +Vulkan에서는 셰이더 코드를 SPIR-V 형태로 제공해야 합니다. 우리는 Vulkan SDK에 포함된 `glslc`를 사용해 GLSL 코드를 필요할 때 수동으로 SPIR-V 파일로 컴파일하겠습니다. diff --git a/guide/translations/ko-KR/src/shader_objects/drawing_triangle.md b/guide/translations/ko-KR/src/shader_objects/drawing_triangle.md new file mode 100644 index 0000000..bee5e66 --- /dev/null +++ b/guide/translations/ko-KR/src/shader_objects/drawing_triangle.md @@ -0,0 +1,130 @@ +# 삼각형 그리기 + +`App` 클래스에 `ShaderProgram`과 이를 생성하는 함수를 추가합니다. + +```cpp +[[nodiscard]] auto asset_path(std::string_view uri) const -> fs::path; + +// ... +void create_shader(); + +// ... +std::optional m_shader{}; +``` + +`asset_path()`와 `create_shader()`를 구현하고 호출합니다. + +```cpp +void App::create_shader() { + auto const vertex_spirv = to_spir_v(asset_path("shader.vert")); + auto const fragment_spirv = to_spir_v(asset_path("shader.frag")); + auto const shader_ci = ShaderProgram::CreateInfo{ + .device = *m_device, + .vertex_spirv = vertex_spirv, + .fragment_spirv = fragment_spirv, + .vertex_input = {}, + .set_layouts = {}, + }; + m_shader.emplace(shader_ci); +} + +auto App::asset_path(std::string_view const uri) const -> fs::path { + return m_assets_dir / uri; +} +``` + +`render()`가 걷잡을 수 없이 커지기 전에, 고수준 로직을 두 멤버 함수로 분리합니다. + +```cpp +// ImGui code goes here. +void inspect(); +// Issue draw calls here. +void draw(vk::CommandBuffer command_buffer) const; + +// ... +void App::inspect() { + ImGui::ShowDemoWindow(); + // TODO +} + +// ... +command_buffer.beginRendering(rendering_info); +inspect(); +draw(command_buffer); +command_buffer.endRendering(); +``` + +이제 셰이더를 바인딩하고 이를 삼각형을 그리는 데 사용할 수 있습니다. `draw()`함수를 `const`로 만들어 `App`을 건드리지 않도록 합니다. + +```cpp +void App::draw(vk::CommandBuffer const command_buffer) const { + m_shader->bind(command_buffer, m_framebuffer_size); + // current shader has hard-coded logic for 3 vertices. + command_buffer.draw(3, 1, 0, 0); +} +``` + +![White Triangle](./white_triangle.png) + +셰이더를 각 정점에 대해 보간된 RGB를 사용하도록 업데이트합니다. + +```glsl +// shader.vert + +layout (location = 0) out vec3 out_color; + +// ... +const vec3 colors[] = { + vec3(1.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(0.0, 0.0, 1.0), +}; + +// ... +out_color = colors[gl_VertexIndex]; + +// shader.frag + +layout (location = 0) in vec3 in_color; + +// ... +out_color = vec4(in_color, 1.0); +``` + +> `assets/`에 있는 두 SPIR-V 파일을 다시 컴파일하는 것을 잊지 마세요. + +그리고 초기화 색상을 검은 색으로 설정합니다. + +```cpp +// ... +.setClearValue(vk::ClearColorValue{0.0f, 0.0f, 0.0f, 1.0f}); +``` + +이제 Vulkan에서 sRGB 포맷으로 표현되는 삼각형을 볼 수 있습니다. + +![sRGB Triangle](./srgb_triangle.png) + +## 동적 상태 변경하기 + +ImGui 창을 사용해 파이프라인 상태를 관찰하거나 일부 설정을 변경할 수 있습니다. + +```cpp +ImGui::SetNextWindowSize({200.0f, 100.0f}, ImGuiCond_Once); +if (ImGui::Begin("Inspect")) { + if (ImGui::Checkbox("wireframe", &m_wireframe)) { + m_shader->polygon_mode = + m_wireframe ? vk::PolygonMode::eLine : vk::PolygonMode::eFill; + } + if (m_wireframe) { + auto const& line_width_range = + m_gpu.properties.limits.lineWidthRange; + ImGui::SetNextItemWidth(100.0f); + ImGui::DragFloat("line width", &m_shader->line_width, 0.25f, + line_width_range[0], line_width_range[1]); + } +} +ImGui::End(); +``` + +![sRGB Triangle (wireframe)](./srgb_triangle_wireframe.png) + diff --git a/guide/translations/ko-KR/src/shader_objects/glsl_to_spir_v.md b/guide/translations/ko-KR/src/shader_objects/glsl_to_spir_v.md new file mode 100644 index 0000000..9e45ebc --- /dev/null +++ b/guide/translations/ko-KR/src/shader_objects/glsl_to_spir_v.md @@ -0,0 +1,71 @@ +# GLSL 에서 SPIR-V + +셰이더는 NDC 공간 X축과 Y축에서 -1에서 1까지 작동합니다. 새로운 정점 셰이더의 삼각형 좌표계를 출력하고 이를 `src/glsl/shader.vert`에 저장합니다. + +```glsl +#version 450 core + +void main() { + const vec2 positions[] = { + vec2(-0.5, -0.5), + vec2(0.5, -0.5), + vec2(0.0, 0.5), + }; + + const vec2 position = positions[gl_VertexIndex]; + + gl_Position = vec4(position, 0.0, 1.0); +} +``` + +`src/glsl/shader.frag`의 프래그먼트 셰이더는 지금은 흰 색을 출력하기만 할 것입니다. + +```glsl +#version 450 core + +layout (location = 0) out vec4 out_color; + +void main() { + out_color = vec4(1.0); +} +``` + +이 둘을 `assets/`로 컴파일합니다. + +``` +glslc src/glsl/shader.vert -o assets/shader.vert +glslc src/glsl/shader.frag -o assets/shader.frag +``` + +> glslc는 Vulkan SDK의 일부입니다. + +## SPIR-V 불러오기 + +SPIR-V 셰이더는 4바이트 단위로 정렬이 되어있는 바이너리 파일입니다. 지금까지 봐왔던 대로, Vulkan API는 `std::uint32_t`의 묶음을 받습니다. 따라서 이러한 종류의 버퍼(단, `std::vector` 혹은 다른 종류의 1바이트 컨테이너는 아닙니다)에 담습니다. 이를 돕는 함수를 `app.cpp`에 추가합니다. + +```cpp +[[nodiscard]] auto to_spir_v(fs::path const& path) + -> std::vector { + // open the file at the end, to get the total size. + auto file = std::ifstream{path, std::ios::binary | std::ios::ate}; + if (!file.is_open()) { + throw std::runtime_error{ + std::format("Failed to open file: '{}'", path.generic_string())}; + } + + auto const size = file.tellg(); + auto const usize = static_cast(size); + // file data must be uint32 aligned. + if (usize % sizeof(std::uint32_t) != 0) { + throw std::runtime_error{std::format("Invalid SPIR-V size: {}", usize)}; + } + + // seek to the beginning before reading. + file.seekg({}, std::ios::beg); + auto ret = std::vector{}; + ret.resize(usize / sizeof(std::uint32_t)); + void* data = ret.data(); + file.read(static_cast(data), size); + return ret; +} +``` diff --git a/guide/translations/ko-KR/src/shader_objects/locating_assets.md b/guide/translations/ko-KR/src/shader_objects/locating_assets.md new file mode 100644 index 0000000..1753bb9 --- /dev/null +++ b/guide/translations/ko-KR/src/shader_objects/locating_assets.md @@ -0,0 +1,49 @@ +# 에셋 위치 + +셰이더를 사용하기 전에, 에셋 파일들을 불러와야 합니다. 이를 제대로 수행하려면 우선 에셋들이 위치한 경로를 알아야 합니다. 에셋 경로를 찾는 방법에는 여러 가지가 있지만, 우리는 현재 작업 디렉토리에서 시작하여 상위 디렉토리로 올라가며 특정 하위 폴더(`assets/`)를 찾는 방식을 사용할 것입니다. 이렇게 하면 프로젝트나 빌드 디렉토리 어디에서 `app`이 실행되더라도 `assets/` 디렉토리를 자동으로 찾아 접근할 수 있게 됩니다. + +``` +. +|-- assets/ +|-- app +|-- build/ + |-- app +|-- out/ + |-- default/Release/ + |-- app + |-- ubsan/Debug/ + |-- app +``` + +릴리즈 패키지에서는 일반적으로 실행 파일의 경로를 기준으로 에셋 경로를 설정하며, 상위 경로로 거슬러 올라가는 방식은 사용하지 않는 것이 보통입니다. 작업 경로에 상관없이 패키지에 포함된 에셋은 보통 실행 파일과 같은 위치나 그 주변에 위치하기 때문입니다. + +## 에셋 경로 +: +`App`에 `assets/` 경로를 담을 멤버를 추가합니다. + +```cpp +namespace fs = std::filesystem; + +// ... +fs::path m_assets_dir{}; +``` + +에셋 경로를 찾는 함수를 추가하고, 그 반환값을 `run()` 함수 상단에서 `m_assets_dir`에 저장하세요. + +```cpp +[[nodiscard]] auto locate_assets_dir() -> fs::path { + // look for '/assets/', starting from the working + // directory and walking up the parent directory tree. + static constexpr std::string_view dir_name_v{"assets"}; + for (auto path = fs::current_path(); + !path.empty() && path.has_parent_path(); path = path.parent_path()) { + auto ret = path / dir_name_v; + if (fs::is_directory(ret)) { return ret; } + } + std::println("[lvk] Warning: could not locate '{}' directory", dir_name_v); + return fs::current_path(); +} + +// ... +m_assets_dir = locate_assets_dir(); +``` diff --git a/guide/translations/ko-KR/src/shader_objects/pipelines.md b/guide/translations/ko-KR/src/shader_objects/pipelines.md new file mode 100644 index 0000000..3078b12 --- /dev/null +++ b/guide/translations/ko-KR/src/shader_objects/pipelines.md @@ -0,0 +1,254 @@ +# 그래픽스 파이프라인 + +여기서는 셰이더 오브젝트 대신 그래픽스 파이프라인을 사용하는 방법을 설명합니다. 이 가이드는 셰이더 오브젝트를 사용한다고 가정하지만, 그래픽스 파이프라인을 대신 사용하더라도 나머지 코드에는 큰 변화가 없을 것입니다. 다만, 알아둬야할 예외 사항은 디스크립터 셋 레이아웃 설정 방식입니다. 셰이더 오브젝트에서는 ShaderEXT의 CreateInfo에 포함되지만, 파이프라인을 사용할 경우 파이프라인 레이아웃에 지정해야 합니다. + +## 파이프라인 상태 + +셰이더 오브젝트는 대부분의 동적 상태를 런타임에 설정할 수 있었지만, 파이프라인에서는 파이프라인 생성 시점에 고정되기 때문에 정적입니다. 파이프라인은 또한 어태치먼트 포맷과 샘플 수 같은 추가 파라미터를 요구합니다. 이러한 값들은 상수로 간주되어 이후의 추상화 클래스에 담길 것입니다. 동적 상태의 일부를 구조체를 통해 나타냅시다. + +```cpp +// bit flags for various binary Pipeline States. +struct PipelineFlag { + enum : std::uint8_t { + None = 0, + AlphaBlend = 1 << 0, // turn on alpha blending. + DepthTest = 1 << 1, // turn on depth write and test. + }; +}; + +// specification of a unique Graphics Pipeline. +struct PipelineState { + using Flag = PipelineFlag; + + [[nodiscard]] static constexpr auto default_flags() -> std::uint8_t { + return Flag::AlphaBlend | Flag::DepthTest; + } + + vk::ShaderModule vertex_shader; // required. + vk::ShaderModule fragment_shader; // required. + + std::span vertex_attributes{}; + std::span vertex_bindings{}; + + vk::PrimitiveTopology topology{vk::PrimitiveTopology::eTriangleList}; + vk::PolygonMode polygon_mode{vk::PolygonMode::eFill}; + vk::CullModeFlags cull_mode{vk::CullModeFlagBits::eNone}; + vk::CompareOp depth_compare{vk::CompareOp::eLess}; + std::uint8_t flags{default_flags()}; +}; +``` + +파이프라인을 구성하는 과정을 클래스로 캡슐화합시다. + +```cpp +struct PipelineBuilderCreateInfo { + vk::Device device{}; + vk::SampleCountFlagBits samples{}; + vk::Format color_format{}; + vk::Format depth_format{}; +}; + +class PipelineBuilder { + public: + using CreateInfo = PipelineBuilderCreateInfo; + + explicit PipelineBuilder(CreateInfo const& create_info) + : m_info(create_info) {} + + [[nodiscard]] auto build(vk::PipelineLayout layout, + PipelineState const& state) const + -> vk::UniquePipeline; + + private: + CreateInfo m_info{}; +}; +``` + +구현은 다소 길어질 수 있으니, 여러 함수로 나누어 작성하는 것이 좋습니다. + +```cpp +// single viewport and scissor. +constexpr auto viewport_state_v = + vk::PipelineViewportStateCreateInfo({}, 1, {}, 1); + +// these dynamic states are guaranteed to be available. +constexpr auto dynamic_states_v = std::array{ + vk::DynamicState::eViewport, + vk::DynamicState::eScissor, + vk::DynamicState::eLineWidth, +}; + +[[nodiscard]] auto create_shader_stages(vk::ShaderModule const vertex, + vk::ShaderModule const fragment) { + // set vertex (0) and fragment (1) shader stages. + auto ret = std::array{}; + ret[0] + .setStage(vk::ShaderStageFlagBits::eVertex) + .setPName("main") + .setModule(vertex); + ret[1] + .setStage(vk::ShaderStageFlagBits::eFragment) + .setPName("main") + .setModule(fragment); + return ret; +} + +[[nodiscard]] constexpr auto +create_depth_stencil_state(std::uint8_t flags, + vk::CompareOp const depth_compare) { + auto ret = vk::PipelineDepthStencilStateCreateInfo{}; + auto const depth_test = + (flags & PipelineFlag::DepthTest) == PipelineFlag::DepthTest; + ret.setDepthTestEnable(depth_test ? vk::True : vk::False) + .setDepthCompareOp(depth_compare); + return ret; +} + +[[nodiscard]] constexpr auto +create_color_blend_attachment(std::uint8_t const flags) { + auto ret = vk::PipelineColorBlendAttachmentState{}; + auto const alpha_blend = + (flags & PipelineFlag::AlphaBlend) == PipelineFlag::AlphaBlend; + using CCF = vk::ColorComponentFlagBits; + ret.setColorWriteMask(CCF::eR | CCF::eG | CCF::eB | CCF::eA) + .setBlendEnable(alpha_blend ? vk::True : vk::False) + // standard alpha blending: + // (alpha * src) + (1 - alpha) * dst + .setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) + .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha) + .setColorBlendOp(vk::BlendOp::eAdd) + .setSrcAlphaBlendFactor(vk::BlendFactor::eOne) + .setDstAlphaBlendFactor(vk::BlendFactor::eZero) + .setAlphaBlendOp(vk::BlendOp::eAdd); + return ret; +} + +// ... +auto PipelineBuilder::build(vk::PipelineLayout const layout, + PipelineState const& state) const + -> vk::UniquePipeline { + auto const shader_stage_ci = + create_shader_stages(state.vertex_shader, state.fragment_shader); + + auto vertex_input_ci = vk::PipelineVertexInputStateCreateInfo{}; + vertex_input_ci.setVertexAttributeDescriptions(state.vertex_attributes) + .setVertexBindingDescriptions(state.vertex_bindings); + + auto multisample_state_ci = vk::PipelineMultisampleStateCreateInfo{}; + multisample_state_ci.setRasterizationSamples(m_info.samples) + .setSampleShadingEnable(vk::False); + + auto const input_assembly_ci = + vk::PipelineInputAssemblyStateCreateInfo{{}, state.topology}; + + auto rasterization_state_ci = vk::PipelineRasterizationStateCreateInfo{}; + rasterization_state_ci.setPolygonMode(state.polygon_mode) + .setCullMode(state.cull_mode); + + auto const depth_stencil_state_ci = + create_depth_stencil_state(state.flags, state.depth_compare); + + auto const color_blend_attachment = + create_color_blend_attachment(state.flags); + auto color_blend_state_ci = vk::PipelineColorBlendStateCreateInfo{}; + color_blend_state_ci.setAttachments(color_blend_attachment); + + auto dynamic_state_ci = vk::PipelineDynamicStateCreateInfo{}; + dynamic_state_ci.setDynamicStates(dynamic_states_v); + + // Dynamic Rendering requires passing this in the pNext chain. + auto rendering_ci = vk::PipelineRenderingCreateInfo{}; + // could be a depth-only pass, argument is span-like (notice the plural + // `Formats()`), only set if not Undefined. + if (m_info.color_format != vk::Format::eUndefined) { + rendering_ci.setColorAttachmentFormats(m_info.color_format); + } + // single depth attachment format, ok to set to Undefined. + rendering_ci.setDepthAttachmentFormat(m_info.depth_format); + + auto pipeline_ci = vk::GraphicsPipelineCreateInfo{}; + pipeline_ci.setLayout(layout) + .setStages(shader_stage_ci) + .setPVertexInputState(&vertex_input_ci) + .setPViewportState(&viewport_state_v) + .setPMultisampleState(&multisample_state_ci) + .setPInputAssemblyState(&input_assembly_ci) + .setPRasterizationState(&rasterization_state_ci) + .setPDepthStencilState(&depth_stencil_state_ci) + .setPColorBlendState(&color_blend_state_ci) + .setPDynamicState(&dynamic_state_ci) + .setPNext(&rendering_ci); + + auto ret = vk::Pipeline{}; + // use non-throwing API. + if (m_info.device.createGraphicsPipelines({}, 1, &pipeline_ci, {}, &ret) != + vk::Result::eSuccess) { + std::println(stderr, "[lvk] Failed to create Graphics Pipeline"); + return {}; + } + + return vk::UniquePipeline{ret, m_info.device}; +} +``` + +`App`은 빌더, 파이프라인 레이아웃, 그리고 파이프라인을 담아야 합니다. + +```cpp +std::optional m_pipeline_builder{}; +vk::UniquePipelineLayout m_pipeline_layout{}; +vk::UniquePipeline m_pipeline{}; + +// ... +void create_pipeline() { + auto const vertex_spirv = to_spir_v(asset_path("shader.vert")); + auto const fragment_spirv = to_spir_v(asset_path("shader.frag")); + if (vertex_spirv.empty() || fragment_spirv.empty()) { + throw std::runtime_error{"Failed to load shaders"}; + } + + auto pipeline_layout_ci = vk::PipelineLayoutCreateInfo{}; + pipeline_layout_ci.setSetLayouts({}); + m_pipeline_layout = + m_device->createPipelineLayoutUnique(pipeline_layout_ci); + + auto const pipeline_builder_ci = PipelineBuilder::CreateInfo{ + .device = *m_device, + .samples = vk::SampleCountFlagBits::e1, + .color_format = m_swapchain->get_format(), + }; + m_pipeline_builder.emplace(pipeline_builder_ci); + + auto vertex_ci = vk::ShaderModuleCreateInfo{}; + vertex_ci.setCode(vertex_spirv); + auto fragment_ci = vk::ShaderModuleCreateInfo{}; + fragment_ci.setCode(fragment_spirv); + + auto const vertex_shader = + m_device->createShaderModuleUnique(vertex_ci); + auto const fragment_shader = + m_device->createShaderModuleUnique(fragment_ci); + auto const pipeline_state = PipelineState{ + .vertex_shader = *vertex_shader, + .fragment_shader = *fragment_shader, + }; + m_pipeline = + m_pipeline_builder->build(*m_pipeline_layout, pipeline_state); +} +``` + +마지막으로 `App::draw()`를 구현합니다. + +```cpp +void draw(vk::CommandBuffer const command_buffer) const { + command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, + *m_pipeline); + auto viewport = vk::Viewport{}; + viewport.setX(0.0f) + .setY(static_cast(m_render_target->extent.height)) + .setWidth(static_cast(m_render_target->extent.width)) + .setHeight(-viewport.y); + command_buffer.setViewport(0, viewport); + command_buffer.setScissor(0, vk::Rect2D{{}, m_render_target->extent}); + command_buffer.draw(3, 1, 0, 0); +} +``` diff --git a/guide/translations/ko-KR/src/shader_objects/shader_program.md b/guide/translations/ko-KR/src/shader_objects/shader_program.md new file mode 100644 index 0000000..8b9ebfb --- /dev/null +++ b/guide/translations/ko-KR/src/shader_objects/shader_program.md @@ -0,0 +1,305 @@ +# 셰이더 프로그램 + +셰이더 오브젝트를 사용하려면 디바이스 생성 시 대응되는 기능과 확장을 활성화해야 합니다. + +```cpp +auto shader_object_feature = + vk::PhysicalDeviceShaderObjectFeaturesEXT{vk::True}; +dynamic_rendering_feature.setPNext(&shader_object_feature); + +// ... +// we need two device extensions: Swapchain and Shader Object. +static constexpr auto extensions_v = std::array{ + VK_KHR_SWAPCHAIN_EXTENSION_NAME, + "VK_EXT_shader_object", +}; +``` + +## 에뮬레이션 레이어 + +현재 사용중인 드라이버나 물리 디바이스가 `VK_EXT_shader_object`를 지원하지 않을 수 있기 때문에(특히 인텔에서 자주 발생합니다) 디바이스 생성이 실패할 수 있습니다. Vulkan SDK는 이 확장을 구현하는 레이어 [`VK_LAYER_KHRONOS_shader_object`](https://github.com/KhronosGroup/Vulkan-ExtensionLayer/blob/main/docs/shader_object_layer.md)를 제공합니다. 이 레이어를 InstanceCreateInfo에 추가하면 해당 기능을 사용할 수 있습니다. + + +```cpp +// ... +// add the Shader Object emulation layer. +static constexpr auto layers_v = std::array{ + "VK_LAYER_KHRONOS_shader_object", +}; +instance_ci.setPEnabledLayerNames(layers_v); + +m_instance = vk::createInstanceUnique(instance_ci); +// ... +``` + +
+이 레이어는 표준 Vulkan 드라이버 설치에 포함되어 있지 않기 때문에, Vulkan SDK나 Vulkan Configurator가 없는 환경에서도 실행할 수 있도록 애플리케이션과 함께 패키징해야 합니다. 자세한 내용은여기를 참고하세요. +
+ +원하는 레이어가 사용 불가능할 수 있으므로, 이를 확인하는 코드를 추가하는 것이 좋습니다. + +```cpp +[[nodiscard]] auto get_layers(std::span desired) + -> std::vector { + auto ret = std::vector{}; + ret.reserve(desired.size()); + auto const available = vk::enumerateInstanceLayerProperties(); + for (char const* layer : desired) { + auto const pred = [layer = std::string_view{layer}]( + vk::LayerProperties const& properties) { + return properties.layerName == layer; + }; + if (std::ranges::find_if(available, pred) == available.end()) { + std::println("[lvk] [WARNING] Vulkan Layer '{}' not found", layer); + continue; + } + ret.push_back(layer); + } + return ret; +} + +// ... +auto const layers = get_layers(layers_v); +instance_ci.setPEnabledLayerNames(layers); +``` + +## `class ShaderProgram` + +정점 셰이더와 프래그먼트 셰이더를 하나의 `ShaderProgram`으로 캡슐화하여, 그리기 전에 셰이더를 바인딩하고 다양한 동적 상태를 설정할 수 있도록 하겠습니다. + +`shader_program.hpp`에서 가장 먼저 `ShaderProgramCreateInfo` 구조체를 추가합니다. + +```cpp +struct ShaderProgramCreateInfo { + vk::Device device; + std::span vertex_spirv; + std::span fragment_spirv; + std::span set_layouts; +}; +``` + +> 디스크립터 셋과 레이아웃은 이후에 다루겠습니다. + +간단한 형태의 정의부터 시작합니다. + +```cpp +class ShaderProgram { + public: + using CreateInfo = ShaderProgramCreateInfo; + + explicit ShaderProgram(CreateInfo const& create_info); + + private: + std::vector m_shaders{}; + + ScopedWaiter m_waiter{}; +}; +``` + +생성자의 정의는 꽤 단순합니다. + +```cpp +ShaderProgram::ShaderProgram(CreateInfo const& create_info) { + auto const create_shader_ci = + [&create_info](std::span spirv) { + auto ret = vk::ShaderCreateInfoEXT{}; + ret.setCodeSize(spirv.size_bytes()) + .setPCode(spirv.data()) + // set common parameters. + .setSetLayouts(create_info.set_layouts) + .setCodeType(vk::ShaderCodeTypeEXT::eSpirv) + .setPName("main"); + return ret; + }; + + auto shader_cis = std::array{ + create_shader_ci(create_info.vertex_spirv), + create_shader_ci(create_info.fragment_spirv), + }; + shader_cis[0] + .setStage(vk::ShaderStageFlagBits::eVertex) + .setNextStage(vk::ShaderStageFlagBits::eFragment); + shader_cis[1].setStage(vk::ShaderStageFlagBits::eFragment); + + auto result = create_info.device.createShadersEXTUnique(shader_cis); + if (result.result != vk::Result::eSuccess) { + throw std::runtime_error{"Failed to create Shader Objects"}; + } + m_shaders = std::move(result.value); + m_waiter = create_info.device; +} +``` + +몇 가지 동적 상태를 public 멤버를 통해 나타냅니다. + +```cpp +static constexpr auto color_blend_equation_v = [] { + auto ret = vk::ColorBlendEquationEXT{}; + ret.setColorBlendOp(vk::BlendOp::eAdd) + // standard alpha blending: + // (alpha * src) + (1 - alpha) * dst + .setSrcColorBlendFactor(vk::BlendFactor::eSrcAlpha) + .setDstColorBlendFactor(vk::BlendFactor::eOneMinusSrcAlpha); + return ret; +}(); + +// ... +vk::PrimitiveTopology topology{vk::PrimitiveTopology::eTriangleList}; +vk::PolygonMode polygon_mode{vk::PolygonMode::eFill}; +float line_width{1.0f}; +vk::ColorBlendEquationEXT color_blend_equation{color_blend_equation_v}; +vk::CompareOp depth_compare_op{vk::CompareOp::eLessOrEqual}; +``` + +불 값을 비트 플래그로 캡슐화합니다. + +```cpp +// bit flags for various binary states. +enum : std::uint8_t { + None = 0, + AlphaBlend = 1 << 0, // turn on alpha blending. + DepthTest = 1 << 1, // turn on depth write and test. +}; + +// ... +static constexpr auto flags_v = AlphaBlend | DepthTest; + +// ... +std::uint8_t flags{flags_v}; +``` + +파이프라인 상태에 필요한 요소가 하나 남아있습니다. 정점 입력입니다. 이는 셰이더마다 고정된 값이 될 것이므로 생성자에 저장할 것입니다. + +```cpp +// shader_program.hpp + +// vertex attributes and bindings. +struct ShaderVertexInput { + std::span attributes{}; + std::span bindings{}; +}; + +struct ShaderProgramCreateInfo { + // ... + ShaderVertexInput vertex_input{}; + // ... +}; + +class ShaderProgram { + // ... + ShaderVertexInput m_vertex_input{}; + std::vector m_shaders{}; + // ... +}; + +// shader_program.cpp +ShaderProgram::ShaderProgram(CreateInfo const& create_info) + : m_vertex_input(create_info.vertex_input) { + // ... +} +``` + +바인딩할 API는 Viewport와 Scissor 설정을 위해 커맨드 버퍼와 프레임 버퍼 크기를 받습니다 + +```cpp +void bind(vk::CommandBuffer command_buffer, + glm::ivec2 framebuffer_size) const; +``` + +멤버 함수를 추가하고 순차적으로 이를 호출하여 `bind()`를 구현합니다. + +```cpp +static void set_viewport_scissor(vk::CommandBuffer command_buffer, + glm::ivec2 framebuffer); +static void set_static_states(vk::CommandBuffer command_buffer); +void set_common_states(vk::CommandBuffer command_buffer) const; +void set_vertex_states(vk::CommandBuffer command_buffer) const; +void set_fragment_states(vk::CommandBuffer command_buffer) const; +void bind_shaders(vk::CommandBuffer command_buffer) const; + +// ... +void ShaderProgram::bind(vk::CommandBuffer const command_buffer, + glm::ivec2 const framebuffer_size) const { + set_viewport_scissor(command_buffer, framebuffer_size); + set_static_states(command_buffer); + set_common_states(command_buffer); + set_vertex_states(command_buffer); + set_fragment_states(command_buffer); + bind_shaders(command_buffer); +} +``` + +구현은 길지만 꽤 단순합니다. + +```cpp +namespace { +constexpr auto to_vkbool(bool const value) { + return value ? vk::True : vk::False; +} +} // namespace + +// ... +void ShaderProgram::set_viewport_scissor(vk::CommandBuffer const command_buffer, + glm::ivec2 const framebuffer_size) { + auto const fsize = glm::vec2{framebuffer_size}; + auto viewport = vk::Viewport{}; + // flip the viewport about the X-axis (negative height): + // https://www.saschawillems.de/blog/2019/03/29/flipping-the-vulkan-viewport/ + viewport.setX(0.0f).setY(fsize.y).setWidth(fsize.x).setHeight(-fsize.y); + command_buffer.setViewportWithCount(viewport); + + auto const usize = glm::uvec2{framebuffer_size}; + auto const scissor = + vk::Rect2D{vk::Offset2D{}, vk::Extent2D{usize.x, usize.y}}; + command_buffer.setScissorWithCount(scissor); +} + +void ShaderProgram::set_static_states(vk::CommandBuffer const command_buffer) { + command_buffer.setRasterizerDiscardEnable(vk::False); + command_buffer.setRasterizationSamplesEXT(vk::SampleCountFlagBits::e1); + command_buffer.setSampleMaskEXT(vk::SampleCountFlagBits::e1, 0xff); + command_buffer.setAlphaToCoverageEnableEXT(vk::False); + command_buffer.setCullMode(vk::CullModeFlagBits::eNone); + command_buffer.setFrontFace(vk::FrontFace::eCounterClockwise); + command_buffer.setDepthBiasEnable(vk::False); + command_buffer.setStencilTestEnable(vk::False); + command_buffer.setPrimitiveRestartEnable(vk::False); + command_buffer.setColorWriteMaskEXT(0, ~vk::ColorComponentFlags{}); +} + +void ShaderProgram::set_common_states( + vk::CommandBuffer const command_buffer) const { + auto const depth_test = to_vkbool((flags & DepthTest) == DepthTest); + command_buffer.setDepthWriteEnable(depth_test); + command_buffer.setDepthTestEnable(depth_test); + command_buffer.setDepthCompareOp(depth_compare_op); + command_buffer.setPolygonModeEXT(polygon_mode); + command_buffer.setLineWidth(line_width); +} + +void ShaderProgram::set_vertex_states( + vk::CommandBuffer const command_buffer) const { + command_buffer.setVertexInputEXT(m_vertex_input.bindings, + m_vertex_input.attributes); + command_buffer.setPrimitiveTopology(topology); +} + +void ShaderProgram::set_fragment_states( + vk::CommandBuffer const command_buffer) const { + auto const alpha_blend = to_vkbool((flags & AlphaBlend) == AlphaBlend); + command_buffer.setColorBlendEnableEXT(0, alpha_blend); + command_buffer.setColorBlendEquationEXT(0, color_blend_equation); +} + +void ShaderProgram::bind_shaders(vk::CommandBuffer const command_buffer) const { + static constexpr auto stages_v = std::array{ + vk::ShaderStageFlagBits::eVertex, + vk::ShaderStageFlagBits::eFragment, + }; + auto const shaders = std::array{ + *m_shaders[0], + *m_shaders[1], + }; + command_buffer.bindShadersEXT(stages_v, shaders); +} +``` diff --git a/guide/translations/ko-KR/src/shader_objects/srgb_triangle.png b/guide/translations/ko-KR/src/shader_objects/srgb_triangle.png new file mode 100644 index 0000000..a160a61 Binary files /dev/null and b/guide/translations/ko-KR/src/shader_objects/srgb_triangle.png differ diff --git a/guide/translations/ko-KR/src/shader_objects/srgb_triangle_wireframe.png b/guide/translations/ko-KR/src/shader_objects/srgb_triangle_wireframe.png new file mode 100644 index 0000000..92b9c6b Binary files /dev/null and b/guide/translations/ko-KR/src/shader_objects/srgb_triangle_wireframe.png differ diff --git a/guide/translations/ko-KR/src/shader_objects/white_triangle.png b/guide/translations/ko-KR/src/shader_objects/white_triangle.png new file mode 100644 index 0000000..e8de477 Binary files /dev/null and b/guide/translations/ko-KR/src/shader_objects/white_triangle.png differ