From cf3de6928c0a9ea6d75b0bbf45de4ca547f48572 Mon Sep 17 00:00:00 2001 From: erenengine Date: Sat, 21 Jun 2025 12:21:47 +0900 Subject: [PATCH 1/4] Add Korean translations for FAQ and Privacy Policy documents - Created a new FAQ document (90_FAQ.md) addressing common issues in Vulkan application development, including solutions for access violations, validation layer issues, and compatibility problems with Steam overlay. - Added a Privacy Policy document (95_Privacy_policy.md) detailing data collection practices, analytics usage, and third-party services like Disqus for comments. --- ko-rust/00_Introduction.md | 65 ++ ko-rust/01_Overview.md | 119 +++ ko-rust/02_Development_environment.md | 206 +++++ .../00_Setup/00_Base_code.md | 217 +++++ .../00_Setup/01_Instance.md | 221 +++++ .../00_Setup/02_Validation_layers.md | 458 +++++++++++ .../03_Physical_devices_and_queue_families.md | 364 +++++++++ .../00_Setup/04_Logical_device_and_queues.md | 171 ++++ .../01_Presentation/00_Window_surface.md | 233 ++++++ .../01_Presentation/01_Swap_chain.md | 603 ++++++++++++++ .../01_Presentation/02_Image_views.md | 127 +++ .../00_Introduction.md | 99 +++ .../01_Shader_modules.md | 467 +++++++++++ .../02_Fixed_functions.md | 439 ++++++++++ .../03_Render_passes.md | 215 +++++ .../04_Conclusion.md | 122 +++ .../03_Drawing/00_Framebuffers.md | 107 +++ .../03_Drawing/01_Command_buffers.md | 344 ++++++++ .../02_Rendering_and_presentation.md | 577 +++++++++++++ .../03_Drawing/03_Frames_in_flight.md | 176 ++++ .../04_Swap_chain_recreation.md | 280 +++++++ .../00_Vertex_input_description.md | 225 +++++ .../01_Vertex_buffer_creation.md | 342 ++++++++ .../04_Vertex_buffers/02_Staging_buffer.md | 267 ++++++ ko-rust/04_Vertex_buffers/03_Index_buffer.md | 179 ++++ .../00_Descriptor_set_layout_and_buffer.md | 416 ++++++++++ .../01_Descriptor_pool_and_sets.md | 391 +++++++++ ko-rust/06_Texture_mapping/00_Images.md | 769 ++++++++++++++++++ .../01_Image_view_and_sampler.md | 369 +++++++++ .../02_Combined_image_sampler.md | 296 +++++++ ko-rust/07_Depth_buffering.md | 445 ++++++++++ ko-rust/08_Loading_models.md | 232 ++++++ ko-rust/09_Generating_Mipmaps.md | 466 +++++++++++ ko-rust/10_Multisampling.md | 408 ++++++++++ ko-rust/11_Compute_Shader.md | 454 +++++++++++ ko-rust/90_FAQ.md | 61 ++ ko-rust/95_Privacy_policy.md | 23 + ko/00_Introduction.md | 67 ++ ko/01_Overview.md | 121 +++ ko/02_Development_environment.md | 462 +++++++++++ .../00_Setup/00_Base_code.md | 217 +++++ .../00_Setup/01_Instance.md | 221 +++++ .../00_Setup/02_Validation_layers.md | 458 +++++++++++ .../03_Physical_devices_and_queue_families.md | 364 +++++++++ .../00_Setup/04_Logical_device_and_queues.md | 171 ++++ .../01_Presentation/00_Window_surface.md | 233 ++++++ .../01_Presentation/01_Swap_chain.md | 603 ++++++++++++++ .../01_Presentation/02_Image_views.md | 127 +++ .../00_Introduction.md | 99 +++ .../01_Shader_modules.md | 467 +++++++++++ .../02_Fixed_functions.md | 439 ++++++++++ .../03_Render_passes.md | 215 +++++ .../04_Conclusion.md | 122 +++ .../03_Drawing/00_Framebuffers.md | 107 +++ .../03_Drawing/01_Command_buffers.md | 344 ++++++++ .../02_Rendering_and_presentation.md | 577 +++++++++++++ .../03_Drawing/03_Frames_in_flight.md | 176 ++++ .../04_Swap_chain_recreation.md | 280 +++++++ .../00_Vertex_input_description.md | 225 +++++ .../01_Vertex_buffer_creation.md | 342 ++++++++ ko/04_Vertex_buffers/02_Staging_buffer.md | 267 ++++++ ko/04_Vertex_buffers/03_Index_buffer.md | 179 ++++ .../00_Descriptor_set_layout_and_buffer.md | 416 ++++++++++ .../01_Descriptor_pool_and_sets.md | 391 +++++++++ ko/06_Texture_mapping/00_Images.md | 769 ++++++++++++++++++ .../01_Image_view_and_sampler.md | 369 +++++++++ .../02_Combined_image_sampler.md | 296 +++++++ ko/07_Depth_buffering.md | 498 ++++++++++++ ko/08_Loading_models.md | 246 ++++++ ko/09_Generating_Mipmaps.md | 352 ++++++++ ko/10_Multisampling.md | 292 +++++++ ko/11_Compute_Shader.md | 651 +++++++++++++++ ko/90_FAQ.md | 51 ++ ko/95_Privacy_policy.md | 21 + 74 files changed, 22188 insertions(+) create mode 100644 ko-rust/00_Introduction.md create mode 100644 ko-rust/01_Overview.md create mode 100644 ko-rust/02_Development_environment.md create mode 100644 ko-rust/03_Drawing_a_triangle/00_Setup/00_Base_code.md create mode 100644 ko-rust/03_Drawing_a_triangle/00_Setup/01_Instance.md create mode 100644 ko-rust/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md create mode 100644 ko-rust/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md create mode 100644 ko-rust/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md create mode 100644 ko-rust/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md create mode 100644 ko-rust/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md create mode 100644 ko-rust/03_Drawing_a_triangle/01_Presentation/02_Image_views.md create mode 100644 ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md create mode 100644 ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md create mode 100644 ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md create mode 100644 ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md create mode 100644 ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md create mode 100644 ko-rust/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md create mode 100644 ko-rust/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md create mode 100644 ko-rust/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md create mode 100644 ko-rust/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md create mode 100644 ko-rust/03_Drawing_a_triangle/04_Swap_chain_recreation.md create mode 100644 ko-rust/04_Vertex_buffers/00_Vertex_input_description.md create mode 100644 ko-rust/04_Vertex_buffers/01_Vertex_buffer_creation.md create mode 100644 ko-rust/04_Vertex_buffers/02_Staging_buffer.md create mode 100644 ko-rust/04_Vertex_buffers/03_Index_buffer.md create mode 100644 ko-rust/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md create mode 100644 ko-rust/05_Uniform_buffers/01_Descriptor_pool_and_sets.md create mode 100644 ko-rust/06_Texture_mapping/00_Images.md create mode 100644 ko-rust/06_Texture_mapping/01_Image_view_and_sampler.md create mode 100644 ko-rust/06_Texture_mapping/02_Combined_image_sampler.md create mode 100644 ko-rust/07_Depth_buffering.md create mode 100644 ko-rust/08_Loading_models.md create mode 100644 ko-rust/09_Generating_Mipmaps.md create mode 100644 ko-rust/10_Multisampling.md create mode 100644 ko-rust/11_Compute_Shader.md create mode 100644 ko-rust/90_FAQ.md create mode 100644 ko-rust/95_Privacy_policy.md create mode 100644 ko/00_Introduction.md create mode 100644 ko/01_Overview.md create mode 100644 ko/02_Development_environment.md create mode 100644 ko/03_Drawing_a_triangle/00_Setup/00_Base_code.md create mode 100644 ko/03_Drawing_a_triangle/00_Setup/01_Instance.md create mode 100644 ko/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md create mode 100644 ko/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md create mode 100644 ko/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md create mode 100644 ko/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md create mode 100644 ko/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md create mode 100644 ko/03_Drawing_a_triangle/01_Presentation/02_Image_views.md create mode 100644 ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md create mode 100644 ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md create mode 100644 ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md create mode 100644 ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md create mode 100644 ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md create mode 100644 ko/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md create mode 100644 ko/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md create mode 100644 ko/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md create mode 100644 ko/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md create mode 100644 ko/03_Drawing_a_triangle/04_Swap_chain_recreation.md create mode 100644 ko/04_Vertex_buffers/00_Vertex_input_description.md create mode 100644 ko/04_Vertex_buffers/01_Vertex_buffer_creation.md create mode 100644 ko/04_Vertex_buffers/02_Staging_buffer.md create mode 100644 ko/04_Vertex_buffers/03_Index_buffer.md create mode 100644 ko/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md create mode 100644 ko/05_Uniform_buffers/01_Descriptor_pool_and_sets.md create mode 100644 ko/06_Texture_mapping/00_Images.md create mode 100644 ko/06_Texture_mapping/01_Image_view_and_sampler.md create mode 100644 ko/06_Texture_mapping/02_Combined_image_sampler.md create mode 100644 ko/07_Depth_buffering.md create mode 100644 ko/08_Loading_models.md create mode 100644 ko/09_Generating_Mipmaps.md create mode 100644 ko/10_Multisampling.md create mode 100644 ko/11_Compute_Shader.md create mode 100644 ko/90_FAQ.md create mode 100644 ko/95_Privacy_policy.md diff --git a/ko-rust/00_Introduction.md b/ko-rust/00_Introduction.md new file mode 100644 index 00000000..ee0a7443 --- /dev/null +++ b/ko-rust/00_Introduction.md @@ -0,0 +1,65 @@ +## 소개 + +이 튜토리얼에서는 [Vulkan](https://www.khronos.org/vulkan/) 그래픽 및 컴퓨팅 API의 기초를 배웁니다. Vulkan은 [Khronos group](https://www.khronos.org/)(OpenGL으로 유명한)에서 만든 새로운 API로, 최신 그래픽 카드를 훨씬 더 잘 추상화합니다. 이 새로운 인터페이스를 통해 애플리케이션이 무엇을 하려는지 더 잘 기술할 수 있으며, 이는 [OpenGL](https://en.wikipedia.org/wiki/OpenGL)이나 [Direct3D](https://en.wikipedia.org/wiki/Direct3D)와 같은 기존 API에 비해 더 나은 성능과 예측하기 쉬운 드라이버 동작으로 이어질 수 있습니다. Vulkan의 기본 개념은 [Direct3D 12](https://en.wikipedia.org/wiki/Direct3D#Direct3D_12)나 [Metal](https://en.wikipedia.org/wiki/Metal_(API))과 유사하지만, Vulkan은 완전한 크로스플랫폼이라는 장점이 있어 Windows, Linux, Android용 개발을 동시에 할 수 있습니다. + +하지만 이러한 이점을 얻기 위해 치러야 할 대가는 상당히 장황한 API를 다뤄야 한다는 것입니다. 초기 프레임 버퍼 생성, 버퍼 및 텍스처 이미지와 같은 객체에 대한 메모리 관리 등 그래픽 API와 관련된 모든 세부 사항을 애플리케이션에서 처음부터 설정해야 합니다. 그래픽 드라이버가 해주는 것들이 훨씬 적어지는데, 이는 정확한 동작을 보장하기 위해 애플리케이션에서 더 많은 작업을 해야 한다는 것을 의미합니다. + +핵심은 Vulkan이 모두를 위한 것은 아니라는 점입니다. Vulkan은 고성능 컴퓨터 그래픽에 열정적이고, 기꺼이 노력을 투자할 의향이 있는 프로그래머를 대상으로 합니다. 컴퓨터 그래픽보다는 게임 개발에 더 관심이 있다면, 가까운 시일 내에 Vulkan 때문에 지원이 중단되지는 않을 OpenGL이나 Direct3D를 계속 사용하는 것이 좋습니다. 또 다른 대안은 [Unreal Engine](https://en.wikipedia.org/wiki/Unreal_Engine#Unreal_Engine_4)이나 [Unity](https://en.wikipedia.org/wiki/Unity_(game_engine))와 같은 엔진을 사용하는 것입니다. 이 엔진들은 내부적으로 Vulkan을 사용하면서도 여러분에게 훨씬 더 높은 수준의 API를 제공할 수 있습니다. + +이제 이 점을 명확히 했으니, 이 튜토리얼을 따라가기 위한 몇 가지 선수 조건을 살펴보겠습니다: + +* Vulkan과 호환되는 그래픽 카드 및 드라이버 ([NVIDIA](https://developer.nvidia.com/vulkan-driver), [AMD](http://www.amd.com/en-us/innovations/software-technologies/technologies-gaming/vulkan), [Intel](https://software.intel.com/en-us/blogs/2016/03/14/new-intel-vulkan-beta-1540204404-graphics-driver-for-windows-78110-1540), [Apple Silicon (또는 Apple M1)](https://www.phoronix.com/scan.php?page=news_item&px=Apple-Silicon-Vulkan-MoltenVK)) +* Rust 경험 (소유권(ownership), 생명주기(lifetimes), 트레잇(traits)에 대한 이해) +* 최신 Rust 툴체인 (`rustup`을 통해 설치) +* 3D 컴퓨터 그래픽에 대한 약간의 경험 + +이 튜토리얼은 OpenGL이나 Direct3D 개념에 대한 지식을 가정하지는 않지만, 3D 컴퓨터 그래픽의 기초는 알고 있어야 합니다. 예를 들어, 원근 투영(perspective projection)의 기저에 있는 수학은 설명하지 않을 것입니다. 컴퓨터 그래픽 개념에 대한 훌륭한 입문서로는 [이 온라인 책](https://paroj.github.io/gltut/)을 참고하세요. 그 외 다른 훌륭한 컴퓨터 그래픽 자료는 다음과 같습니다: + +* [주말 동안 레이 트레이싱 (Ray tracing in one weekend)](https://github.com/RayTracing/raytracing.github.io) +* [물리 기반 렌더링(PBR) 책 (Physically Based Rendering book)](http://www.pbr-book.org/) +* 실제 엔진에서 Vulkan이 사용된 예시: 오픈소스 [Quake](https://github.com/Novum/vkQuake)와 [DOOM 3](https://github.com/DustinHLand/vkDOOM3) + +우리는 Rust의 강력한 타입 시스템과 소유권 모델을 활용하여 로직과 리소스의 생명 주기를 안전하게 관리할 것입니다. 이 튜토리얼에서는 Vulkan API에 대한 얇고(thin), `unsafe`한 래퍼(wrapper)인 [Ash](https://github.com/ash-rs/ash) 라이브러리를 사용할 것입니다. Ash를 사용하면 거의 네이티브 C API와 유사한 수준의 제어가 가능하여 Vulkan의 핵심을 깊이 이해하는 데 도움이 됩니다. 이는 안전성을 위해 많은 세부 사항을 추상화하는 [vulkano](https://github.com/vulkano-rs/vulkano)와 같은 고수준 라이브러리와는 다른 접근 방식입니다. + +## 전자책 + +이 튜토리얼을 전자책으로 읽고 싶다면, 아래 링크에서 EPUB 또는 PDF 버전을 다운로드할 수 있습니다: + +* [EPUB](https://vulkan-tutorial.com/resources/vulkan_tutorial_en.epub) +* [PDF](https://vulkan-tutorial.com/resources/vulkan_tutorial_en.pdf) + +## 튜토리얼 구조 + +우리는 Vulkan이 어떻게 작동하는지에 대한 개요와 화면에 첫 번째 삼각형을 띄우기 위해 해야 할 작업들을 살펴보는 것으로 시작할 것입니다. 전체 그림 속에서 각 작은 단계들의 기본적인 역할을 이해하고 나면 그 목적이 더 명확해질 것입니다. 다음으로, Cargo를 사용하여 개발 환경을 설정할 것입니다. 여기에는 [Vulkan SDK](https://lunarg.com/vulkan-sdk/), Vulkan 바인딩을 위한 [Ash](https://github.com/ash-rs/ash), 선형대수 연산을 위한 [glam](https://github.com/bitshifter/glam-rs), 그리고 창 생성을 위한 [winit](https://github.com/rust-windowing/winit) 라이브러리가 포함됩니다. + +그 후, 첫 번째 삼각형을 렌더링하는 데 필요한 Vulkan 프로그램의 모든 기본 구성 요소를 구현할 것입니다. 각 챕터는 대략 다음과 같은 구조를 따릅니다: + +* 새로운 개념과 그 목적을 소개합니다 +* 관련된 모든 Ash API 호출을 사용하여 프로그램에 통합합니다 +* `unsafe` 코드 블록을 감싸는 안전한 헬퍼 함수로 추상화합니다 + +각 챕터는 이전 챕터에 이어지도록 작성되었지만, 특정 Vulkan 기능을 소개하는 독립적인 문서로도 읽을 수 있습니다. 이는 이 사이트가 참고 자료로도 유용하다는 것을 의미합니다. 모든 Vulkan 함수와 타입은 사양(specification)에 링크되어 있으므로, 클릭하여 더 자세히 알아볼 수 있습니다. Vulkan은 매우 새로운 API이므로 사양 자체에 일부 미흡한 점이 있을 수 있습니다. [이 Khronos 리포지토리](https://github.com/KhronosGroup/Vulkan-Docs)에 피드백을 제출하는 것을 권장합니다. + +앞서 언급했듯이, Vulkan API는 그래픽 하드웨어를 최대한 제어할 수 있도록 많은 파라미터를 가진 장황한 API를 가지고 있습니다. 이로 인해 텍스처 생성과 같은 기본 작업도 매번 반복해야 하는 많은 단계를 거치게 됩니다. 따라서 우리는 `unsafe` Ash 호출을 안전한 인터페이스로 감싸는 우리만의 헬퍼 함수 모음을 튜토리얼 전반에 걸쳐 만들어 나갈 것입니다. + +또한 각 챕터는 해당 지점까지의 전체 코드 목록 링크로 마무리됩니다. 코드 구조에 대해 의문이 있거나, 버그를 처리하며 비교하고 싶을 때 참조할 수 있습니다. 모든 코드 파일은 여러 벤더의 그래픽 카드에서 테스트하여 정확성을 검증했습니다. 각 챕터 끝에는 댓글 섹션도 있어 특정 주제와 관련된 질문을 할 수 있습니다. 저희가 여러분을 돕기 쉽도록 플랫폼, 드라이버 버전, 소스 코드, 예상 동작 및 실제 동작을 명시해 주세요. + +이 튜토리얼은 커뮤니티의 노력으로 만들어지는 것을 목표로 합니다. Vulkan은 아직 매우 새로운 API이며 모범 사례(best practice)가 완전히 정립되지 않았습니다. 튜토리얼과 사이트 자체에 대한 어떤 종류의 피드백이든 있다면, 주저하지 말고 [GitHub 리포지토리](https://github.com/Overv/VulkanTutorial)에 이슈를 제출하거나 풀 리퀘스트(pull request)를 보내주세요. 리포지토리를 'watch'하면 튜토리얼 업데이트 알림을 받을 수 있습니다. + +Vulkan으로 여러분의 첫 번째 삼각형을 화면에 그리는 의식을 치른 후에는, 선형 변환, 텍스처, 3D 모델을 포함하도록 프로그램을 확장해 나갈 것입니다. + +이전에 그래픽 API를 다뤄본 적이 있다면, 첫 도형이 화면에 나타나기까지 많은 단계가 있을 수 있다는 것을 알 것입니다. Vulkan에는 이러한 초기 단계가 많지만, 각각의 개별 단계는 이해하기 쉽고 불필요하게 느껴지지 않을 것입니다. 또한, 그 지루해 보이는 삼각형을 일단 그리고 나면, 완전한 텍스처를 입힌 3D 모델을 그리는 데는 그리 많은 추가 작업이 필요하지 않으며, 그 지점을 넘어선 각 단계는 훨씬 더 보람찰 것이라는 점을 명심하는 것이 중요합니다. + +튜토리얼을 따라가다가 문제가 발생하면, 먼저 FAQ를 확인하여 문제와 해결책이 이미 있는지 확인해 보세요. 그래도 문제가 해결되지 않으면, 가장 관련 있는 챕터의 댓글 섹션에서 자유롭게 도움을 요청하세요. + +고성능 그래픽 API의 미래로 뛰어들 준비가 되셨나요? [시작합시다!](!ko/Overview) + +## 라이선스 + +Copyright (C) 2015-2023, Alexander Overvoorde + +콘텐츠는 별도로 명시되지 않는 한 [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)에 따라 라이선스가 부여됩니다. 기여함으로써 귀하는 귀하의 기여물을 동일한 라이선스 하에 대중에게 라이선스하는 데 동의하는 것입니다. + +소스 리포지토리의 `code` 디렉토리에 있는 코드 목록은 [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/)에 따라 라이선스가 부여됩니다. 해당 디렉토리에 기여함으로써 귀하는 귀하의 기여물을 동일한 퍼블릭 도메인과 유사한 라이선스 하에 대중에게 라이선스하는 데 동의하는 것입니다. + +이 프로그램은 유용할 것이라는 희망으로 배포되지만, 어떠한 보증도 없이 배포됩니다. 상품성이나 특정 목적에의 적합성에 대한 묵시적인 보증조차 없습니다. \ No newline at end of file diff --git a/ko-rust/01_Overview.md b/ko-rust/01_Overview.md new file mode 100644 index 00000000..7f3cc205 --- /dev/null +++ b/ko-rust/01_Overview.md @@ -0,0 +1,119 @@ +이번 장에서는 먼저 벌칸(Vulkan)의 소개와 벌칸이 해결하고자 하는 문제점들에 대해 알아봅니다. 그 후, Rust와 **`ash`** 라이브러리를 사용하여 첫 번째 삼각형을 그리는 데 필요한 요소들을 살펴볼 것입니다. 이를 통해 앞으로 이어질 각 장의 내용을 전체적인 그림 안에서 파악할 수 있게 될 것입니다. 마지막으로 벌칸 API의 구조와 `ash`를 사용한 일반적인 코딩 패턴을 다루며 마무리하겠습니다. + +## 벌칸의 기원 + +이전의 그래픽 API들과 마찬가지로, 벌칸은 [GPU](https.en.wikipedia.org/wiki/Graphics_processing_unit)에 대한 크로스플랫폼 추상화로 설계되었습니다. 이러한 기존 API 대부분의 문제점은, 이들이 설계될 당시의 그래픽 하드웨어가 대부분 설정 가능한 고정 기능(fixed functionality)에 제한되어 있었다는 것입니다. 프로그래머들은 정점(vertex) 데이터를 표준 형식으로 제공해야 했고, 조명이나 셰이딩 옵션에 대해서는 GPU 제조사의 재량에 맡겨야 했습니다. + +그래픽 카드 아키텍처가 발전함에 따라, 프로그래밍 가능한 기능들이 점점 더 많이 제공되기 시작했습니다. 이 모든 새로운 기능들은 어떻게든 기존 API와 통합되어야 했습니다. 그 결과 이상적이지 않은 추상화가 생겨났고, 그래픽 드라이버는 프로그래머의 의도를 현대 그래픽 아키텍처에 매핑하기 위해 많은 추측을 해야 했습니다. 이것이 바로 게임 성능을 향상시키기 위한 드라이버 업데이트가, 때로는 상당한 폭으로, 빈번하게 이루어지는 이유입니다. 이러한 드라이버의 복잡성 때문에, 애플리케이션 개발자들은 [셰이더](https.en.wikipedia.org/wiki/Shader)에 허용되는 문법과 같이 제조사 간의 불일치 문제도 다루어야 합니다. 이러한 새로운 기능 외에도, 지난 10년간 강력한 그래픽 하드웨어를 갖춘 모바일 기기들이 대거 등장했습니다. 이 모바일 GPU들은 에너지 및 공간 요구사항에 따라 다른 아키텍처를 가집니다. 한 예로 [타일 기반 렌더링(tiled rendering)](https.en.wikipedia.org/wiki/Tiled_rendering)이 있는데, 이는 프로그래머에게 해당 기능에 대한 더 많은 제어권을 제공함으로써 성능을 향상시킬 수 있습니다. 이러한 API들의 시대에서 비롯된 또 다른 한계는 제한적인 멀티스레딩 지원으로, 이는 CPU 측의 병목 현상을 유발할 수 있습니다. + +벌칸은 현대 그래픽 아키텍처를 위해 처음부터 새롭게 설계됨으로써 이러한 문제들을 해결합니다. 벌칸은 프로그래머가 더 상세한(verbose) API를 사용하여 자신의 의도를 명확하게 지정할 수 있게 함으로써 드라이버 오버헤드를 줄이고, 여러 스레드가 병렬로 커맨드를 생성하고 제출할 수 있도록 합니다. 또한 단일 컴파일러를 사용하는 표준화된 바이트코드 형식으로 전환하여 셰이더 컴파일의 불일치를 줄입니다. 마지막으로, 현대 그래픽 카드의 범용 처리 능력을 인정하여 그래픽과 컴퓨팅 기능을 단일 API로 통합합니다. + +## 삼각형 하나를 그리기까지 (Rust와 `ash` 버전) + +이제 잘 만들어진 벌칸 프로그램에서 삼각형 하나를 렌더링하는 데 필요한 모든 단계를 `ash` 라이브러리 관점에서 개괄적으로 살펴보겠습니다. 여기서 소개되는 모든 개념은 다음 장들에서 자세히 설명될 것입니다. + +### 1단계 - 엔트리, 인스턴스와 물리 장치 선택 + +Rust에서 `ash`를 사용한 벌칸 애플리케이션은 `ash::Entry`를 로드하는 것으로 시작합니다. 이는 벌칸 로더에 대한 진입점입니다. `Entry`를 통해 `ash::Instance`를 생성합니다. 인스턴스는 애플리케이션 정보와 사용할 API 확장을 기술하여 만듭니다. 인스턴스를 생성한 후, 벌칸을 지원하는 하드웨어(`vk::PhysicalDevice`)를 질의하고 연산에 사용할 하나 이상의 물리 장치를 선택할 수 있습니다. `instance.enumerate_physical_devices()`와 같은 메소드를 사용하여 장치 목록을 얻고, `instance.get_physical_device_properties()` 등으로 속성을 확인하여 원하는 장치를 선택합니다. + +### 2단계 - 논리 장치와 큐 패밀리 + +사용할 하드웨어 장치를 선택한 후에는 `ash::Device`(논리 장치)를 생성해야 합니다. 여기서는 멀티 뷰포트 렌더링이나 64비트 부동소수점 같은, 사용할 기능(`vk::PhysicalDeviceFeatures`)을 더 구체적으로 기술합니다. 또한 사용하고자 하는 큐 패밀리도 지정해야 합니다. 벌칸의 대부분 작업은 `vk::Queue`에 제출되어 비동기적으로 실행됩니다. 큐는 특정 작업 유형(그래픽, 컴퓨트 등)을 지원하는 큐 패밀리로부터 할당받습니다. `ash::Device`는 `instance.create_device()` 메소드를 통해 생성되며, 이후 대부분의 벌칸 객체 생성과 커맨드 호출은 이 `Device` 객체를 통해 이루어집니다. + +### 3단계 - 윈도우 서피스와 스왑 체인 + +오프스크린 렌더링이 아니라면 렌더링 결과를 표시할 윈도우가 필요합니다. Rust 생태계에서는 보통 `winit` 크레이트를 사용하여 윈도우를 생성합니다. + +윈도우에 렌더링하려면 윈도우 서피스(`vk::SurfaceKHR`)와 스왑 체인(`vk::SwapchainKHR`)이 필요합니다. `KHR` 접미사는 이것이 벌칸 확장 기능임을 의미합니다. `ash`에서는 `ash::extensions::khr::Surface`와 `ash::extensions::khr::Swapchain` 같은 확장 로더를 통해 관련 함수를 사용합니다. 서피스는 `ash_window` 크레이트를 사용하면 `winit` 윈도우로부터 쉽게 생성할 수 있습니다. + +스왑 체인은 화면에 표시될 이미지들의 집합입니다. 현재 렌더링 중인 이미지와 화면에 표시 중인 이미지를 분리하여, 찢어짐(tearing) 없이 완전한 이미지만 표시되도록 보장합니다. 매 프레임마다 스왑 체인에서 렌더링할 이미지를 받아오고, 렌더링이 끝나면 다시 스왑 체인에 반환하여 화면에 표시되도록 합니다. + +### 4단계 - 이미지 뷰와 프레임버퍼 + +스왑 체인에서 얻은 이미지(`vk::Image`)에 그리려면, 이를 `vk::ImageView`와 `vk::Framebuffer`로 감싸야 합니다. 이미지 뷰는 이미지의 특정 부분을 어떻게 사용할지 정의하고, 프레임버퍼는 렌더링 시 색상, 깊이, 스텐실 버퍼로 사용될 이미지 뷰들을 참조합니다. `device.create_image_view()`와 `device.create_framebuffer()` 메소드를 사용해 생성하며, 스왑 체인의 각 이미지마다 프레임버퍼를 미리 만들어 둡니다. + +### 5단계 - 렌더 패스 + +렌더 패스(`vk::RenderPass`)는 렌더링 작업 동안 사용될 첨부 파일(attachment)들의 형식과 사용 방법을 정의합니다. 예를 들어, "하나의 색상 첨부파일을 사용하며, 렌더링 시작 시 파란색으로 클리어한다"와 같이 설정합니다. `device.create_render_pass()`를 통해 생성합니다. + +### 6단계 - 그래픽 파이ప్라인 + +그래픽 파이프라인(`vk::Pipeline`)은 뷰포트 크기, 깊이 테스트 설정 등 그래픽 카드의 설정 가능한 상태와 셰이더(`vk::ShaderModule`)를 포함한 프로그래밍 가능한 상태를 모두 캡슐화한 객체입니다. `ash`에서는 `device.create_graphics_pipelines()` 메소드로 생성합니다. + +벌칸의 가장 큰 특징은 거의 모든 파이프라인 상태를 사전에 고정해야 한다는 점입니다. 셰이더를 바꾸거나 정점 데이터 형식을 조금이라도 변경하려면 파이프라인 객체 전체를 새로 만들어야 합니다. 따라서 애플리케이션에서 사용할 모든 상태 조합에 대해 미리 여러 파이프라인을 만들어두는 것이 일반적입니다. 이는 번거롭지만, 드라이버가 사전에 최적화를 수행할 수 있게 하여 런타임 성능을 더 예측 가능하게 만듭니다. + +### 7단계 - 커맨드 풀과 커맨드 버퍼 + +그리기와 같은 벌칸 작업은 큐에 제출하기 전에 `vk::CommandBuffer`에 기록되어야 합니다. 커맨드 버퍼는 특정 큐 패밀리와 연결된 `vk::CommandPool`에서 할당받습니다. 삼각형을 그리기 위해 커맨드 버퍼에 기록할 내용은 다음과 같습니다. + +* `device.cmd_begin_render_pass()` +* `device.cmd_bind_pipeline()` +* `device.cmd_draw()` +* `device.cmd_end_render_pass()` + +스왑 체인의 각 이미지에 대해 별도의 커맨드 버퍼를 미리 기록해두고, 렌더링 시점에 해당 프레임의 이미지에 맞는 커맨드 버퍼를 선택하여 제출하는 것이 효율적입니다. + +### 8단계 - 메인 루프 (이벤트 루프) + +메인 루프는 비교적 간단합니다. `swapchain_loader.acquire_next_image_khr()`로 스왑 체인에서 렌더링할 이미지를 얻고, 해당 이미지의 인덱스에 맞는 커맨드 버퍼를 `device.queue_submit()`으로 큐에 제출합니다. 작업이 완료되면 `swapchain_loader.queue_present_khr()`로 이미지를 화면에 표시하도록 요청합니다. + +이 모든 작업은 비동기이므로 세마포어(`vk::Semaphore`)와 펜스(`vk::Fence`) 같은 동기화 객체를 사용하여 실행 순서를 보장해야 합니다. 예를 들어, 이미지 획득이 끝나야 렌더링을 시작하고, 렌더링이 끝나야 화면에 표시하도록 순서를 제어해야 합니다. + +### 요약 + +이 간략한 여정은 `ash`를 사용하여 첫 번째 삼각형을 그리기 위해 앞으로 해야 할 일에 대한 기본적인 이해를 제공했을 것입니다. 실제 프로그램에는 정점 버퍼 할당, 유니폼 버퍼 생성 등이 추가되지만, 일단은 간단한 구조부터 시작하겠습니다. + +요약하자면, 첫 번째 삼각형을 그리기 위해 우리는 다음을 수행해야 합니다: + +* `ash::Entry` 로드 후 `ash::Instance` 생성 +* 지원되는 `vk::PhysicalDevice` 선택 +* `ash::Device`와 `vk::Queue` 생성 +* `winit` 등으로 윈도우 생성 후 서피스와 스왑 체인 생성 +* 스왑 체인 이미지들을 `vk::ImageView`로 감싸기 +* 렌더 타겟과 사용법을 명시하는 `vk::RenderPass` 생성 +* 렌더 패스를 위한 `vk::Framebuffer` 생성 +* `vk::Pipeline`(그래픽 파이프라인) 설정 +* 각 스왑 체인 이미지에 대한 `vk::CommandBuffer`를 할당하고 그리기 커맨드 기록 +* 메인 루프에서 이미지 획득, 커맨드 버퍼 제출, 화면 표시를 반복 + +## API 개념 (`ash` 중심) + +이 장은 `ash` 라이브러리 관점에서 벌칸 API가 어떻게 구조화되어 있는지 간략히 살펴보며 마무리합니다. + +### 코딩 규칙 + +벌칸의 모든 함수, 타입, 상수는 **`ash`** 크레이트를 통해 접근합니다. + +* 함수는 `Entry`, `Instance`, `Device` 객체의 **메소드**로 호출됩니다. (예: `device.create_buffer(...)`) +* 타입은 `ash::vk` 모듈 아래에 있습니다. (예: `ash::vk::BufferCreateInfo`, `ash::vk::SubmitInfo`) +* 상수 역시 `ash::vk` 모듈 아래에 대문자로 정의됩니다. (예: `ash::vk::Result::SUCCESS`) + +`ash`는 객체 생성 시 **빌더(builder) 패턴**을 광범위하게 사용합니다. 이는 C 스타일의 구조체 초기화보다 훨씬 안전하고 직관적입니다. + +```rust +use ash::vk; + +// 빌더 패턴을 사용하여 생성 정보 구조체 초기화 +let create_info = vk::FenceCreateInfo::builder() + .flags(vk::FenceCreateFlags::SIGNALED); + // .s_type은 빌더가 자동으로 설정 + // .p_next는 .push_next() 메소드로 설정 가능 + +let fence = unsafe { + device + .create_fence(&create_info, None) + .expect("펜스 생성 실패") +}; +``` + +C API와 달리, `ash`의 빌더는 `sType` 필드를 자동으로 채워줍니다. `pNext`를 이용한 확장 구조체 체인은 `.push_next()` 메소드를 사용하여 타입 안전하게 연결할 수 있습니다. 대부분의 `ash` 함수는 `Result`를 반환하므로, Rust의 `?` 연산자나 `match`를 사용한 에러 처리가 자연스럽습니다. + +객체 소멸 또한 `device.destroy_fence(fence, None)`와 같이 명시적인 메소드 호출을 통해 이루어집니다. `ash` 핸들 자체는 `Drop`을 구현하지 않으므로, 자원 누수를 막기 위해 직접 해제 코드를 호출해야 합니다. (또는 `ash-rs` 커뮤니티의 래퍼 라이브러리를 사용할 수 있습니다.) + +### 유효성 검사 레이어 + +벌칸은 낮은 오버헤드를 위해 기본적으로 오류 검사를 거의 하지 않습니다. 따라서 개발 중에는 **유효성 검사 레이어(validation layers)**를 활성화하는 것이 필수적입니다. 이 레이어들은 API와 드라이버 사이에서 파라미터 유효성 검사, 메모리 관리 추적 등 다양한 디버깅 정보를 제공합니다. + +`ash`에서는 `Instance`를 생성할 때 활성화할 레이어의 이름을 문자열 슬라이스로 전달하여 간단히 활성화할 수 있습니다. 레이어가 보내는 디버그 메시지를 수신하기 위한 콜백 함수 또한 `Instance` 생성 시 등록합니다. 벌칸의 명시적인 API와 상세한 유효성 검사 레이어 덕분에, 문제가 발생했을 때 원인을 찾는 것이 OpenGL이나 Direct3D보다 오히려 쉬울 수 있습니다. + +코드를 작성하기 전까지 이제 단 한 단계만 남았습니다. 바로 [개발 환경 설정하기](!en/Development_environment)입니다. \ No newline at end of file diff --git a/ko-rust/02_Development_environment.md b/ko-rust/02_Development_environment.md new file mode 100644 index 00000000..0c096153 --- /dev/null +++ b/ko-rust/02_Development_environment.md @@ -0,0 +1,206 @@ +이 챕터에서는 Rust와 `ash`를 사용한 Vulkan 애플리케이션 개발 환경을 설정하고 몇 가지 유용한 라이브러리를 설치합니다. Rust의 빌드 시스템인 Cargo 덕분에 대부분의 설정은 플랫폼에 상관없이 동일하지만, Vulkan SDK 자체의 설치는 운영체제별로 다르기 때문에 여기서는 각각 따로 설명합니다. + +## 공통 설정: Rust 및 Cargo 프로젝트 + +운영체제별 설정을 진행하기 전에, 모든 플랫폼에서 공통으로 필요한 Rust 개발 환경을 먼저 구성합니다. + +### Rust 설치 + +아직 Rust가 설치되지 않았다면, [공식 웹사이트](https://rustup.rs/)의 안내에 따라 `rustup`을 설치하세요. `rustup`은 Rust 컴파일러(`rustc`)와 패키지 매니저 겸 빌드 시스템인 `cargo`를 관리해주는 도구입니다. + +### Cargo 프로젝트 생성 및 의존성 추가 + +먼저, 프로젝트를 위한 새 디렉터리를 만들고 그 안에서 Cargo 프로젝트를 시작합니다. + +```bash +mkdir vulkan-test-rs +cd vulkan-test-rs +cargo new . +``` + +이제 `Cargo.toml` 파일을 열어 필요한 라이브러리(크레이트, crate)들을 `[dependencies]` 섹션에 추가합니다. + +* **ash**: Vulkan API에 대한 저수준의 안전하지 않은 Rust 바인딩을 제공합니다. +* **winit**: 창 생성 및 이벤트 처리를 위한 크로스플랫폼 라이브러리입니다. C++의 GLFW 역할을 합니다. +* **glam**: Rust를 위한 간단하고 빠른 선형대수 라이브러리입니다. C++의 GLM 역할을 합니다. +* **raw-window-handle**: `winit` 창과 Vulkan을 연결하기 위해 필요한 플랫폼별 창 핸들 정보를 제공합니다. + +`Cargo.toml` 파일은 다음과 같이 보일 것입니다: + +```toml +[package] +name = "vulkan-test-rs" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ash = "0.37" +winit = "0.28" +glam = "0.24" +raw-window-handle = "0.5" +``` +*(참고: 위 버전 번호는 작성 시점의 최신 버전일 수 있습니다. 실제 프로젝트에서는 최신 버전을 확인하고 사용하세요.)* + +이제 각 운영체제에 맞는 Vulkan SDK 설치를 진행합니다. + +## Windows + +Windows에서 개발하신다면, 먼저 Vulkan SDK를 설치해야 합니다. + +### Vulkan SDK + +Vulkan 애플리케이션 개발에 필요한 가장 중요한 구성 요소는 SDK입니다. SDK에는 헤더, 표준 유효성 검사 레이어, 디버깅 도구, 그리고 Vulkan 함수를 위한 로더(`vulkan-1.dll`)가 포함되어 있습니다. + +SDK는 [LunarG 웹사이트](https://vulkan.lunarg.com/) 페이지 하단의 버튼을 통해 다운로드할 수 있습니다. + +![](/images/vulkan_sdk_download_buttons.png) + +설치를 진행하고 SDK가 설치된 위치를 잘 기억해두세요. 설치가 완료되면 그래픽 카드와 드라이버가 Vulkan을 제대로 지원하는지 확인합니다. SDK 설치 경로의 `Bin` 디렉터리로 이동하여 `vkcube.exe` 데모를 실행하세요. 다음과 같은 화면이 나타나야 합니다: + +![](/images/cube_demo.png) + +만약 오류 메시지가 표시된다면 드라이버가 최신 버전인지, Vulkan 런타임을 포함하고 있는지, 그리고 그래픽 카드가 지원되는 모델인지 확인하세요. + +### 프로젝트 테스트 + +이제 모든 준비가 끝났습니다. `src/main.rs` 파일을 열고 모든 내용을 아래 코드로 교체하세요. 이 코드는 `winit`으로 창을 만들고, `ash`를 통해 사용 가능한 Vulkan 확장 기능의 개수를 출력합니다. + +```rust +use ash::vk; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; +use glam::{Mat4, Vec4}; + +fn main() { + // winit으로 이벤트 루프와 창 생성 + let event_loop = EventLoop::new(); + let window = WindowBuilder::new() + .with_title("Vulkan Window (Rust)") + .with_inner_size(winit::dpi::LogicalSize::new(800, 600)) + .build(&event_loop) + .expect("Failed to create window."); + + // ash로 Vulkan 로더 진입점 확보 + // unsafe: Vulkan 라이브러리를 로드하는 것은 외부 C 라이브러리와 상호작용하므로 unsafe합니다. + let entry = unsafe { ash::Entry::load() }.expect("Failed to load Vulkan loader"); + + // 사용 가능한 인스턴스 확장 기능 열거 및 개수 출력 + let extension_properties = entry + .enumerate_instance_extension_properties() + .expect("Failed to enumerate instance extension properties."); + println!("{} extensions supported", extension_properties.len()); + + // glam 라이브러리 테스트 + let matrix = Mat4::IDENTITY; + let vec = Vec4::ONE; + let _test = matrix * vec; + + // 이벤트 루프 실행 + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + *control_flow = ControlFlow::Exit; + } + _ => (), + } + }); +} +``` + +이제 터미널에서 `cargo run` 명령을 실행하여 프로젝트를 컴파일하고 실행합니다. + +```bash +cargo run +``` + +다음과 같이 터미널에 확장 기능 개수가 출력되고 빈 창이 나타나면 성공입니다. + +![](/images/vs_test_window.png) + +축하합니다, 이제 Rust로 Vulkan을 탐험할 모든 준비가 끝났습니다! + +## Linux + +이 설명은 Ubuntu, Fedora, Arch Linux 사용자를 대상으로 하지만, 자신의 배포판에 맞는 패키지 매니저 명령어로 변경하여 따라 할 수 있습니다. + +### Vulkan 패키지 + +Linux에서 Vulkan 개발에 필요한 주요 구성 요소는 Vulkan 로더, 유효성 검사 레이어, 그리고 Vulkan 지원 여부를 테스트할 커맨드 라인 유틸리티입니다. + +* `sudo apt install vulkan-tools libvulkan-dev` 또는 `sudo dnf install vulkan-tools vulkan-loader-devel`: `vkcube`와 같은 유틸리티와 Vulkan 로더 및 개발 파일을 설치합니다. +* `sudo apt install vulkan-validationlayers-dev spirv-tools` 또는 `sudo dnf install vulkan-validation-layers-devel`: 표준 유효성 검사 레이어와 SPIR-V 도구를 설치합니다. +* Arch Linux: `sudo pacman -S vulkan-devel`로 필요한 모든 도구를 설치할 수 있습니다. + +`winit`이 창을 생성하기 위해 필요한 시스템 라이브러리도 설치해야 합니다. Ubuntu/Debian 기준으로는 다음과 같습니다. + +```bash +sudo apt install libx11-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-xfixes0-dev +``` + +설치가 성공적으로 완료되었다면, `vkcube`를 실행하여 다음과 같은 창이 나타나는지 확인하세요: + +![](/images/cube_demo_nowindow.png) + +### 프로젝트 테스트 + +이제 공통 설정 섹션에서 작성한 Rust 프로젝트를 테스트할 차례입니다. `src/main.rs` 파일을 위 Windows 섹션의 예제 코드로 채우세요. + +그리고 터미널에서 `cargo run`을 실행합니다. + +```bash +cargo run +``` + +터미널에 확장 기능 개수가 출력되고 빈 창이 나타나면 성공입니다. 이제 Rust로 Vulkan을 탐험할 모든 준비가 끝났습니다! + +## MacOS + +이 설명은 Homebrew 패키지 매니저를 사용한다고 가정합니다. 또한, 최소 MacOS 버전 10.11이 필요하며, 기기가 [Metal API](https://en.wikipedia.org/wiki/Metal_(API)#Supported_GPUs)를 지원해야 합니다. + +### Vulkan SDK + +MacOS는 Vulkan을 네이티브로 지원하지 않으므로, LunarG의 SDK는 [MoltenVK](https://moltengl.com/)를 사용하여 Vulkan API 호출을 Apple의 Metal API 호출로 변환합니다. + +[LunarG 웹사이트](https://vulkan.lunarg.com/)에서 SDK를 다운로드하여 원하는 위치에 압축을 해제하세요. + +![](/images/vulkan_sdk_download_buttons.png) + +압축 해제한 폴더의 `Applications` 디렉터리에서 `vkcube`를 실행하여 다음과 같은 화면이 나타나는지 확인하세요: + +![](/images/cube_demo_mac.png) + +### 환경 변수 설정 + +MoltenVK가 올바르게 동작하려면, Vulkan 로더가 필요한 파일을 찾을 수 있도록 몇 가지 환경 변수를 설정해야 합니다. 터미널에서 `cargo run`을 실행하기 전에 다음 변수들을 설정하거나, 셸 설정 파일(`.zshrc`, `.bash_profile` 등)에 추가할 수 있습니다. `path/to/your/vulkansdk` 부분은 실제 SDK 압축을 해제한 경로로 변경해야 합니다. + +```bash +export VK_ICD_FILENAMES=path/to/your/vulkansdk/macOS/share/vulkan/icd.d/MoltenVK_icd.json +export VK_LAYER_PATH=path/to/your/vulkansdk/macOS/share/vulkan/explicit_layer.d +``` + +### 프로젝트 테스트 + +이제 공통 설정 섹션에서 작성한 Rust 프로젝트를 테스트할 차례입니다. `src/main.rs` 파일을 위 Windows 섹션의 예제 코드로 채우세요. + +그리고 위에서 설명한 환경 변수를 설정한 터미널에서 `cargo run`을 실행합니다. + +```bash +# 환경 변수를 현재 세션에만 적용하여 실행하는 예시 +VK_ICD_FILENAMES=path/to/your/vulkansdk/macOS/share/vulkan/icd.d/MoltenVK_icd.json \ +VK_LAYER_PATH=path/to/your/vulkansdk/macOS/share/vulkan/explicit_layer.d \ +cargo run +``` + +터미널에 확장 기능 개수가 출력되고 빈 창이 나타나면 성공입니다. `ash`와 MoltenVK가 성공적으로 연동된 것입니다. + +축하합니다, 이제 Rust로 Vulkan을 탐험할 모든 준비가 끝났습니다 \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/00_Setup/00_Base_code.md b/ko-rust/03_Drawing_a_triangle/00_Setup/00_Base_code.md new file mode 100644 index 00000000..df26c6ac --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/00_Base_code.md @@ -0,0 +1,217 @@ +## General structure + +In the previous chapter you've created a Vulkan project with all of the proper +configuration and tested it with the sample code. In this chapter we're starting +from scratch with the following code: + +```c++ +#include + +#include +#include +#include + +class HelloTriangleApplication { +public: + void run() { + initVulkan(); + mainLoop(); + cleanup(); + } + +private: + void initVulkan() { + + } + + void mainLoop() { + + } + + void cleanup() { + + } +}; + +int main() { + HelloTriangleApplication app; + + try { + app.run(); + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} +``` + +We first include the Vulkan header from the LunarG SDK, which provides the +functions, structures and enumerations. The `stdexcept` and `iostream` headers +are included for reporting and propagating errors. The `cstdlib` +header provides the `EXIT_SUCCESS` and `EXIT_FAILURE` macros. + +The program itself is wrapped into a class where we'll store the Vulkan objects +as private class members and add functions to initiate each of them, which will +be called from the `initVulkan` function. Once everything has been prepared, we +enter the main loop to start rendering frames. We'll fill in the `mainLoop` +function to include a loop that iterates until the window is closed in a moment. +Once the window is closed and `mainLoop` returns, we'll make sure to deallocate +the resources we've used in the `cleanup` function. + +If any kind of fatal error occurs during execution then we'll throw a +`std::runtime_error` exception with a descriptive message, which will propagate +back to the `main` function and be printed to the command prompt. To handle +a variety of standard exception types as well, we catch the more general `std::exception`. One example of an error that we will deal with soon is finding +out that a certain required extension is not supported. + +Roughly every chapter that follows after this one will add one new function that +will be called from `initVulkan` and one or more new Vulkan objects to the +private class members that need to be freed at the end in `cleanup`. + +## Resource management + +Just like each chunk of memory allocated with `malloc` requires a call to +`free`, every Vulkan object that we create needs to be explicitly destroyed when +we no longer need it. In C++ it is possible to perform automatic resource +management using [RAII](https://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization) +or smart pointers provided in the `` header. However, I've chosen to be +explicit about allocation and deallocation of Vulkan objects in this tutorial. +After all, Vulkan's niche is to be explicit about every operation to avoid +mistakes, so it's good to be explicit about the lifetime of objects to learn how +the API works. + +After following this tutorial, you could implement automatic resource management +by writing C++ classes that acquire Vulkan objects in their constructor and +release them in their destructor, or by providing a custom deleter to either +`std::unique_ptr` or `std::shared_ptr`, depending on your ownership requirements. +RAII is the recommended model for larger Vulkan programs, but +for learning purposes it's always good to know what's going on behind the +scenes. + +Vulkan objects are either created directly with functions like `vkCreateXXX`, or +allocated through another object with functions like `vkAllocateXXX`. After +making sure that an object is no longer used anywhere, you need to destroy it +with the counterparts `vkDestroyXXX` and `vkFreeXXX`. The parameters for these +functions generally vary for different types of objects, but there is one +parameter that they all share: `pAllocator`. This is an optional parameter that +allows you to specify callbacks for a custom memory allocator. We will ignore +this parameter in the tutorial and always pass `nullptr` as argument. + +## Integrating GLFW + +Vulkan works perfectly fine without creating a window if you want to use it for +off-screen rendering, but it's a lot more exciting to actually show something! +First replace the `#include ` line with + +```c++ +#define GLFW_INCLUDE_VULKAN +#include +``` + +That way GLFW will include its own definitions and automatically load the Vulkan +header with it. Add a `initWindow` function and add a call to it from the `run` +function before the other calls. We'll use that function to initialize GLFW and +create a window. + +```c++ +void run() { + initWindow(); + initVulkan(); + mainLoop(); + cleanup(); +} + +private: + void initWindow() { + + } +``` + +The very first call in `initWindow` should be `glfwInit()`, which initializes +the GLFW library. Because GLFW was originally designed to create an OpenGL +context, we need to tell it to not create an OpenGL context with a subsequent +call: + +```c++ +glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); +``` + +Because handling resized windows takes special care that we'll look into later, +disable it for now with another window hint call: + +```c++ +glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); +``` + +All that's left now is creating the actual window. Add a `GLFWwindow* window;` +private class member to store a reference to it and initialize the window with: + +```c++ +window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr); +``` + +The first three parameters specify the width, height and title of the window. +The fourth parameter allows you to optionally specify a monitor to open the +window on and the last parameter is only relevant to OpenGL. + +It's a good idea to use constants instead of hardcoded width and height numbers +because we'll be referring to these values a couple of times in the future. I've +added the following lines above the `HelloTriangleApplication` class definition: + +```c++ +const uint32_t WIDTH = 800; +const uint32_t HEIGHT = 600; +``` + +and replaced the window creation call with + +```c++ +window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); +``` + +You should now have a `initWindow` function that looks like this: + +```c++ +void initWindow() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + + window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); +} +``` + +To keep the application running until either an error occurs or the window is +closed, we need to add an event loop to the `mainLoop` function as follows: + +```c++ +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + } +} +``` + +This code should be fairly self-explanatory. It loops and checks for events like +pressing the X button until the window has been closed by the user. This is also +the loop where we'll later call a function to render a single frame. + +Once the window is closed, we need to clean up resources by destroying it and +terminating GLFW itself. This will be our first `cleanup` code: + +```c++ +void cleanup() { + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +When you run the program now you should see a window titled `Vulkan` show up +until the application is terminated by closing the window. Now that we have the +skeleton for the Vulkan application, let's [create the first Vulkan object](!en/Drawing_a_triangle/Setup/Instance)! + +[C++ code](/code/00_base_code.cpp) diff --git a/ko-rust/03_Drawing_a_triangle/00_Setup/01_Instance.md b/ko-rust/03_Drawing_a_triangle/00_Setup/01_Instance.md new file mode 100644 index 00000000..d9744a1c --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/01_Instance.md @@ -0,0 +1,221 @@ +## Creating an instance + +The very first thing you need to do is initialize the Vulkan library by creating +an *instance*. The instance is the connection between your application and the +Vulkan library and creating it involves specifying some details about your +application to the driver. + +Start by adding a `createInstance` function and invoking it in the +`initVulkan` function. + +```c++ +void initVulkan() { + createInstance(); +} +``` + +Additionally add a data member to hold the handle to the instance: + +```c++ +private: +VkInstance instance; +``` + +Now, to create an instance we'll first have to fill in a struct with some +information about our application. This data is technically optional, but it may +provide some useful information to the driver in order to optimize our specific +application (e.g. because it uses a well-known graphics engine with +certain special behavior). This struct is called `VkApplicationInfo`: + +```c++ +void createInstance() { + VkApplicationInfo appInfo{}; + appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + appInfo.pApplicationName = "Hello Triangle"; + appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.pEngineName = "No Engine"; + appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.apiVersion = VK_API_VERSION_1_0; +} +``` + +As mentioned before, many structs in Vulkan require you to explicitly specify +the type in the `sType` member. This is also one of the many structs with a +`pNext` member that can point to extension information in the future. We're +using value initialization here to leave it as `nullptr`. + +A lot of information in Vulkan is passed through structs instead of function +parameters and we'll have to fill in one more struct to provide sufficient +information for creating an instance. This next struct is not optional and tells +the Vulkan driver which global extensions and validation layers we want to use. +Global here means that they apply to the entire program and not a specific +device, which will become clear in the next few chapters. + +```c++ +VkInstanceCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; +createInfo.pApplicationInfo = &appInfo; +``` + +The first two parameters are straightforward. The next two layers specify the +desired global extensions. As mentioned in the overview chapter, Vulkan is a +platform agnostic API, which means that you need an extension to interface with +the window system. GLFW has a handy built-in function that returns the +extension(s) it needs to do that which we can pass to the struct: + +```c++ +uint32_t glfwExtensionCount = 0; +const char** glfwExtensions; + +glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + +createInfo.enabledExtensionCount = glfwExtensionCount; +createInfo.ppEnabledExtensionNames = glfwExtensions; +``` + +The last two members of the struct determine the global validation layers to +enable. We'll talk about these more in-depth in the next chapter, so just leave +these empty for now. + +```c++ +createInfo.enabledLayerCount = 0; +``` + +We've now specified everything Vulkan needs to create an instance and we can +finally issue the `vkCreateInstance` call: + +```c++ +VkResult result = vkCreateInstance(&createInfo, nullptr, &instance); +``` + +As you'll see, the general pattern that object creation function parameters in +Vulkan follow is: + +* Pointer to struct with creation info +* Pointer to custom allocator callbacks, always `nullptr` in this tutorial +* Pointer to the variable that stores the handle to the new object + +If everything went well then the handle to the instance was stored in the +`VkInstance` class member. Nearly all Vulkan functions return a value of type +`VkResult` that is either `VK_SUCCESS` or an error code. To check if the +instance was created successfully, we don't need to store the result and can +just use a check for the success value instead: + +```c++ +if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); +} +``` + +Now run the program to make sure that the instance is created successfully. + +## Encountered VK_ERROR_INCOMPATIBLE_DRIVER: +If using MacOS with the latest MoltenVK sdk, you may get `VK_ERROR_INCOMPATIBLE_DRIVER` +returned from `vkCreateInstance`. According to the [Getting Start Notes](https://vulkan.lunarg.com/doc/sdk/1.3.216.0/mac/getting_started.html). Beginning with the 1.3.216 Vulkan SDK, the `VK_KHR_PORTABILITY_subset` +extension is mandatory. + +To get over this error, first add the `VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR` bit +to `VkInstanceCreateInfo` struct's flags, then add `VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME` +to instance enabled extension list. + +Typically the code could be like this: +```c++ +... + +std::vector requiredExtensions; + +for(uint32_t i = 0; i < glfwExtensionCount; i++) { + requiredExtensions.emplace_back(glfwExtensions[i]); +} + +requiredExtensions.emplace_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + +createInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + +createInfo.enabledExtensionCount = (uint32_t) requiredExtensions.size(); +createInfo.ppEnabledExtensionNames = requiredExtensions.data(); + +if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); +} +``` + +## Checking for extension support + +If you look at the `vkCreateInstance` documentation then you'll see that one of +the possible error codes is `VK_ERROR_EXTENSION_NOT_PRESENT`. We could simply +specify the extensions we require and terminate if that error code comes back. +That makes sense for essential extensions like the window system interface, but +what if we want to check for optional functionality? + +To retrieve a list of supported extensions before creating an instance, there's +the `vkEnumerateInstanceExtensionProperties` function. It takes a pointer to a +variable that stores the number of extensions and an array of +`VkExtensionProperties` to store details of the extensions. It also takes an +optional first parameter that allows us to filter extensions by a specific +validation layer, which we'll ignore for now. + +To allocate an array to hold the extension details we first need to know how +many there are. You can request just the number of extensions by leaving the +latter parameter empty: + +```c++ +uint32_t extensionCount = 0; +vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); +``` + +Now allocate an array to hold the extension details (`include `): + +```c++ +std::vector extensions(extensionCount); +``` + +Finally we can query the extension details: + +```c++ +vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data()); +``` + +Each `VkExtensionProperties` struct contains the name and version of an +extension. We can list them with a simple for loop (`\t` is a tab for +indentation): + +```c++ +std::cout << "available extensions:\n"; + +for (const auto& extension : extensions) { + std::cout << '\t' << extension.extensionName << '\n'; +} +``` + +You can add this code to the `createInstance` function if you'd like to provide +some details about the Vulkan support. As a challenge, try to create a function +that checks if all of the extensions returned by +`glfwGetRequiredInstanceExtensions` are included in the supported extensions +list. + +## Cleaning up + +The `VkInstance` should be only destroyed right before the program exits. It can +be destroyed in `cleanup` with the `vkDestroyInstance` function: + +```c++ +void cleanup() { + vkDestroyInstance(instance, nullptr); + + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +The parameters for the `vkDestroyInstance` function are straightforward. As +mentioned in the previous chapter, the allocation and deallocation functions +in Vulkan have an optional allocator callback that we'll ignore by passing +`nullptr` to it. All of the other Vulkan resources that we'll create in the +following chapters should be cleaned up before the instance is destroyed. + +Before continuing with the more complex steps after instance creation, it's time +to evaluate our debugging options by checking out [validation layers](!en/Drawing_a_triangle/Setup/Validation_layers). + +[C++ code](/code/01_instance_creation.cpp) diff --git a/ko-rust/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md b/ko-rust/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md new file mode 100644 index 00000000..569a0178 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md @@ -0,0 +1,458 @@ +## What are validation layers? + +The Vulkan API is designed around the idea of minimal driver overhead and one of +the manifestations of that goal is that there is very limited error checking in +the API by default. Even mistakes as simple as setting enumerations to incorrect +values or passing null pointers to required parameters are generally not +explicitly handled and will simply result in crashes or undefined behavior. +Because Vulkan requires you to be very explicit about everything you're doing, +it's easy to make many small mistakes like using a new GPU feature and +forgetting to request it at logical device creation time. + +However, that doesn't mean that these checks can't be added to the API. Vulkan +introduces an elegant system for this known as *validation layers*. Validation +layers are optional components that hook into Vulkan function calls to apply +additional operations. Common operations in validation layers are: + +* Checking the values of parameters against the specification to detect misuse +* Tracking creation and destruction of objects to find resource leaks +* Checking thread safety by tracking the threads that calls originate from +* Logging every call and its parameters to the standard output +* Tracing Vulkan calls for profiling and replaying + +Here's an example of what the implementation of a function in a diagnostics +validation layer could look like: + +```c++ +VkResult vkCreateInstance( + const VkInstanceCreateInfo* pCreateInfo, + const VkAllocationCallbacks* pAllocator, + VkInstance* instance) { + + if (pCreateInfo == nullptr || instance == nullptr) { + log("Null pointer passed to required parameter!"); + return VK_ERROR_INITIALIZATION_FAILED; + } + + return real_vkCreateInstance(pCreateInfo, pAllocator, instance); +} +``` + +These validation layers can be freely stacked to include all the debugging +functionality that you're interested in. You can simply enable validation layers +for debug builds and completely disable them for release builds, which gives you +the best of both worlds! + +Vulkan does not come with any validation layers built-in, but the LunarG Vulkan +SDK provides a nice set of layers that check for common errors. They're also +completely [open source](https://github.com/KhronosGroup/Vulkan-ValidationLayers), +so you can check which kind of mistakes they check for and contribute. Using the +validation layers is the best way to avoid your application breaking on +different drivers by accidentally relying on undefined behavior. + +Validation layers can only be used if they have been installed onto the system. +For example, the LunarG validation layers are only available on PCs with the +Vulkan SDK installed. + +There were formerly two different types of validation layers in Vulkan: instance +and device specific. The idea was that instance layers would only check +calls related to global Vulkan objects like instances, and device specific layers +would only check calls related to a specific GPU. Device specific layers have now been +deprecated, which means that instance validation layers apply to all Vulkan +calls. The specification document still recommends that you enable validation +layers at device level as well for compatibility, which is required by some +implementations. We'll simply specify the same layers as the instance at logical +device level, which we'll see [later on](!en/Drawing_a_triangle/Setup/Logical_device_and_queues). + +## Using validation layers + +In this section we'll see how to enable the standard diagnostics layers provided +by the Vulkan SDK. Just like extensions, validation layers need to be enabled by +specifying their name. All of the useful standard validation is bundled into a layer included in the SDK that is known as `VK_LAYER_KHRONOS_validation`. + +Let's first add two configuration variables to the program to specify the layers +to enable and whether to enable them or not. I've chosen to base that value on +whether the program is being compiled in debug mode or not. The `NDEBUG` macro +is part of the C++ standard and means "not debug". + +```c++ +const uint32_t WIDTH = 800; +const uint32_t HEIGHT = 600; + +const std::vector validationLayers = { + "VK_LAYER_KHRONOS_validation" +}; + +#ifdef NDEBUG + const bool enableValidationLayers = false; +#else + const bool enableValidationLayers = true; +#endif +``` + +We'll add a new function `checkValidationLayerSupport` that checks if all of +the requested layers are available. First list all of the available layers +using the `vkEnumerateInstanceLayerProperties` function. Its usage is identical +to that of `vkEnumerateInstanceExtensionProperties` which was discussed in the +instance creation chapter. + +```c++ +bool checkValidationLayerSupport() { + uint32_t layerCount; + vkEnumerateInstanceLayerProperties(&layerCount, nullptr); + + std::vector availableLayers(layerCount); + vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data()); + + return false; +} +``` + +Next, check if all of the layers in `validationLayers` exist in the +`availableLayers` list. You may need to include `` for `strcmp`. + +```c++ +for (const char* layerName : validationLayers) { + bool layerFound = false; + + for (const auto& layerProperties : availableLayers) { + if (strcmp(layerName, layerProperties.layerName) == 0) { + layerFound = true; + break; + } + } + + if (!layerFound) { + return false; + } +} + +return true; +``` + +We can now use this function in `createInstance`: + +```c++ +void createInstance() { + if (enableValidationLayers && !checkValidationLayerSupport()) { + throw std::runtime_error("validation layers requested, but not available!"); + } + + ... +} +``` + +Now run the program in debug mode and ensure that the error does not occur. If +it does, then have a look at the FAQ. + +Finally, modify the `VkInstanceCreateInfo` struct instantiation to include the +validation layer names if they are enabled: + +```c++ +if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); +} else { + createInfo.enabledLayerCount = 0; +} +``` + +If the check was successful then `vkCreateInstance` should not ever return a +`VK_ERROR_LAYER_NOT_PRESENT` error, but you should run the program to make sure. + +## Message callback + +The validation layers will print debug messages to the standard output by default, but we can also handle them ourselves by providing an explicit callback in our program. This will also allow you to decide which kind of messages you would like to see, because not all are necessarily (fatal) errors. If you don't want to do that right now then you may skip to the last section in this chapter. + +To set up a callback in the program to handle messages and the associated details, we have to set up a debug messenger with a callback using the `VK_EXT_debug_utils` extension. + +We'll first create a `getRequiredExtensions` function that will return the +required list of extensions based on whether validation layers are enabled or +not: + +```c++ +std::vector getRequiredExtensions() { + uint32_t glfwExtensionCount = 0; + const char** glfwExtensions; + glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + + std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); + + if (enableValidationLayers) { + extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + } + + return extensions; +} +``` + +The extensions specified by GLFW are always required, but the debug messenger +extension is conditionally added. Note that I've used the +`VK_EXT_DEBUG_UTILS_EXTENSION_NAME` macro here which is equal to the literal +string "VK_EXT_debug_utils". Using this macro lets you avoid typos. + +We can now use this function in `createInstance`: + +```c++ +auto extensions = getRequiredExtensions(); +createInfo.enabledExtensionCount = static_cast(extensions.size()); +createInfo.ppEnabledExtensionNames = extensions.data(); +``` + +Run the program to make sure you don't receive a +`VK_ERROR_EXTENSION_NOT_PRESENT` error. We don't really need to check for the +existence of this extension, because it should be implied by the availability of +the validation layers. + +Now let's see what a debug callback function looks like. Add a new static member +function called `debugCallback` with the `PFN_vkDebugUtilsMessengerCallbackEXT` +prototype. The `VKAPI_ATTR` and `VKAPI_CALL` ensure that the function has the +right signature for Vulkan to call it. + +```c++ +static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( + VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + VkDebugUtilsMessageTypeFlagsEXT messageType, + const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, + void* pUserData) { + + std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl; + + return VK_FALSE; +} +``` + +The first parameter specifies the severity of the message, which is one of the following flags: + +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT`: Diagnostic message +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT`: Informational message like the creation of a resource +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT`: Message about behavior that is not necessarily an error, but very likely a bug in your application +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT`: Message about behavior that is invalid and may cause crashes + +The values of this enumeration are set up in such a way that you can use a comparison operation to check if a message is equal or worse compared to some level of severity, for example: + +```c++ +if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + // Message is important enough to show +} +``` + +The `messageType` parameter can have the following values: + +* `VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT`: Some event has happened that is unrelated to the specification or performance +* `VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT`: Something has happened that violates the specification or indicates a possible mistake +* `VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT`: Potential non-optimal use of Vulkan + +The `pCallbackData` parameter refers to a `VkDebugUtilsMessengerCallbackDataEXT` struct containing the details of the message itself, with the most important members being: + +* `pMessage`: The debug message as a null-terminated string +* `pObjects`: Array of Vulkan object handles related to the message +* `objectCount`: Number of objects in array + +Finally, the `pUserData` parameter contains a pointer that was specified during the setup of the callback and allows you to pass your own data to it. + +The callback returns a boolean that indicates if the Vulkan call that triggered +the validation layer message should be aborted. If the callback returns true, +then the call is aborted with the `VK_ERROR_VALIDATION_FAILED_EXT` error. This +is normally only used to test the validation layers themselves, so you should +always return `VK_FALSE`. + +All that remains now is telling Vulkan about the callback function. Perhaps +somewhat surprisingly, even the debug callback in Vulkan is managed with a +handle that needs to be explicitly created and destroyed. Such a callback is part of a *debug messenger* and you can have as many of them as you want. Add a class member for +this handle right under `instance`: + +```c++ +VkDebugUtilsMessengerEXT debugMessenger; +``` + +Now add a function `setupDebugMessenger` to be called from `initVulkan` right +after `createInstance`: + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); +} + +void setupDebugMessenger() { + if (!enableValidationLayers) return; + +} +``` + +We'll need to fill in a structure with details about the messenger and its callback: + +```c++ +VkDebugUtilsMessengerCreateInfoEXT createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; +createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; +createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; +createInfo.pfnUserCallback = debugCallback; +createInfo.pUserData = nullptr; // Optional +``` + +The `messageSeverity` field allows you to specify all the types of severities you would like your callback to be called for. I've specified all types except for `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT` here to receive notifications about possible problems while leaving out verbose general debug info. + +Similarly the `messageType` field lets you filter which types of messages your callback is notified about. I've simply enabled all types here. You can always disable some if they're not useful to you. + +Finally, the `pfnUserCallback` field specifies the pointer to the callback function. You can optionally pass a pointer to the `pUserData` field which will be passed along to the callback function via the `pUserData` parameter. You could use this to pass a pointer to the `HelloTriangleApplication` class, for example. + +Note that there are many more ways to configure validation layer messages and debug callbacks, but this is a good setup to get started with for this tutorial. See the [extension specification](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap50.html#VK_EXT_debug_utils) for more info about the possibilities. + +This struct should be passed to the `vkCreateDebugUtilsMessengerEXT` function to +create the `VkDebugUtilsMessengerEXT` object. Unfortunately, because this +function is an extension function, it is not automatically loaded. We have to +look up its address ourselves using `vkGetInstanceProcAddr`. We're going to +create our own proxy function that handles this in the background. I've added it +right above the `HelloTriangleApplication` class definition. + +```c++ +VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) { + auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT"); + if (func != nullptr) { + return func(instance, pCreateInfo, pAllocator, pDebugMessenger); + } else { + return VK_ERROR_EXTENSION_NOT_PRESENT; + } +} +``` + +The `vkGetInstanceProcAddr` function will return `nullptr` if the function +couldn't be loaded. We can now call this function to create the extension +object if it's available: + +```c++ +if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { + throw std::runtime_error("failed to set up debug messenger!"); +} +``` + +The second to last parameter is again the optional allocator callback that we +set to `nullptr`, other than that the parameters are fairly straightforward. +Since the debug messenger is specific to our Vulkan instance and its layers, it +needs to be explicitly specified as first argument. You will also see this +pattern with other *child* objects later on. + +The `VkDebugUtilsMessengerEXT` object also needs to be cleaned up with a call to +`vkDestroyDebugUtilsMessengerEXT`. Similarly to `vkCreateDebugUtilsMessengerEXT` +the function needs to be explicitly loaded. + +Create another proxy function right below `CreateDebugUtilsMessengerEXT`: + +```c++ +void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) { + auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT"); + if (func != nullptr) { + func(instance, debugMessenger, pAllocator); + } +} +``` + +Make sure that this function is either a static class function or a function +outside the class. We can then call it in the `cleanup` function: + +```c++ +void cleanup() { + if (enableValidationLayers) { + DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr); + } + + vkDestroyInstance(instance, nullptr); + + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +## Debugging instance creation and destruction + +Although we've now added debugging with validation layers to the program we're not covering everything quite yet. The `vkCreateDebugUtilsMessengerEXT` call requires a valid instance to have been created and `vkDestroyDebugUtilsMessengerEXT` must be called before the instance is destroyed. This currently leaves us unable to debug any issues in the `vkCreateInstance` and `vkDestroyInstance` calls. + +However, if you closely read the [extension documentation](https://github.com/KhronosGroup/Vulkan-Docs/blob/main/appendices/VK_EXT_debug_utils.adoc#examples), you'll see that there is a way to create a separate debug utils messenger specifically for those two function calls. It requires you to simply pass a pointer to a `VkDebugUtilsMessengerCreateInfoEXT` struct in the `pNext` extension field of `VkInstanceCreateInfo`. First extract population of the messenger create info into a separate function: + +```c++ +void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) { + createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; + createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; + createInfo.pfnUserCallback = debugCallback; +} + +... + +void setupDebugMessenger() { + if (!enableValidationLayers) return; + + VkDebugUtilsMessengerCreateInfoEXT createInfo; + populateDebugMessengerCreateInfo(createInfo); + + if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { + throw std::runtime_error("failed to set up debug messenger!"); + } +} +``` + +We can now re-use this in the `createInstance` function: + +```c++ +void createInstance() { + ... + + VkInstanceCreateInfo createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + createInfo.pApplicationInfo = &appInfo; + + ... + + VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; + if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); + + populateDebugMessengerCreateInfo(debugCreateInfo); + createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*) &debugCreateInfo; + } else { + createInfo.enabledLayerCount = 0; + + createInfo.pNext = nullptr; + } + + if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); + } +} +``` + +The `debugCreateInfo` variable is placed outside the if statement to ensure that it is not destroyed before the `vkCreateInstance` call. By creating an additional debug messenger this way it will automatically be used during `vkCreateInstance` and `vkDestroyInstance` and cleaned up after that. + +## Testing + +Now let's intentionally make a mistake to see the validation layers in action. Temporarily remove the call to `DestroyDebugUtilsMessengerEXT` in the `cleanup` function and run your program. Once it exits you should see something like this: + +![](/images/validation_layer_test.png) + +>If you don't see any messages then [check your installation](https://vulkan.lunarg.com/doc/view/1.2.131.1/windows/getting_started.html#user-content-verify-the-installation). + +If you want to see which call triggered a message, you can add a breakpoint to the message callback and look at the stack trace. + +## Configuration + +There are a lot more settings for the behavior of validation layers than just +the flags specified in the `VkDebugUtilsMessengerCreateInfoEXT` struct. Browse +to the Vulkan SDK and go to the `Config` directory. There you will find a +`vk_layer_settings.txt` file that explains how to configure the layers. + +To configure the layer settings for your own application, copy the file to the +`Debug` and `Release` directories of your project and follow the instructions to +set the desired behavior. However, for the remainder of this tutorial I'll +assume that you're using the default settings. + +Throughout this tutorial I'll be making a couple of intentional mistakes to show +you how helpful the validation layers are with catching them and to teach you +how important it is to know exactly what you're doing with Vulkan. Now it's time +to look at [Vulkan devices in the system](!en/Drawing_a_triangle/Setup/Physical_devices_and_queue_families). + +[C++ code](/code/02_validation_layers.cpp) diff --git a/ko-rust/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md b/ko-rust/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md new file mode 100644 index 00000000..5761b9bc --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md @@ -0,0 +1,364 @@ +## Selecting a physical device + +After initializing the Vulkan library through a VkInstance we need to look for +and select a graphics card in the system that supports the features we need. In +fact we can select any number of graphics cards and use them simultaneously, but +in this tutorial we'll stick to the first graphics card that suits our needs. + +We'll add a function `pickPhysicalDevice` and add a call to it in the +`initVulkan` function. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + pickPhysicalDevice(); +} + +void pickPhysicalDevice() { + +} +``` + +The graphics card that we'll end up selecting will be stored in a +VkPhysicalDevice handle that is added as a new class member. This object will be +implicitly destroyed when the VkInstance is destroyed, so we won't need to do +anything new in the `cleanup` function. + +```c++ +VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; +``` + +Listing the graphics cards is very similar to listing extensions and starts with +querying just the number. + +```c++ +uint32_t deviceCount = 0; +vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr); +``` + +If there are 0 devices with Vulkan support then there is no point going further. + +```c++ +if (deviceCount == 0) { + throw std::runtime_error("failed to find GPUs with Vulkan support!"); +} +``` + +Otherwise we can now allocate an array to hold all of the VkPhysicalDevice +handles. + +```c++ +std::vector devices(deviceCount); +vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()); +``` + +Now we need to evaluate each of them and check if they are suitable for the +operations we want to perform, because not all graphics cards are created equal. +For that we'll introduce a new function: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + return true; +} +``` + +And we'll check if any of the physical devices meet the requirements that we'll +add to that function. + +```c++ +for (const auto& device : devices) { + if (isDeviceSuitable(device)) { + physicalDevice = device; + break; + } +} + +if (physicalDevice == VK_NULL_HANDLE) { + throw std::runtime_error("failed to find a suitable GPU!"); +} +``` + +The next section will introduce the first requirements that we'll check for in +the `isDeviceSuitable` function. As we'll start using more Vulkan features in +the later chapters we will also extend this function to include more checks. + +## Base device suitability checks + +To evaluate the suitability of a device we can start by querying for some +details. Basic device properties like the name, type and supported Vulkan +version can be queried using vkGetPhysicalDeviceProperties. + +```c++ +VkPhysicalDeviceProperties deviceProperties; +vkGetPhysicalDeviceProperties(device, &deviceProperties); +``` + +The support for optional features like texture compression, 64 bit floats and +multi viewport rendering (useful for VR) can be queried using +vkGetPhysicalDeviceFeatures: + +```c++ +VkPhysicalDeviceFeatures deviceFeatures; +vkGetPhysicalDeviceFeatures(device, &deviceFeatures); +``` + +There are more details that can be queried from devices that we'll discuss later +concerning device memory and queue families (see the next section). + +As an example, let's say we consider our application only usable for dedicated +graphics cards that support geometry shaders. Then the `isDeviceSuitable` +function would look like this: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + VkPhysicalDeviceProperties deviceProperties; + VkPhysicalDeviceFeatures deviceFeatures; + vkGetPhysicalDeviceProperties(device, &deviceProperties); + vkGetPhysicalDeviceFeatures(device, &deviceFeatures); + + return deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU && + deviceFeatures.geometryShader; +} +``` + +Instead of just checking if a device is suitable or not and going with the first +one, you could also give each device a score and pick the highest one. That way +you could favor a dedicated graphics card by giving it a higher score, but fall +back to an integrated GPU if that's the only available one. You could implement +something like that as follows: + +```c++ +#include + +... + +void pickPhysicalDevice() { + ... + + // Use an ordered map to automatically sort candidates by increasing score + std::multimap candidates; + + for (const auto& device : devices) { + int score = rateDeviceSuitability(device); + candidates.insert(std::make_pair(score, device)); + } + + // Check if the best candidate is suitable at all + if (candidates.rbegin()->first > 0) { + physicalDevice = candidates.rbegin()->second; + } else { + throw std::runtime_error("failed to find a suitable GPU!"); + } +} + +int rateDeviceSuitability(VkPhysicalDevice device) { + ... + + int score = 0; + + // Discrete GPUs have a significant performance advantage + if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { + score += 1000; + } + + // Maximum possible size of textures affects graphics quality + score += deviceProperties.limits.maxImageDimension2D; + + // Application can't function without geometry shaders + if (!deviceFeatures.geometryShader) { + return 0; + } + + return score; +} +``` + +You don't need to implement all that for this tutorial, but it's to give you an +idea of how you could design your device selection process. Of course you can +also just display the names of the choices and allow the user to select. + +Because we're just starting out, Vulkan support is the only thing we need and +therefore we'll settle for just any GPU: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + return true; +} +``` + +In the next section we'll discuss the first real required feature to check for. + +## Queue families + +It has been briefly touched upon before that almost every operation in Vulkan, +anything from drawing to uploading textures, requires commands to be submitted +to a queue. There are different types of queues that originate from different +*queue families* and each family of queues allows only a subset of commands. For +example, there could be a queue family that only allows processing of compute +commands or one that only allows memory transfer related commands. + +We need to check which queue families are supported by the device and which one +of these supports the commands that we want to use. For that purpose we'll add a +new function `findQueueFamilies` that looks for all the queue families we need. + +Right now we are only going to look for a queue that supports graphics commands, +so the function could look like this: + +```c++ +uint32_t findQueueFamilies(VkPhysicalDevice device) { + // Logic to find graphics queue family +} +``` + +However, in one of the next chapters we're already going to look for yet another +queue, so it's better to prepare for that and bundle the indices into a struct: + +```c++ +struct QueueFamilyIndices { + uint32_t graphicsFamily; +}; + +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + // Logic to find queue family indices to populate struct with + return indices; +} +``` + +But what if a queue family is not available? We could throw an exception in +`findQueueFamilies`, but this function is not really the right place to make +decisions about device suitability. For example, we may *prefer* devices with a +dedicated transfer queue family, but not require it. Therefore we need some way +of indicating whether a particular queue family was found. + +It's not really possible to use a magic value to indicate the nonexistence of a +queue family, since any value of `uint32_t` could in theory be a valid queue +family index including `0`. Luckily C++17 introduced a data structure to +distinguish between the case of a value existing or not: + +```c++ +#include + +... + +std::optional graphicsFamily; + +std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // false + +graphicsFamily = 0; + +std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // true +``` + +`std::optional` is a wrapper that contains no value until you assign something +to it. At any point you can query if it contains a value or not by calling its +`has_value()` member function. That means that we can change the logic to: + +```c++ +#include + +... + +struct QueueFamilyIndices { + std::optional graphicsFamily; +}; + +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + // Assign index to queue families that could be found + return indices; +} +``` + +We can now begin to actually implement `findQueueFamilies`: + +```c++ +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + + ... + + return indices; +} +``` + +The process of retrieving the list of queue families is exactly what you expect +and uses `vkGetPhysicalDeviceQueueFamilyProperties`: + +```c++ +uint32_t queueFamilyCount = 0; +vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr); + +std::vector queueFamilies(queueFamilyCount); +vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data()); +``` + +The VkQueueFamilyProperties struct contains some details about the queue family, +including the type of operations that are supported and the number of queues +that can be created based on that family. We need to find at least one queue +family that supports `VK_QUEUE_GRAPHICS_BIT`. + +```c++ +int i = 0; +for (const auto& queueFamily : queueFamilies) { + if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) { + indices.graphicsFamily = i; + } + + i++; +} +``` + +Now that we have this fancy queue family lookup function, we can use it as a +check in the `isDeviceSuitable` function to ensure that the device can process +the commands we want to use: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + QueueFamilyIndices indices = findQueueFamilies(device); + + return indices.graphicsFamily.has_value(); +} +``` + +To make this a little bit more convenient, we'll also add a generic check to the +struct itself: + +```c++ +struct QueueFamilyIndices { + std::optional graphicsFamily; + + bool isComplete() { + return graphicsFamily.has_value(); + } +}; + +... + +bool isDeviceSuitable(VkPhysicalDevice device) { + QueueFamilyIndices indices = findQueueFamilies(device); + + return indices.isComplete(); +} +``` + +We can now also use this for an early exit from `findQueueFamilies`: + +```c++ +for (const auto& queueFamily : queueFamilies) { + ... + + if (indices.isComplete()) { + break; + } + + i++; +} +``` + +Great, that's all we need for now to find the right physical device! The next +step is to [create a logical device](!en/Drawing_a_triangle/Setup/Logical_device_and_queues) +to interface with it. + +[C++ code](/code/03_physical_device_selection.cpp) diff --git a/ko-rust/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md b/ko-rust/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md new file mode 100644 index 00000000..f2677d08 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md @@ -0,0 +1,171 @@ +## Introduction + +After selecting a physical device to use we need to set up a *logical device* to +interface with it. The logical device creation process is similar to the +instance creation process and describes the features we want to use. We also +need to specify which queues to create now that we've queried which queue +families are available. You can even create multiple logical devices from the +same physical device if you have varying requirements. + +Start by adding a new class member to store the logical device handle in. + +```c++ +VkDevice device; +``` + +Next, add a `createLogicalDevice` function that is called from `initVulkan`. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + pickPhysicalDevice(); + createLogicalDevice(); +} + +void createLogicalDevice() { + +} +``` + +## Specifying the queues to be created + +The creation of a logical device involves specifying a bunch of details in +structs again, of which the first one will be `VkDeviceQueueCreateInfo`. This +structure describes the number of queues we want for a single queue family. +Right now we're only interested in a queue with graphics capabilities. + +```c++ +QueueFamilyIndices indices = findQueueFamilies(physicalDevice); + +VkDeviceQueueCreateInfo queueCreateInfo{}; +queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; +queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value(); +queueCreateInfo.queueCount = 1; +``` + +The currently available drivers will only allow you to create a small number of +queues for each queue family and you don't really need more than one. That's +because you can create all of the command buffers on multiple threads and then +submit them all at once on the main thread with a single low-overhead call. + +Vulkan lets you assign priorities to queues to influence the scheduling of +command buffer execution using floating point numbers between `0.0` and `1.0`. +This is required even if there is only a single queue: + +```c++ +float queuePriority = 1.0f; +queueCreateInfo.pQueuePriorities = &queuePriority; +``` + +## Specifying used device features + +The next information to specify is the set of device features that we'll be +using. These are the features that we queried support for with +`vkGetPhysicalDeviceFeatures` in the previous chapter, like geometry shaders. +Right now we don't need anything special, so we can simply define it and leave +everything to `VK_FALSE`. We'll come back to this structure once we're about to +start doing more interesting things with Vulkan. + +```c++ +VkPhysicalDeviceFeatures deviceFeatures{}; +``` + +## Creating the logical device + +With the previous two structures in place, we can start filling in the main +`VkDeviceCreateInfo` structure. + +```c++ +VkDeviceCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; +``` + +First add pointers to the queue creation info and device features structs: + +```c++ +createInfo.pQueueCreateInfos = &queueCreateInfo; +createInfo.queueCreateInfoCount = 1; + +createInfo.pEnabledFeatures = &deviceFeatures; +``` + +The remainder of the information bears a resemblance to the +`VkInstanceCreateInfo` struct and requires you to specify extensions and +validation layers. The difference is that these are device specific this time. + +An example of a device specific extension is `VK_KHR_swapchain`, which allows +you to present rendered images from that device to windows. It is possible that +there are Vulkan devices in the system that lack this ability, for example +because they only support compute operations. We will come back to this +extension in the swap chain chapter. + +Previous implementations of Vulkan made a distinction between instance and device specific validation layers, but this is [no longer the case](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap40.html#extendingvulkan-layers-devicelayerdeprecation). That means that the `enabledLayerCount` and `ppEnabledLayerNames` fields of `VkDeviceCreateInfo` are ignored by up-to-date implementations. However, it is still a good idea to set them anyway to be compatible with older implementations: + +```c++ +createInfo.enabledExtensionCount = 0; + +if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); +} else { + createInfo.enabledLayerCount = 0; +} +``` + +We won't need any device specific extensions for now. + +That's it, we're now ready to instantiate the logical device with a call to the +appropriately named `vkCreateDevice` function. + +```c++ +if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) { + throw std::runtime_error("failed to create logical device!"); +} +``` + +The parameters are the physical device to interface with, the queue and usage +info we just specified, the optional allocation callbacks pointer and a pointer +to a variable to store the logical device handle in. Similarly to the instance +creation function, this call can return errors based on enabling non-existent +extensions or specifying the desired usage of unsupported features. + +The device should be destroyed in `cleanup` with the `vkDestroyDevice` function: + +```c++ +void cleanup() { + vkDestroyDevice(device, nullptr); + ... +} +``` + +Logical devices don't interact directly with instances, which is why it's not +included as a parameter. + +## Retrieving queue handles + +The queues are automatically created along with the logical device, but we don't +have a handle to interface with them yet. First add a class member to store a +handle to the graphics queue: + +```c++ +VkQueue graphicsQueue; +``` + +Device queues are implicitly cleaned up when the device is destroyed, so we +don't need to do anything in `cleanup`. + +We can use the `vkGetDeviceQueue` function to retrieve queue handles for each +queue family. The parameters are the logical device, queue family, queue index +and a pointer to the variable to store the queue handle in. Because we're only +creating a single queue from this family, we'll simply use index `0`. + +```c++ +vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue); +``` + +With the logical device and queue handles we can now actually start using the +graphics card to do things! In the next few chapters we'll set up the resources +to present results to the window system. + +[C++ code](/code/04_logical_device.cpp) diff --git a/ko-rust/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md b/ko-rust/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md new file mode 100644 index 00000000..966a8946 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md @@ -0,0 +1,233 @@ +Since Vulkan is a platform agnostic API, it can not interface directly with the +window system on its own. To establish the connection between Vulkan and the +window system to present results to the screen, we need to use the WSI (Window +System Integration) extensions. In this chapter we'll discuss the first one, +which is `VK_KHR_surface`. It exposes a `VkSurfaceKHR` object that represents an +abstract type of surface to present rendered images to. The surface in our +program will be backed by the window that we've already opened with GLFW. + +The `VK_KHR_surface` extension is an instance level extension and we've actually +already enabled it, because it's included in the list returned by +`glfwGetRequiredInstanceExtensions`. The list also includes some other WSI +extensions that we'll use in the next couple of chapters. + +The window surface needs to be created right after the instance creation, +because it can actually influence the physical device selection. The reason we +postponed this is because window surfaces are part of the larger topic of +render targets and presentation for which the explanation would have cluttered +the basic setup. It should also be noted that window surfaces are an entirely +optional component in Vulkan, if you just need off-screen rendering. Vulkan +allows you to do that without hacks like creating an invisible window +(necessary for OpenGL). + +## Window surface creation + +Start by adding a `surface` class member right below the debug callback. + +```c++ +VkSurfaceKHR surface; +``` + +Although the `VkSurfaceKHR` object and its usage is platform agnostic, its +creation isn't because it depends on window system details. For example, it +needs the `HWND` and `HMODULE` handles on Windows. Therefore there is a +platform-specific addition to the extension, which on Windows is called +`VK_KHR_win32_surface` and is also automatically included in the list from +`glfwGetRequiredInstanceExtensions`. + +I will demonstrate how this platform specific extension can be used to create a +surface on Windows, but we won't actually use it in this tutorial. It doesn't +make any sense to use a library like GLFW and then proceed to use +platform-specific code anyway. GLFW actually has `glfwCreateWindowSurface` that +handles the platform differences for us. Still, it's good to see what it does +behind the scenes before we start relying on it. + +To access native platform functions, you need to update the includes at the top: + +```c++ +#define VK_USE_PLATFORM_WIN32_KHR +#define GLFW_INCLUDE_VULKAN +#include +#define GLFW_EXPOSE_NATIVE_WIN32 +#include +``` + +Because a window surface is a Vulkan object, it comes with a +`VkWin32SurfaceCreateInfoKHR` struct that needs to be filled in. It has two +important parameters: `hwnd` and `hinstance`. These are the handles to the +window and the process. + +```c++ +VkWin32SurfaceCreateInfoKHR createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; +createInfo.hwnd = glfwGetWin32Window(window); +createInfo.hinstance = GetModuleHandle(nullptr); +``` + +The `glfwGetWin32Window` function is used to get the raw `HWND` from the GLFW +window object. The `GetModuleHandle` call returns the `HINSTANCE` handle of the +current process. + +After that the surface can be created with `vkCreateWin32SurfaceKHR`, which includes a parameter for the instance, surface creation details, custom allocators and the variable for the surface handle to be stored in. Technically this is a WSI extension function, but it is so commonly used that the standard Vulkan loader includes it, so unlike other extensions you don't need to explicitly load it. + +```c++ +if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) { + throw std::runtime_error("failed to create window surface!"); +} +``` + +The process is similar for other platforms like Linux, where +`vkCreateXcbSurfaceKHR` takes an XCB connection and window as creation details +with X11. + +The `glfwCreateWindowSurface` function performs exactly this operation with a +different implementation for each platform. We'll now integrate it into our +program. Add a function `createSurface` to be called from `initVulkan` right +after instance creation and `setupDebugMessenger`. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); +} + +void createSurface() { + +} +``` + +The GLFW call takes simple parameters instead of a struct which makes the +implementation of the function very straightforward: + +```c++ +void createSurface() { + if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) { + throw std::runtime_error("failed to create window surface!"); + } +} +``` + +The parameters are the `VkInstance`, GLFW window pointer, custom allocators and +pointer to `VkSurfaceKHR` variable. It simply passes through the `VkResult` from +the relevant platform call. GLFW doesn't offer a special function for destroying +a surface, but that can easily be done through the original API: + +```c++ +void cleanup() { + ... + vkDestroySurfaceKHR(instance, surface, nullptr); + vkDestroyInstance(instance, nullptr); + ... + } +``` + +Make sure that the surface is destroyed before the instance. + +## Querying for presentation support + +Although the Vulkan implementation may support window system integration, that +does not mean that every device in the system supports it. Therefore we need to +extend `isDeviceSuitable` to ensure that a device can present images to the +surface we created. Since the presentation is a queue-specific feature, the +problem is actually about finding a queue family that supports presenting to the +surface we created. + +It's actually possible that the queue families supporting drawing commands and +the ones supporting presentation do not overlap. Therefore we have to take into +account that there could be a distinct presentation queue by modifying the +`QueueFamilyIndices` structure: + +```c++ +struct QueueFamilyIndices { + std::optional graphicsFamily; + std::optional presentFamily; + + bool isComplete() { + return graphicsFamily.has_value() && presentFamily.has_value(); + } +}; +``` + +Next, we'll modify the `findQueueFamilies` function to look for a queue family +that has the capability of presenting to our window surface. The function to +check for that is `vkGetPhysicalDeviceSurfaceSupportKHR`, which takes the +physical device, queue family index and surface as parameters. Add a call to it +in the same loop as the `VK_QUEUE_GRAPHICS_BIT`: + +```c++ +VkBool32 presentSupport = false; +vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport); +``` + +Then simply check the value of the boolean and store the presentation family +queue index: + +```c++ +if (presentSupport) { + indices.presentFamily = i; +} +``` + +Note that it's very likely that these end up being the same queue family after +all, but throughout the program we will treat them as if they were separate +queues for a uniform approach. Nevertheless, you could add logic to explicitly +prefer a physical device that supports drawing and presentation in the same +queue for improved performance. + +## Creating the presentation queue + +The one thing that remains is modifying the logical device creation procedure to +create the presentation queue and retrieve the `VkQueue` handle. Add a member +variable for the handle: + +```c++ +VkQueue presentQueue; +``` + +Next, we need to have multiple `VkDeviceQueueCreateInfo` structs to create a +queue from both families. An elegant way to do that is to create a set of all +unique queue families that are necessary for the required queues: + +```c++ +#include + +... + +QueueFamilyIndices indices = findQueueFamilies(physicalDevice); + +std::vector queueCreateInfos; +std::set uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()}; + +float queuePriority = 1.0f; +for (uint32_t queueFamily : uniqueQueueFamilies) { + VkDeviceQueueCreateInfo queueCreateInfo{}; + queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queueCreateInfo.queueFamilyIndex = queueFamily; + queueCreateInfo.queueCount = 1; + queueCreateInfo.pQueuePriorities = &queuePriority; + queueCreateInfos.push_back(queueCreateInfo); +} +``` + +And modify `VkDeviceCreateInfo` to point to the vector: + +```c++ +createInfo.queueCreateInfoCount = static_cast(queueCreateInfos.size()); +createInfo.pQueueCreateInfos = queueCreateInfos.data(); +``` + +If the queue families are the same, then we only need to pass its index once. +Finally, add a call to retrieve the queue handle: + +```c++ +vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue); +``` + +In case the queue families are the same, the two handles will most likely have +the same value now. In the next chapter we're going to look at swap chains and +how they give us the ability to present images to the surface. + +[C++ code](/code/05_window_surface.cpp) diff --git a/ko-rust/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md b/ko-rust/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md new file mode 100644 index 00000000..f593b5a6 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md @@ -0,0 +1,603 @@ +Vulkan does not have the concept of a "default framebuffer", hence it requires an infrastructure that will own the buffers we will render to before we visualize them on the screen. This infrastructure is +known as the *swap chain* and must be created explicitly in Vulkan. The swap +chain is essentially a queue of images that are waiting to be presented to the +screen. Our application will acquire such an image to draw to it, and then +return it to the queue. How exactly the queue works and the conditions for +presenting an image from the queue depend on how the swap chain is set up, but +the general purpose of the swap chain is to synchronize the presentation of +images with the refresh rate of the screen. + +## Checking for swap chain support + +Not all graphics cards are capable of presenting images directly to a screen for +various reasons, for example because they are designed for servers and don't +have any display outputs. Secondly, since image presentation is heavily tied +into the window system and the surfaces associated with windows, it is not +actually part of the Vulkan core. You have to enable the `VK_KHR_swapchain` +device extension after querying for its support. + +For that purpose we'll first extend the `isDeviceSuitable` function to check if +this extension is supported. We've previously seen how to list the extensions +that are supported by a `VkPhysicalDevice`, so doing that should be fairly +straightforward. Note that the Vulkan header file provides a nice macro +`VK_KHR_SWAPCHAIN_EXTENSION_NAME` that is defined as `VK_KHR_swapchain`. The +advantage of using this macro is that the compiler will catch misspellings. + +First declare a list of required device extensions, similar to the list of +validation layers to enable. + +```c++ +const std::vector deviceExtensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME +}; +``` + +Next, create a new function `checkDeviceExtensionSupport` that is called from +`isDeviceSuitable` as an additional check: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + QueueFamilyIndices indices = findQueueFamilies(device); + + bool extensionsSupported = checkDeviceExtensionSupport(device); + + return indices.isComplete() && extensionsSupported; +} + +bool checkDeviceExtensionSupport(VkPhysicalDevice device) { + return true; +} +``` + +Modify the body of the function to enumerate the extensions and check if all of +the required extensions are amongst them. + +```c++ +bool checkDeviceExtensionSupport(VkPhysicalDevice device) { + uint32_t extensionCount; + vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr); + + std::vector availableExtensions(extensionCount); + vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data()); + + std::set requiredExtensions(deviceExtensions.begin(), deviceExtensions.end()); + + for (const auto& extension : availableExtensions) { + requiredExtensions.erase(extension.extensionName); + } + + return requiredExtensions.empty(); +} +``` + +I've chosen to use a set of strings here to represent the unconfirmed required +extensions. That way we can easily tick them off while enumerating the sequence +of available extensions. Of course you can also use a nested loop like in +`checkValidationLayerSupport`. The performance difference is irrelevant. Now run +the code and verify that your graphics card is indeed capable of creating a +swap chain. It should be noted that the availability of a presentation queue, +as we checked in the previous chapter, implies that the swap chain extension +must be supported. However, it's still good to be explicit about things, and +the extension does have to be explicitly enabled. + +## Enabling device extensions + +Using a swapchain requires enabling the `VK_KHR_swapchain` extension first. +Enabling the extension just requires a small change to the logical device +creation structure: + +```c++ +createInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); +createInfo.ppEnabledExtensionNames = deviceExtensions.data(); +``` + +Make sure to replace the existing line `createInfo.enabledExtensionCount = 0;` when you do so. + +## Querying details of swap chain support + +Just checking if a swap chain is available is not sufficient, because it may not +actually be compatible with our window surface. Creating a swap chain also +involves a lot more settings than instance and device creation, so we need to +query for some more details before we're able to proceed. + +There are basically three kinds of properties we need to check: + +* Basic surface capabilities (min/max number of images in swap chain, min/max +width and height of images) +* Surface formats (pixel format, color space) +* Available presentation modes + +Similar to `findQueueFamilies`, we'll use a struct to pass these details around +once they've been queried. The three aforementioned types of properties come in +the form of the following structs and lists of structs: + +```c++ +struct SwapChainSupportDetails { + VkSurfaceCapabilitiesKHR capabilities; + std::vector formats; + std::vector presentModes; +}; +``` + +We'll now create a new function `querySwapChainSupport` that will populate this +struct. + +```c++ +SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) { + SwapChainSupportDetails details; + + return details; +} +``` + +This section covers how to query the structs that include this information. The +meaning of these structs and exactly which data they contain is discussed in the +next section. + +Let's start with the basic surface capabilities. These properties are simple to +query and are returned into a single `VkSurfaceCapabilitiesKHR` struct. + +```c++ +vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities); +``` + +This function takes the specified `VkPhysicalDevice` and `VkSurfaceKHR` window +surface into account when determining the supported capabilities. All of the +support querying functions have these two as first parameters because they are +the core components of the swap chain. + +The next step is about querying the supported surface formats. Because this is a +list of structs, it follows the familiar ritual of 2 function calls: + +```c++ +uint32_t formatCount; +vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr); + +if (formatCount != 0) { + details.formats.resize(formatCount); + vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data()); +} +``` + +Make sure that the vector is resized to hold all the available formats. And +finally, querying the supported presentation modes works exactly the same way +with `vkGetPhysicalDeviceSurfacePresentModesKHR`: + +```c++ +uint32_t presentModeCount; +vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr); + +if (presentModeCount != 0) { + details.presentModes.resize(presentModeCount); + vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data()); +} +``` + +All of the details are in the struct now, so let's extend `isDeviceSuitable` +once more to utilize this function to verify that swap chain support is +adequate. Swap chain support is sufficient for this tutorial if there is at +least one supported image format and one supported presentation mode given the +window surface we have. + +```c++ +bool swapChainAdequate = false; +if (extensionsSupported) { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); + swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); +} +``` + +It is important that we only try to query for swap chain support after verifying +that the extension is available. The last line of the function changes to: + +```c++ +return indices.isComplete() && extensionsSupported && swapChainAdequate; +``` + +## Choosing the right settings for the swap chain + +If the `swapChainAdequate` conditions were met then the support is definitely +sufficient, but there may still be many different modes of varying optimality. +We'll now write a couple of functions to find the right settings for the best +possible swap chain. There are three types of settings to determine: + +* Surface format (color depth) +* Presentation mode (conditions for "swapping" images to the screen) +* Swap extent (resolution of images in swap chain) + +For each of these settings we'll have an ideal value in mind that we'll go with +if it's available and otherwise we'll create some logic to find the next best +thing. + +### Surface format + +The function for this setting starts out like this. We'll later pass the +`formats` member of the `SwapChainSupportDetails` struct as argument. + +```c++ +VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { + +} +``` + +Each `VkSurfaceFormatKHR` entry contains a `format` and a `colorSpace` member. The +`format` member specifies the color channels and types. For example, +`VK_FORMAT_B8G8R8A8_SRGB` means that we store the B, G, R and alpha channels in +that order with an 8 bit unsigned integer for a total of 32 bits per pixel. The +`colorSpace` member indicates if the SRGB color space is supported or not using +the `VK_COLOR_SPACE_SRGB_NONLINEAR_KHR` flag. Note that this flag used to be +called `VK_COLORSPACE_SRGB_NONLINEAR_KHR` in old versions of the specification. + +For the color space we'll use SRGB if it is available, because it [results in more accurate perceived colors](http://stackoverflow.com/questions/12524623/). It is also pretty much the standard color space for images, like the textures we'll use later on. +Because of that we should also use an SRGB color format, of which one of the most common ones is `VK_FORMAT_B8G8R8A8_SRGB`. + +Let's go through the list and see if the preferred combination is available: + +```c++ +for (const auto& availableFormat : availableFormats) { + if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + return availableFormat; + } +} +``` + +If that also fails then we could start ranking the available formats based on +how "good" they are, but in most cases it's okay to just settle with the first +format that is specified. + +```c++ +VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { + for (const auto& availableFormat : availableFormats) { + if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + return availableFormat; + } + } + + return availableFormats[0]; +} +``` + +### Presentation mode + +The presentation mode is arguably the most important setting for the swap chain, +because it represents the actual conditions for showing images to the screen. +There are four possible modes available in Vulkan: + +* `VK_PRESENT_MODE_IMMEDIATE_KHR`: Images submitted by your application are +transferred to the screen right away, which may result in tearing. +* `VK_PRESENT_MODE_FIFO_KHR`: The swap chain is a queue where the display takes +an image from the front of the queue when the display is refreshed and the +program inserts rendered images at the back of the queue. If the queue is full +then the program has to wait. This is most similar to vertical sync as found in +modern games. The moment that the display is refreshed is known as "vertical +blank". +* `VK_PRESENT_MODE_FIFO_RELAXED_KHR`: This mode only differs from the previous +one if the application is late and the queue was empty at the last vertical +blank. Instead of waiting for the next vertical blank, the image is transferred +right away when it finally arrives. This may result in visible tearing. +* `VK_PRESENT_MODE_MAILBOX_KHR`: This is another variation of the second mode. +Instead of blocking the application when the queue is full, the images that are +already queued are simply replaced with the newer ones. This mode can be used to +render frames as fast as possible while still avoiding tearing, resulting in fewer latency issues than standard vertical sync. This is commonly known as "triple buffering", although the existence of three buffers alone does not necessarily mean that the framerate is unlocked. + +Only the `VK_PRESENT_MODE_FIFO_KHR` mode is guaranteed to be available, so we'll +again have to write a function that looks for the best mode that is available: + +```c++ +VkPresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { + return VK_PRESENT_MODE_FIFO_KHR; +} +``` + +I personally think that `VK_PRESENT_MODE_MAILBOX_KHR` is a very nice trade-off if energy usage is not a concern. It allows us to avoid tearing while still maintaining a fairly low latency by rendering new images that are as up-to-date as possible right until the vertical blank. On mobile devices, where energy usage is more important, you will probably want to use `VK_PRESENT_MODE_FIFO_KHR` instead. Now, let's look through the list to see if `VK_PRESENT_MODE_MAILBOX_KHR` is available: + +```c++ +VkPresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { + for (const auto& availablePresentMode : availablePresentModes) { + if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) { + return availablePresentMode; + } + } + + return VK_PRESENT_MODE_FIFO_KHR; +} +``` + +### Swap extent + +That leaves only one major property, for which we'll add one last function: + +```c++ +VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) { + +} +``` + +The swap extent is the resolution of the swap chain images and it's almost +always exactly equal to the resolution of the window that we're drawing to _in +pixels_ (more on that in a moment). The range of the possible resolutions is +defined in the `VkSurfaceCapabilitiesKHR` structure. Vulkan tells us to match +the resolution of the window by setting the width and height in the +`currentExtent` member. However, some window managers do allow us to differ here +and this is indicated by setting the width and height in `currentExtent` to a +special value: the maximum value of `uint32_t`. In that case we'll pick the +resolution that best matches the window within the `minImageExtent` and +`maxImageExtent` bounds. But we must specify the resolution in the correct unit. + +GLFW uses two units when measuring sizes: pixels and +[screen coordinates](https://www.glfw.org/docs/latest/intro_guide.html#coordinate_systems). +For example, the resolution `{WIDTH, HEIGHT}` that we specified earlier when +creating the window is measured in screen coordinates. But Vulkan works with +pixels, so the swap chain extent must be specified in pixels as well. +Unfortunately, if you are using a high DPI display (like Apple's Retina +display), screen coordinates don't correspond to pixels. Instead, due to the +higher pixel density, the resolution of the window in pixel will be larger than +the resolution in screen coordinates. So if Vulkan doesn't fix the swap extent +for us, we can't just use the original `{WIDTH, HEIGHT}`. Instead, we must use +`glfwGetFramebufferSize` to query the resolution of the window in pixel before +matching it against the minimum and maximum image extent. + +```c++ +#include // Necessary for uint32_t +#include // Necessary for std::numeric_limits +#include // Necessary for std::clamp + +... + +VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) { + if (capabilities.currentExtent.width != std::numeric_limits::max()) { + return capabilities.currentExtent; + } else { + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + VkExtent2D actualExtent = { + static_cast(width), + static_cast(height) + }; + + actualExtent.width = std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); + actualExtent.height = std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); + + return actualExtent; + } +} +``` + +The `clamp` function is used here to bound the values of `width` and `height` between the allowed minimum and maximum extents that are supported by the implementation. + +## Creating the swap chain + +Now that we have all of these helper functions assisting us with the choices we +have to make at runtime, we finally have all the information that is needed to +create a working swap chain. + +Create a `createSwapChain` function that starts out with the results of these +calls and make sure to call it from `initVulkan` after logical device creation. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); +} + +void createSwapChain() { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice); + + VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats); + VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes); + VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities); +} +``` + +Aside from these properties we also have to decide how many images we would like to have in the swap chain. The implementation specifies the minimum number that it requires to function: + +```c++ +uint32_t imageCount = swapChainSupport.capabilities.minImageCount; +``` + +However, simply sticking to this minimum means that we may sometimes have to wait on the driver to complete internal operations before we can acquire another image to render to. Therefore it is recommended to request at least one more image than the minimum: + +```c++ +uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1; +``` + +We should also make sure to not exceed the maximum number of images while doing this, where `0` is a special value that means that there is no maximum: + +```c++ +if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) { + imageCount = swapChainSupport.capabilities.maxImageCount; +} +``` + +As is tradition with Vulkan objects, creating the swap chain object requires +filling in a large structure. It starts out very familiarly: + +```c++ +VkSwapchainCreateInfoKHR createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; +createInfo.surface = surface; +``` + +After specifying which surface the swap chain should be tied to, the details of +the swap chain images are specified: + +```c++ +createInfo.minImageCount = imageCount; +createInfo.imageFormat = surfaceFormat.format; +createInfo.imageColorSpace = surfaceFormat.colorSpace; +createInfo.imageExtent = extent; +createInfo.imageArrayLayers = 1; +createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; +``` + +The `imageArrayLayers` specifies the amount of layers each image consists of. +This is always `1` unless you are developing a stereoscopic 3D application. The +`imageUsage` bit field specifies what kind of operations we'll use the images in +the swap chain for. In this tutorial we're going to render directly to them, +which means that they're used as color attachment. It is also possible that +you'll render images to a separate image first to perform operations like +post-processing. In that case you may use a value like +`VK_IMAGE_USAGE_TRANSFER_DST_BIT` instead and use a memory operation to transfer +the rendered image to a swap chain image. + +```c++ +QueueFamilyIndices indices = findQueueFamilies(physicalDevice); +uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()}; + +if (indices.graphicsFamily != indices.presentFamily) { + createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT; + createInfo.queueFamilyIndexCount = 2; + createInfo.pQueueFamilyIndices = queueFamilyIndices; +} else { + createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + createInfo.queueFamilyIndexCount = 0; // Optional + createInfo.pQueueFamilyIndices = nullptr; // Optional +} +``` + +Next, we need to specify how to handle swap chain images that will be used +across multiple queue families. That will be the case in our application if the +graphics queue family is different from the presentation queue. We'll be drawing +on the images in the swap chain from the graphics queue and then submitting them +on the presentation queue. There are two ways to handle images that are +accessed from multiple queues: + +* `VK_SHARING_MODE_EXCLUSIVE`: An image is owned by one queue family at a time +and ownership must be explicitly transferred before using it in another queue +family. This option offers the best performance. +* `VK_SHARING_MODE_CONCURRENT`: Images can be used across multiple queue +families without explicit ownership transfers. + +If the queue families differ, then we'll be using the concurrent mode in this +tutorial to avoid having to do the ownership chapters, because these involve +some concepts that are better explained at a later time. Concurrent mode +requires you to specify in advance between which queue families ownership will +be shared using the `queueFamilyIndexCount` and `pQueueFamilyIndices` +parameters. If the graphics queue family and presentation queue family are the +same, which will be the case on most hardware, then we should stick to exclusive +mode, because concurrent mode requires you to specify at least two distinct +queue families. + +```c++ +createInfo.preTransform = swapChainSupport.capabilities.currentTransform; +``` + +We can specify that a certain transform should be applied to images in the swap +chain if it is supported (`supportedTransforms` in `capabilities`), like a 90 +degree clockwise rotation or horizontal flip. To specify that you do not want +any transformation, simply specify the current transformation. + +```c++ +createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; +``` + +The `compositeAlpha` field specifies if the alpha channel should be used for +blending with other windows in the window system. You'll almost always want to +simply ignore the alpha channel, hence `VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR`. + +```c++ +createInfo.presentMode = presentMode; +createInfo.clipped = VK_TRUE; +``` + +The `presentMode` member speaks for itself. If the `clipped` member is set to +`VK_TRUE` then that means that we don't care about the color of pixels that are +obscured, for example because another window is in front of them. Unless you +really need to be able to read these pixels back and get predictable results, +you'll get the best performance by enabling clipping. + +```c++ +createInfo.oldSwapchain = VK_NULL_HANDLE; +``` + +That leaves one last field, `oldSwapchain`. With Vulkan it's possible that your swap chain becomes invalid or unoptimized while your application is +running, for example because the window was resized. In that case the swap chain +actually needs to be recreated from scratch and a reference to the old one must +be specified in this field. This is a complex topic that we'll learn more about +in [a future chapter](!en/Drawing_a_triangle/Swap_chain_recreation). For now we'll +assume that we'll only ever create one swap chain. + +Now add a class member to store the `VkSwapchainKHR` object: + +```c++ +VkSwapchainKHR swapChain; +``` + +Creating the swap chain is now as simple as calling `vkCreateSwapchainKHR`: + +```c++ +if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) { + throw std::runtime_error("failed to create swap chain!"); +} +``` + +The parameters are the logical device, swap chain creation info, optional custom +allocators and a pointer to the variable to store the handle in. No surprises +there. It should be cleaned up using `vkDestroySwapchainKHR` before the device: + +```c++ +void cleanup() { + vkDestroySwapchainKHR(device, swapChain, nullptr); + ... +} +``` + +Now run the application to ensure that the swap chain is created successfully! If at this point you get an access violation error in `vkCreateSwapchainKHR` or see a message like `Failed to find 'vkGetInstanceProcAddress' in layer SteamOverlayVulkanLayer.dll`, then see the [FAQ entry](!en/FAQ) about the Steam overlay layer. + +Try removing the `createInfo.imageExtent = extent;` line with validation layers +enabled. You'll see that one of the validation layers immediately catches the +mistake and a helpful message is printed: + +![](/images/swap_chain_validation_layer.png) + +## Retrieving the swap chain images + +The swap chain has been created now, so all that remains is retrieving the +handles of the `VkImage`s in it. We'll reference these during rendering +operations in later chapters. Add a class member to store the handles: + +```c++ +std::vector swapChainImages; +``` + +The images were created by the implementation for the swap chain and they will +be automatically cleaned up once the swap chain has been destroyed, therefore we +don't need to add any cleanup code. + +I'm adding the code to retrieve the handles to the end of the `createSwapChain` +function, right after the `vkCreateSwapchainKHR` call. Retrieving them is very +similar to the other times where we retrieved an array of objects from Vulkan. Remember that we only specified a minimum number of images in the swap chain, so the implementation is allowed to create a swap chain with more. That's why we'll first query the final number of images with `vkGetSwapchainImagesKHR`, then resize the container and finally call it again +to retrieve the handles. + +```c++ +vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr); +swapChainImages.resize(imageCount); +vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data()); +``` + +One last thing, store the format and extent we've chosen for the swap chain +images in member variables. We'll need them in future chapters. + +```c++ +VkSwapchainKHR swapChain; +std::vector swapChainImages; +VkFormat swapChainImageFormat; +VkExtent2D swapChainExtent; + +... + +swapChainImageFormat = surfaceFormat.format; +swapChainExtent = extent; +``` + +We now have a set of images that can be drawn onto and can be presented to the +window. The next chapter will begin to cover how we can set up the images as +render targets and then we start looking into the actual graphics pipeline and +drawing commands! + +[C++ code](/code/06_swap_chain_creation.cpp) diff --git a/ko-rust/03_Drawing_a_triangle/01_Presentation/02_Image_views.md b/ko-rust/03_Drawing_a_triangle/01_Presentation/02_Image_views.md new file mode 100644 index 00000000..5988468a --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/01_Presentation/02_Image_views.md @@ -0,0 +1,127 @@ +To use any `VkImage`, including those in the swap chain, in the render pipeline +we have to create a `VkImageView` object. An image view is quite literally a +view into an image. It describes how to access the image and which part of the +image to access, for example if it should be treated as a 2D texture depth +texture without any mipmapping levels. + +In this chapter we'll write a `createImageViews` function that creates a basic +image view for every image in the swap chain so that we can use them as color +targets later on. + +First add a class member to store the image views in: + +```c++ +std::vector swapChainImageViews; +``` + +Create the `createImageViews` function and call it right after swap chain +creation. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); +} + +void createImageViews() { + +} +``` + +The first thing we need to do is resize the list to fit all of the image views +we'll be creating: + +```c++ +void createImageViews() { + swapChainImageViews.resize(swapChainImages.size()); + +} +``` + +Next, set up the loop that iterates over all of the swap chain images. + +```c++ +for (size_t i = 0; i < swapChainImages.size(); i++) { + +} +``` + +The parameters for image view creation are specified in a +`VkImageViewCreateInfo` structure. The first few parameters are straightforward. + +```c++ +VkImageViewCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; +createInfo.image = swapChainImages[i]; +``` + +The `viewType` and `format` fields specify how the image data should be +interpreted. The `viewType` parameter allows you to treat images as 1D textures, +2D textures, 3D textures and cube maps. + +```c++ +createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; +createInfo.format = swapChainImageFormat; +``` + +The `components` field allows you to swizzle the color channels around. For +example, you can map all of the channels to the red channel for a monochrome +texture. You can also map constant values of `0` and `1` to a channel. In our +case we'll stick to the default mapping. + +```c++ +createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; +createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; +createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; +createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; +``` + +The `subresourceRange` field describes what the image's purpose is and which +part of the image should be accessed. Our images will be used as color targets +without any mipmapping levels or multiple layers. + +```c++ +createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +createInfo.subresourceRange.baseMipLevel = 0; +createInfo.subresourceRange.levelCount = 1; +createInfo.subresourceRange.baseArrayLayer = 0; +createInfo.subresourceRange.layerCount = 1; +``` + +If you were working on a stereographic 3D application, then you would create a +swap chain with multiple layers. You could then create multiple image views for +each image representing the views for the left and right eyes by accessing +different layers. + +Creating the image view is now a matter of calling `vkCreateImageView`: + +```c++ +if (vkCreateImageView(device, &createInfo, nullptr, &swapChainImageViews[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create image views!"); +} +``` + +Unlike images, the image views were explicitly created by us, so we need to add +a similar loop to destroy them again at the end of the program: + +```c++ +void cleanup() { + for (auto imageView : swapChainImageViews) { + vkDestroyImageView(device, imageView, nullptr); + } + + ... +} +``` + +An image view is sufficient to start using an image as a texture, but it's not +quite ready to be used as a render target just yet. That requires one more step +of indirection, known as a framebuffer. But first we'll have to set up the +graphics pipeline. + +[C++ code](/code/07_image_views.cpp) diff --git a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md new file mode 100644 index 00000000..9ee7f739 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md @@ -0,0 +1,99 @@ +Over the course of the next few chapters we'll be setting up a graphics pipeline +that is configured to draw our first triangle. The graphics pipeline is the +sequence of operations that take the vertices and textures of your meshes all +the way to the pixels in the render targets. A simplified overview is displayed +below: + +![](/images/vulkan_simplified_pipeline.svg) + +The *input assembler* collects the raw vertex data from the buffers you specify +and may also use an index buffer to repeat certain elements without having to +duplicate the vertex data itself. + +The *vertex shader* is run for every vertex and generally applies +transformations to turn vertex positions from model space to screen space. It +also passes per-vertex data down the pipeline. + +The *tessellation shaders* allow you to subdivide geometry based on certain +rules to increase the mesh quality. This is often used to make surfaces like +brick walls and staircases look less flat when they are nearby. + +The *geometry shader* is run on every primitive (triangle, line, point) and can +discard it or output more primitives than came in. This is similar to the +tessellation shader, but much more flexible. However, it is not used much in +today's applications because the performance is not that good on most graphics +cards except for Intel's integrated GPUs. + +The *rasterization* stage discretizes the primitives into *fragments*. These are +the pixel elements that they fill on the framebuffer. Any fragments that fall +outside the screen are discarded and the attributes outputted by the vertex +shader are interpolated across the fragments, as shown in the figure. Usually +the fragments that are behind other primitive fragments are also discarded here +because of depth testing. + +The *fragment shader* is invoked for every fragment that survives and determines +which framebuffer(s) the fragments are written to and with which color and depth +values. It can do this using the interpolated data from the vertex shader, which +can include things like texture coordinates and normals for lighting. + +The *color blending* stage applies operations to mix different fragments that +map to the same pixel in the framebuffer. Fragments can simply overwrite each +other, add up or be mixed based upon transparency. + +Stages with a green color are known as *fixed-function* stages. These stages +allow you to tweak their operations using parameters, but the way they work is +predefined. + +Stages with an orange color on the other hand are `programmable`, which means +that you can upload your own code to the graphics card to apply exactly the +operations you want. This allows you to use fragment shaders, for example, to +implement anything from texturing and lighting to ray tracers. These programs +run on many GPU cores simultaneously to process many objects, like vertices and +fragments in parallel. + +If you've used older APIs like OpenGL and Direct3D before, then you'll be used +to being able to change any pipeline settings at will with calls like +`glBlendFunc` and `OMSetBlendState`. The graphics pipeline in Vulkan is almost +completely immutable, so you must recreate the pipeline from scratch if you want +to change shaders, bind different framebuffers or change the blend function. The +disadvantage is that you'll have to create a number of pipelines that represent +all of the different combinations of states you want to use in your rendering +operations. However, because all of the operations you'll be doing in the +pipeline are known in advance, the driver can optimize for it much better. + +Some of the programmable stages are optional based on what you intend to do. For +example, the tessellation and geometry stages can be disabled if you are just +drawing simple geometry. If you are only interested in depth values then you can +disable the fragment shader stage, which is useful for [shadow map](https://en.wikipedia.org/wiki/Shadow_mapping) +generation. + +In the next chapter we'll first create the two programmable stages required to +put a triangle onto the screen: the vertex shader and fragment shader. The +fixed-function configuration like blending mode, viewport, rasterization will be +set up in the chapter after that. The final part of setting up the graphics +pipeline in Vulkan involves the specification of input and output framebuffers. + +Create a `createGraphicsPipeline` function that is called right after +`createImageViews` in `initVulkan`. We'll work on this function throughout the +following chapters. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createGraphicsPipeline(); +} + +... + +void createGraphicsPipeline() { + +} +``` + +[C++ code](/code/08_graphics_pipeline.cpp) diff --git a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md new file mode 100644 index 00000000..ef12e836 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md @@ -0,0 +1,467 @@ +Unlike earlier APIs, shader code in Vulkan has to be specified in a bytecode +format as opposed to human-readable syntax like [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language) +and [HLSL](https://en.wikipedia.org/wiki/High-Level_Shading_Language). This +bytecode format is called [SPIR-V](https://www.khronos.org/spir) and is designed +to be used with both Vulkan and OpenCL (both Khronos APIs). It is a format that +can be used to write graphics and compute shaders, but we will focus on shaders +used in Vulkan's graphics pipelines in this tutorial. + +The advantage of using a bytecode format is that the compilers written by GPU +vendors to turn shader code into native code are significantly less complex. The +past has shown that with human-readable syntax like GLSL, some GPU vendors were +rather flexible with their interpretation of the standard. If you happen to +write non-trivial shaders with a GPU from one of these vendors, then you'd risk +other vendor's drivers rejecting your code due to syntax errors, or worse, your +shader running differently because of compiler bugs. With a straightforward +bytecode format like SPIR-V that will hopefully be avoided. + +However, that does not mean that we need to write this bytecode by hand. Khronos +has released their own vendor-independent compiler that compiles GLSL to SPIR-V. +This compiler is designed to verify that your shader code is fully standards +compliant and produces one SPIR-V binary that you can ship with your program. +You can also include this compiler as a library to produce SPIR-V at runtime, +but we won't be doing that in this tutorial. Although we can use this compiler directly via `glslangValidator.exe`, we will be using `glslc.exe` by Google instead. The advantage of `glslc` is that it uses the same parameter format as well-known compilers like GCC and Clang and includes some extra functionality like *includes*. Both of them are already included in the Vulkan SDK, so you don't need to download anything extra. + +GLSL is a shading language with a C-style syntax. Programs written in it have a +`main` function that is invoked for every object. Instead of using parameters +for input and a return value as output, GLSL uses global variables to handle +input and output. The language includes many features to aid in graphics +programming, like built-in vector and matrix primitives. Functions for +operations like cross products, matrix-vector products and reflections around a +vector are included. The vector type is called `vec` with a number indicating +the amount of elements. For example, a 3D position would be stored in a `vec3`. +It is possible to access single components through members like `.x`, but it's +also possible to create a new vector from multiple components at the same time. +For example, the expression `vec3(1.0, 2.0, 3.0).xy` would result in `vec2`. The +constructors of vectors can also take combinations of vector objects and scalar +values. For example, a `vec3` can be constructed with +`vec3(vec2(1.0, 2.0), 3.0)`. + +As the previous chapter mentioned, we need to write a vertex shader and a +fragment shader to get a triangle on the screen. The next two sections will +cover the GLSL code of each of those and after that I'll show you how to produce +two SPIR-V binaries and load them into the program. + +## Vertex shader + +The vertex shader processes each incoming vertex. It takes its attributes, like +model space position, color, normal and texture coordinates as input. The output is +the final position in clip coordinates and the attributes that need to be passed +on to the fragment shader, like color and texture coordinates. These values will +then be interpolated over the fragments by the rasterizer to produce a smooth +gradient. + +A *clip coordinate* is a four dimensional vector from the vertex shader that is +subsequently turned into a *normalized device coordinate* by dividing the whole +vector by its last component. These normalized device coordinates are +[homogeneous coordinates](https://en.wikipedia.org/wiki/Homogeneous_coordinates) +that map the framebuffer to a [-1, 1] by [-1, 1] coordinate system that looks +like the following: + +![](/images/normalized_device_coordinates.svg) + +You should already be familiar with these if you have dabbled in computer +graphics before. If you have used OpenGL before, then you'll notice that the +sign of the Y coordinates is now flipped. The Z coordinate now uses the same +range as it does in Direct3D, from 0 to 1. + +For our first triangle we won't be applying any transformations, we'll just +specify the positions of the three vertices directly as normalized device +coordinates to create the following shape: + +![](/images/triangle_coordinates.svg) + +We can directly output normalized device coordinates by outputting them as clip +coordinates from the vertex shader with the last component set to `1`. That way +the division to transform clip coordinates to normalized device coordinates will +not change anything. + +Normally these coordinates would be stored in a vertex buffer, but creating a +vertex buffer in Vulkan and filling it with data is not trivial. Therefore I've +decided to postpone that until after we've had the satisfaction of seeing a +triangle pop up on the screen. We're going to do something a little unorthodox +in the meanwhile: include the coordinates directly inside the vertex shader. The +code looks like this: + +```glsl +#version 450 + +vec2 positions[3] = vec2[]( + vec2(0.0, -0.5), + vec2(0.5, 0.5), + vec2(-0.5, 0.5) +); + +void main() { + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); +} +``` + +The `main` function is invoked for every vertex. The built-in `gl_VertexIndex` +variable contains the index of the current vertex. This is usually an index into +the vertex buffer, but in our case it will be an index into a hardcoded array +of vertex data. The position of each vertex is accessed from the constant array +in the shader and combined with dummy `z` and `w` components to produce a +position in clip coordinates. The built-in variable `gl_Position` functions as +the output. + +## Fragment shader + +The triangle that is formed by the positions from the vertex shader fills an +area on the screen with fragments. The fragment shader is invoked on these +fragments to produce a color and depth for the framebuffer (or framebuffers). A +simple fragment shader that outputs the color red for the entire triangle looks +like this: + +```glsl +#version 450 + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(1.0, 0.0, 0.0, 1.0); +} +``` + +The `main` function is called for every fragment just like the vertex shader +`main` function is called for every vertex. Colors in GLSL are 4-component +vectors with the R, G, B and alpha channels within the [0, 1] range. Unlike +`gl_Position` in the vertex shader, there is no built-in variable to output a +color for the current fragment. You have to specify your own output variable for +each framebuffer where the `layout(location = 0)` modifier specifies the index +of the framebuffer. The color red is written to this `outColor` variable that is +linked to the first (and only) framebuffer at index `0`. + +## Per-vertex colors + +Making the entire triangle red is not very interesting, wouldn't something like +the following look a lot nicer? + +![](/images/triangle_coordinates_colors.png) + +We have to make a couple of changes to both shaders to accomplish this. First +off, we need to specify a distinct color for each of the three vertices. The +vertex shader should now include an array with colors just like it does for +positions: + +```glsl +vec3 colors[3] = vec3[]( + vec3(1.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(0.0, 0.0, 1.0) +); +``` + +Now we just need to pass these per-vertex colors to the fragment shader so it +can output their interpolated values to the framebuffer. Add an output for color +to the vertex shader and write to it in the `main` function: + +```glsl +layout(location = 0) out vec3 fragColor; + +void main() { + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); + fragColor = colors[gl_VertexIndex]; +} +``` + +Next, we need to add a matching input in the fragment shader: + +```glsl +layout(location = 0) in vec3 fragColor; + +void main() { + outColor = vec4(fragColor, 1.0); +} +``` + +The input variable does not necessarily have to use the same name, they will be +linked together using the indexes specified by the `location` directives. The +`main` function has been modified to output the color along with an alpha value. +As shown in the image above, the values for `fragColor` will be automatically +interpolated for the fragments between the three vertices, resulting in a smooth +gradient. + +## Compiling the shaders + +Create a directory called `shaders` in the root directory of your project and +store the vertex shader in a file called `shader.vert` and the fragment shader +in a file called `shader.frag` in that directory. GLSL shaders don't have an +official extension, but these two are commonly used to distinguish them. + +The contents of `shader.vert` should be: + +```glsl +#version 450 + +layout(location = 0) out vec3 fragColor; + +vec2 positions[3] = vec2[]( + vec2(0.0, -0.5), + vec2(0.5, 0.5), + vec2(-0.5, 0.5) +); + +vec3 colors[3] = vec3[]( + vec3(1.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(0.0, 0.0, 1.0) +); + +void main() { + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); + fragColor = colors[gl_VertexIndex]; +} +``` + +And the contents of `shader.frag` should be: + +```glsl +#version 450 + +layout(location = 0) in vec3 fragColor; + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(fragColor, 1.0); +} +``` + +We're now going to compile these into SPIR-V bytecode using the +`glslc` program. + +**Windows** + +Create a `compile.bat` file with the following contents: + +```bash +C:/VulkanSDK/x.x.x.x/Bin/glslc.exe shader.vert -o vert.spv +C:/VulkanSDK/x.x.x.x/Bin/glslc.exe shader.frag -o frag.spv +pause +``` + +Replace the path to `glslc.exe` with the path to where you installed +the Vulkan SDK. Double click the file to run it. + +**Linux** + +Create a `compile.sh` file with the following contents: + +```bash +/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.vert -o vert.spv +/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.frag -o frag.spv +``` + +Replace the path to `glslc` with the path to where you installed the +Vulkan SDK. Make the script executable with `chmod +x compile.sh` and run it. + +**End of platform-specific instructions** + +These two commands tell the compiler to read the GLSL source file and output a SPIR-V bytecode file using the `-o` (output) flag. + +If your shader contains a syntax error then the compiler will tell you the line +number and problem, as you would expect. Try leaving out a semicolon for example +and run the compile script again. Also try running the compiler without any +arguments to see what kinds of flags it supports. It can, for example, also +output the bytecode into a human-readable format so you can see exactly what +your shader is doing and any optimizations that have been applied at this stage. + +Compiling shaders on the commandline is one of the most straightforward options and it's the one that we'll use in this tutorial, but it's also possible to compile shaders directly from your own code. The Vulkan SDK includes [libshaderc](https://github.com/google/shaderc), which is a library to compile GLSL code to SPIR-V from within your program. + +## Loading a shader + +Now that we have a way of producing SPIR-V shaders, it's time to load them into +our program to plug them into the graphics pipeline at some point. We'll first +write a simple helper function to load the binary data from the files. + +```c++ +#include + +... + +static std::vector readFile(const std::string& filename) { + std::ifstream file(filename, std::ios::ate | std::ios::binary); + + if (!file.is_open()) { + throw std::runtime_error("failed to open file!"); + } +} +``` + +The `readFile` function will read all of the bytes from the specified file and +return them in a byte array managed by `std::vector`. We start by opening the +file with two flags: + +* `ate`: Start reading at the end of the file +* `binary`: Read the file as binary file (avoid text transformations) + +The advantage of starting to read at the end of the file is that we can use the +read position to determine the size of the file and allocate a buffer: + +```c++ +size_t fileSize = (size_t) file.tellg(); +std::vector buffer(fileSize); +``` + +After that, we can seek back to the beginning of the file and read all of the +bytes at once: + +```c++ +file.seekg(0); +file.read(buffer.data(), fileSize); +``` + +And finally close the file and return the bytes: + +```c++ +file.close(); + +return buffer; +``` + +We'll now call this function from `createGraphicsPipeline` to load the bytecode +of the two shaders: + +```c++ +void createGraphicsPipeline() { + auto vertShaderCode = readFile("shaders/vert.spv"); + auto fragShaderCode = readFile("shaders/frag.spv"); +} +``` + +Make sure that the shaders are loaded correctly by printing the size of the +buffers and checking if they match the actual file size in bytes. Note that the code doesn't need to be null terminated since it's binary code and we will later be explicit about its size. + +## Creating shader modules + +Before we can pass the code to the pipeline, we have to wrap it in a +`VkShaderModule` object. Let's create a helper function `createShaderModule` to +do that. + +```c++ +VkShaderModule createShaderModule(const std::vector& code) { + +} +``` + +The function will take a buffer with the bytecode as parameter and create a +`VkShaderModule` from it. + +Creating a shader module is simple, we only need to specify a pointer to the +buffer with the bytecode and the length of it. This information is specified in +a `VkShaderModuleCreateInfo` structure. The one catch is that the size of the +bytecode is specified in bytes, but the bytecode pointer is a `uint32_t` pointer +rather than a `char` pointer. Therefore we will need to cast the pointer with +`reinterpret_cast` as shown below. When you perform a cast like this, you also +need to ensure that the data satisfies the alignment requirements of `uint32_t`. +Lucky for us, the data is stored in an `std::vector` where the default allocator +already ensures that the data satisfies the worst case alignment requirements. + +```c++ +VkShaderModuleCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; +createInfo.codeSize = code.size(); +createInfo.pCode = reinterpret_cast(code.data()); +``` + +The `VkShaderModule` can then be created with a call to `vkCreateShaderModule`: + +```c++ +VkShaderModule shaderModule; +if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) { + throw std::runtime_error("failed to create shader module!"); +} +``` + +The parameters are the same as those in previous object creation functions: the +logical device, pointer to create info structure, optional pointer to custom +allocators and handle output variable. The buffer with the code can be freed +immediately after creating the shader module. Don't forget to return the created +shader module: + +```c++ +return shaderModule; +``` + +Shader modules are just a thin wrapper around the shader bytecode that we've previously loaded from a file and the functions defined in it. The compilation and linking of the SPIR-V bytecode to machine code for execution by the GPU doesn't happen until the graphics pipeline is created. That means that we're allowed to destroy the shader modules again as soon as pipeline creation is finished, which is why we'll make them local variables in the `createGraphicsPipeline` function instead of class members: + +```c++ +void createGraphicsPipeline() { + auto vertShaderCode = readFile("shaders/vert.spv"); + auto fragShaderCode = readFile("shaders/frag.spv"); + + VkShaderModule vertShaderModule = createShaderModule(vertShaderCode); + VkShaderModule fragShaderModule = createShaderModule(fragShaderCode); +``` + +The cleanup should then happen at the end of the function by adding two calls to `vkDestroyShaderModule`. All of the remaining code in this chapter will be inserted before these lines. + +```c++ + ... + vkDestroyShaderModule(device, fragShaderModule, nullptr); + vkDestroyShaderModule(device, vertShaderModule, nullptr); +} +``` + +## Shader stage creation + +To actually use the shaders we'll need to assign them to a specific pipeline stage through `VkPipelineShaderStageCreateInfo` structures as part of the actual pipeline creation process. + +We'll start by filling in the structure for the vertex shader, again in the +`createGraphicsPipeline` function. + +```c++ +VkPipelineShaderStageCreateInfo vertShaderStageInfo{}; +vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; +vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; +``` + +The first step, besides the obligatory `sType` member, is telling Vulkan in +which pipeline stage the shader is going to be used. There is an enum value for +each of the programmable stages described in the previous chapter. + +```c++ +vertShaderStageInfo.module = vertShaderModule; +vertShaderStageInfo.pName = "main"; +``` + +The next two members specify the shader module containing the code, and the +function to invoke, known as the *entrypoint*. That means that it's possible to combine multiple fragment +shaders into a single shader module and use different entry points to +differentiate between their behaviors. In this case we'll stick to the standard +`main`, however. + +There is one more (optional) member, `pSpecializationInfo`, which we won't be +using here, but is worth discussing. It allows you to specify values for shader +constants. You can use a single shader module where its behavior can be +configured at pipeline creation by specifying different values for the constants +used in it. This is more efficient than configuring the shader using variables +at render time, because the compiler can do optimizations like eliminating `if` +statements that depend on these values. If you don't have any constants like +that, then you can set the member to `nullptr`, which our struct initialization +does automatically. + +Modifying the structure to suit the fragment shader is easy: + +```c++ +VkPipelineShaderStageCreateInfo fragShaderStageInfo{}; +fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; +fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT; +fragShaderStageInfo.module = fragShaderModule; +fragShaderStageInfo.pName = "main"; +``` + +Finish by defining an array that contains these two structs, which we'll later +use to reference them in the actual pipeline creation step. + +```c++ +VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; +``` + +That's all there is to describing the programmable stages of the pipeline. In +the next chapter we'll look at the fixed-function stages. + +[C++ code](/code/09_shader_modules.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md new file mode 100644 index 00000000..5b4bfdec --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md @@ -0,0 +1,439 @@ + +The older graphics APIs provided default state for most of the stages of the +graphics pipeline. In Vulkan you have to be explicit about most pipeline states as +it'll be baked into an immutable pipeline state object. In this chapter we'll fill +in all of the structures to configure these fixed-function operations. + +## Dynamic state + +While *most* of the pipeline state needs to be baked into the pipeline state, +a limited amount of the state *can* actually be changed without recreating the +pipeline at draw time. Examples are the size of the viewport, line width +and blend constants. If you want to use dynamic state and keep these properties out, +then you'll have to fill in a `VkPipelineDynamicStateCreateInfo` structure like this: + +```c++ +std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR +}; + +VkPipelineDynamicStateCreateInfo dynamicState{}; +dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; +dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); +dynamicState.pDynamicStates = dynamicStates.data(); +``` + +This will cause the configuration of these values to be ignored and you will be +able (and required) to specify the data at drawing time. This results in a more flexible +setup and is very common for things like viewport and scissor state, which would +result in a more complex setup when being baked into the pipeline state. + +## Vertex input + +The `VkPipelineVertexInputStateCreateInfo` structure describes the format of the +vertex data that will be passed to the vertex shader. It describes this in +roughly two ways: + +* Bindings: spacing between data and whether the data is per-vertex or +per-instance (see [instancing](https://en.wikipedia.org/wiki/Geometry_instancing)) +* Attribute descriptions: type of the attributes passed to the vertex shader, +which binding to load them from and at which offset + +Because we're hard coding the vertex data directly in the vertex shader, we'll +fill in this structure to specify that there is no vertex data to load for now. +We'll get back to it in the vertex buffer chapter. + +```c++ +VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; +vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; +vertexInputInfo.vertexBindingDescriptionCount = 0; +vertexInputInfo.pVertexBindingDescriptions = nullptr; // Optional +vertexInputInfo.vertexAttributeDescriptionCount = 0; +vertexInputInfo.pVertexAttributeDescriptions = nullptr; // Optional +``` + +The `pVertexBindingDescriptions` and `pVertexAttributeDescriptions` members +point to an array of structs that describe the aforementioned details for +loading vertex data. Add this structure to the `createGraphicsPipeline` function +right after the `shaderStages` array. + +## Input assembly + +The `VkPipelineInputAssemblyStateCreateInfo` struct describes two things: what +kind of geometry will be drawn from the vertices and if primitive restart should +be enabled. The former is specified in the `topology` member and can have values +like: + +* `VK_PRIMITIVE_TOPOLOGY_POINT_LIST`: points from vertices +* `VK_PRIMITIVE_TOPOLOGY_LINE_LIST`: line from every 2 vertices without reuse +* `VK_PRIMITIVE_TOPOLOGY_LINE_STRIP`: the end vertex of every line is used as +start vertex for the next line +* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST`: triangle from every 3 vertices without +reuse +* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP `: the second and third vertex of every +triangle are used as first two vertices of the next triangle + +Normally, the vertices are loaded from the vertex buffer by index in sequential +order, but with an *element buffer* you can specify the indices to use yourself. +This allows you to perform optimizations like reusing vertices. If you set the +`primitiveRestartEnable` member to `VK_TRUE`, then it's possible to break up +lines and triangles in the `_STRIP` topology modes by using a special index of +`0xFFFF` or `0xFFFFFFFF`. + +We intend to draw triangles throughout this tutorial, so we'll stick to the +following data for the structure: + +```c++ +VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; +inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; +inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; +inputAssembly.primitiveRestartEnable = VK_FALSE; +``` + +## Viewports and scissors + +A viewport basically describes the region of the framebuffer that the output +will be rendered to. This will almost always be `(0, 0)` to `(width, height)` +and in this tutorial that will also be the case. + +```c++ +VkViewport viewport{}; +viewport.x = 0.0f; +viewport.y = 0.0f; +viewport.width = (float) swapChainExtent.width; +viewport.height = (float) swapChainExtent.height; +viewport.minDepth = 0.0f; +viewport.maxDepth = 1.0f; +``` + +Remember that the size of the swap chain and its images may differ from the +`WIDTH` and `HEIGHT` of the window. The swap chain images will be used as +framebuffers later on, so we should stick to their size. + +The `minDepth` and `maxDepth` values specify the range of depth values to use +for the framebuffer. These values must be within the `[0.0f, 1.0f]` range, but +`minDepth` may be higher than `maxDepth`. If you aren't doing anything special, +then you should stick to the standard values of `0.0f` and `1.0f`. + +While viewports define the transformation from the image to the framebuffer, +scissor rectangles define in which regions pixels will actually be stored. Any +pixels outside the scissor rectangles will be discarded by the rasterizer. They +function like a filter rather than a transformation. The difference is +illustrated below. Note that the left scissor rectangle is just one of the many +possibilities that would result in that image, as long as it's larger than the +viewport. + +![](/images/viewports_scissors.png) + +So if we wanted to draw to the entire framebuffer, we would specify a scissor rectangle that covers it entirely: + +```c++ +VkRect2D scissor{}; +scissor.offset = {0, 0}; +scissor.extent = swapChainExtent; +``` + +Viewport(s) and scissor rectangle(s) can either be specified as a static part of the pipeline or as a [dynamic state](#dynamic-state) set in the command buffer. While the former is more in line with the other states it's often convenient to make viewport and scissor state dynamic as it gives you a lot more flexibility. This is very common and all implementations can handle this dynamic state without a performance penalty. + +When opting for dynamic viewport(s) and scissor rectangle(s) you need to enable the respective dynamic states for the pipeline: + +```c++ +std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR +}; + +VkPipelineDynamicStateCreateInfo dynamicState{}; +dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; +dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); +dynamicState.pDynamicStates = dynamicStates.data(); +``` + +And then you only need to specify their count at pipeline creation time: + +```c++ +VkPipelineViewportStateCreateInfo viewportState{}; +viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; +viewportState.viewportCount = 1; +viewportState.scissorCount = 1; +``` + +The actual viewport(s) and scissor rectangle(s) will then later be set up at drawing time. + +With dynamic state it's even possible to specify different viewports and or scissor rectangles within a single command buffer. + +Without dynamic state, the viewport and scissor rectangle need to be set in the pipeline using the `VkPipelineViewportStateCreateInfo` struct. This makes the viewport and scissor rectangle for this pipeline immutable. +Any changes required to these values would require a new pipeline to be created with the new values. + +```c++ +VkPipelineViewportStateCreateInfo viewportState{}; +viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; +viewportState.viewportCount = 1; +viewportState.pViewports = &viewport; +viewportState.scissorCount = 1; +viewportState.pScissors = &scissor; +``` + +Independent of how you set them, it's possible to use multiple viewports and scissor rectangles on some graphics cards, so the structure members reference an array of them. Using multiple requires enabling a GPU feature (see logical device creation). + +## Rasterizer + +The rasterizer takes the geometry that is shaped by the vertices from the vertex +shader and turns it into fragments to be colored by the fragment shader. It also +performs [depth testing](https://en.wikipedia.org/wiki/Z-buffering), +[face culling](https://en.wikipedia.org/wiki/Back-face_culling) and the scissor +test, and it can be configured to output fragments that fill entire polygons or +just the edges (wireframe rendering). All this is configured using the +`VkPipelineRasterizationStateCreateInfo` structure. + +```c++ +VkPipelineRasterizationStateCreateInfo rasterizer{}; +rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; +rasterizer.depthClampEnable = VK_FALSE; +``` + +If `depthClampEnable` is set to `VK_TRUE`, then fragments that are beyond the +near and far planes are clamped to them as opposed to discarding them. This is +useful in some special cases like shadow maps. Using this requires enabling a +GPU feature. + +```c++ +rasterizer.rasterizerDiscardEnable = VK_FALSE; +``` + +If `rasterizerDiscardEnable` is set to `VK_TRUE`, then geometry never passes +through the rasterizer stage. This basically disables any output to the +framebuffer. + +```c++ +rasterizer.polygonMode = VK_POLYGON_MODE_FILL; +``` + +The `polygonMode` determines how fragments are generated for geometry. The +following modes are available: + +* `VK_POLYGON_MODE_FILL`: fill the area of the polygon with fragments +* `VK_POLYGON_MODE_LINE`: polygon edges are drawn as lines +* `VK_POLYGON_MODE_POINT`: polygon vertices are drawn as points + +Using any mode other than fill requires enabling a GPU feature. + +```c++ +rasterizer.lineWidth = 1.0f; +``` + +The `lineWidth` member is straightforward, it describes the thickness of lines +in terms of number of fragments. The maximum line width that is supported +depends on the hardware and any line thicker than `1.0f` requires you to enable +the `wideLines` GPU feature. + +```c++ +rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; +rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE; +``` + +The `cullMode` variable determines the type of face culling to use. You can +disable culling, cull the front faces, cull the back faces or both. The +`frontFace` variable specifies the vertex order for faces to be considered +front-facing and can be clockwise or counterclockwise. + +```c++ +rasterizer.depthBiasEnable = VK_FALSE; +rasterizer.depthBiasConstantFactor = 0.0f; // Optional +rasterizer.depthBiasClamp = 0.0f; // Optional +rasterizer.depthBiasSlopeFactor = 0.0f; // Optional +``` + +The rasterizer can alter the depth values by adding a constant value or biasing +them based on a fragment's slope. This is sometimes used for shadow mapping, but +we won't be using it. Just set `depthBiasEnable` to `VK_FALSE`. + +## Multisampling + +The `VkPipelineMultisampleStateCreateInfo` struct configures multisampling, +which is one of the ways to perform [anti-aliasing](https://en.wikipedia.org/wiki/Multisample_anti-aliasing). +It works by combining the fragment shader results of multiple polygons that +rasterize to the same pixel. This mainly occurs along edges, which is also where +the most noticeable aliasing artifacts occur. Because it doesn't need to run the +fragment shader multiple times if only one polygon maps to a pixel, it is +significantly less expensive than simply rendering to a higher resolution and +then downscaling. Enabling it requires enabling a GPU feature. + +```c++ +VkPipelineMultisampleStateCreateInfo multisampling{}; +multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; +multisampling.sampleShadingEnable = VK_FALSE; +multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; +multisampling.minSampleShading = 1.0f; // Optional +multisampling.pSampleMask = nullptr; // Optional +multisampling.alphaToCoverageEnable = VK_FALSE; // Optional +multisampling.alphaToOneEnable = VK_FALSE; // Optional +``` + +We'll revisit multisampling in later chapter, for now let's keep it disabled. + +## Depth and stencil testing + +If you are using a depth and/or stencil buffer, then you also need to configure +the depth and stencil tests using `VkPipelineDepthStencilStateCreateInfo`. We +don't have one right now, so we can simply pass a `nullptr` instead of a pointer +to such a struct. We'll get back to it in the depth buffering chapter. + +## Color blending + +After a fragment shader has returned a color, it needs to be combined with the +color that is already in the framebuffer. This transformation is known as color +blending and there are two ways to do it: + +* Mix the old and new value to produce a final color +* Combine the old and new value using a bitwise operation + +There are two types of structs to configure color blending. The first struct, +`VkPipelineColorBlendAttachmentState` contains the configuration per attached +framebuffer and the second struct, `VkPipelineColorBlendStateCreateInfo` +contains the *global* color blending settings. In our case we only have one +framebuffer: + +```c++ +VkPipelineColorBlendAttachmentState colorBlendAttachment{}; +colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; +colorBlendAttachment.blendEnable = VK_FALSE; +colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; // Optional +colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional +colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; // Optional +colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; // Optional +colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional +colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // Optional +``` + +This per-framebuffer struct allows you to configure the first way of color +blending. The operations that will be performed are best demonstrated using the +following pseudocode: + +```c++ +if (blendEnable) { + finalColor.rgb = (srcColorBlendFactor * newColor.rgb) (dstColorBlendFactor * oldColor.rgb); + finalColor.a = (srcAlphaBlendFactor * newColor.a) (dstAlphaBlendFactor * oldColor.a); +} else { + finalColor = newColor; +} + +finalColor = finalColor & colorWriteMask; +``` + +If `blendEnable` is set to `VK_FALSE`, then the new color from the fragment +shader is passed through unmodified. Otherwise, the two mixing operations are +performed to compute a new color. The resulting color is AND'd with the +`colorWriteMask` to determine which channels are actually passed through. + +The most common way to use color blending is to implement alpha blending, where +we want the new color to be blended with the old color based on its opacity. The +`finalColor` should then be computed as follows: + +```c++ +finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor; +finalColor.a = newAlpha.a; +``` + +This can be accomplished with the following parameters: + +```c++ +colorBlendAttachment.blendEnable = VK_TRUE; +colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; +colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; +colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; +colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; +colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; +colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; +``` + +You can find all of the possible operations in the `VkBlendFactor` and +`VkBlendOp` enumerations in the specification. + +The second structure references the array of structures for all of the +framebuffers and allows you to set blend constants that you can use as blend +factors in the aforementioned calculations. + +```c++ +VkPipelineColorBlendStateCreateInfo colorBlending{}; +colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; +colorBlending.logicOpEnable = VK_FALSE; +colorBlending.logicOp = VK_LOGIC_OP_COPY; // Optional +colorBlending.attachmentCount = 1; +colorBlending.pAttachments = &colorBlendAttachment; +colorBlending.blendConstants[0] = 0.0f; // Optional +colorBlending.blendConstants[1] = 0.0f; // Optional +colorBlending.blendConstants[2] = 0.0f; // Optional +colorBlending.blendConstants[3] = 0.0f; // Optional +``` + +If you want to use the second method of blending (bitwise combination), then you +should set `logicOpEnable` to `VK_TRUE`. The bitwise operation can then be +specified in the `logicOp` field. Note that this will automatically disable the +first method, as if you had set `blendEnable` to `VK_FALSE` for every +attached framebuffer! The `colorWriteMask` will also be used in this mode to +determine which channels in the framebuffer will actually be affected. It is +also possible to disable both modes, as we've done here, in which case the +fragment colors will be written to the framebuffer unmodified. + +## Pipeline layout + +You can use `uniform` values in shaders, which are globals similar to dynamic +state variables that can be changed at drawing time to alter the behavior of +your shaders without having to recreate them. They are commonly used to pass the +transformation matrix to the vertex shader, or to create texture samplers in the +fragment shader. + +These uniform values need to be specified during pipeline creation by creating a +`VkPipelineLayout` object. Even though we won't be using them until a future +chapter, we are still required to create an empty pipeline layout. + +Create a class member to hold this object, because we'll refer to it from other +functions at a later point in time: + +```c++ +VkPipelineLayout pipelineLayout; +``` + +And then create the object in the `createGraphicsPipeline` function: + +```c++ +VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; +pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; +pipelineLayoutInfo.setLayoutCount = 0; // Optional +pipelineLayoutInfo.pSetLayouts = nullptr; // Optional +pipelineLayoutInfo.pushConstantRangeCount = 0; // Optional +pipelineLayoutInfo.pPushConstantRanges = nullptr; // Optional + +if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) { + throw std::runtime_error("failed to create pipeline layout!"); +} +``` + +The structure also specifies *push constants*, which are another way of passing +dynamic values to shaders that we may get into in a future chapter. The pipeline +layout will be referenced throughout the program's lifetime, so it should be +destroyed at the end: + +```c++ +void cleanup() { + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + ... +} +``` + +## Conclusion + +That's it for all of the fixed-function state! It's a lot of work to set all of +this up from scratch, but the advantage is that we're now nearly fully aware of +everything that is going on in the graphics pipeline! This reduces the chance of +running into unexpected behavior because the default state of certain components +is not what you expect. + +There is however one more object to create before we can finally create the +graphics pipeline and that is a [render pass](!en/Drawing_a_triangle/Graphics_pipeline_basics/Render_passes). + +[C++ code](/code/10_fixed_functions.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md new file mode 100644 index 00000000..a635d32f --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md @@ -0,0 +1,215 @@ +## Setup + +Before we can finish creating the pipeline, we need to tell Vulkan about the +framebuffer attachments that will be used while rendering. We need to specify +how many color and depth buffers there will be, how many samples to use for each +of them and how their contents should be handled throughout the rendering +operations. All of this information is wrapped in a *render pass* object, for +which we'll create a new `createRenderPass` function. Call this function from +`initVulkan` before `createGraphicsPipeline`. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); +} + +... + +void createRenderPass() { + +} +``` + +## Attachment description + +In our case we'll have just a single color buffer attachment represented by one +of the images from the swap chain. + +```c++ +void createRenderPass() { + VkAttachmentDescription colorAttachment{}; + colorAttachment.format = swapChainImageFormat; + colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; +} +``` + +The `format` of the color attachment should match the format of the swap chain +images, and we're not doing anything with multisampling yet, so we'll stick to 1 +sample. + +```c++ +colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; +colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; +``` + +The `loadOp` and `storeOp` determine what to do with the data in the attachment +before rendering and after rendering. We have the following choices for +`loadOp`: + +* `VK_ATTACHMENT_LOAD_OP_LOAD`: Preserve the existing contents of the attachment +* `VK_ATTACHMENT_LOAD_OP_CLEAR`: Clear the values to a constant at the start +* `VK_ATTACHMENT_LOAD_OP_DONT_CARE`: Existing contents are undefined; we don't +care about them + +In our case we're going to use the clear operation to clear the framebuffer to +black before drawing a new frame. There are only two possibilities for the +`storeOp`: + +* `VK_ATTACHMENT_STORE_OP_STORE`: Rendered contents will be stored in memory and +can be read later +* `VK_ATTACHMENT_STORE_OP_DONT_CARE`: Contents of the framebuffer will be +undefined after the rendering operation + +We're interested in seeing the rendered triangle on the screen, so we're going +with the store operation here. + +```c++ +colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; +colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; +``` + +The `loadOp` and `storeOp` apply to color and depth data, and `stencilLoadOp` / +`stencilStoreOp` apply to stencil data. Our application won't do anything with +the stencil buffer, so the results of loading and storing are irrelevant. + +```c++ +colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; +colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; +``` + +Textures and framebuffers in Vulkan are represented by `VkImage` objects with a +certain pixel format, however the layout of the pixels in memory can change +based on what you're trying to do with an image. + +Some of the most common layouts are: + +* `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`: Images used as color attachment +* `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`: Images to be presented in the swap chain +* `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`: Images to be used as destination for a +memory copy operation + +We'll discuss this topic in more depth in the texturing chapter, but what's +important to know right now is that images need to be transitioned to specific +layouts that are suitable for the operation that they're going to be involved in +next. + +The `initialLayout` specifies which layout the image will have before the render +pass begins. The `finalLayout` specifies the layout to automatically transition +to when the render pass finishes. Using `VK_IMAGE_LAYOUT_UNDEFINED` for +`initialLayout` means that we don't care what previous layout the image was in. +The caveat of this special value is that the contents of the image are not +guaranteed to be preserved, but that doesn't matter since we're going to clear +it anyway. We want the image to be ready for presentation using the swap chain +after rendering, which is why we use `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR` as +`finalLayout`. + +## Subpasses and attachment references + +A single render pass can consist of multiple subpasses. Subpasses are subsequent +rendering operations that depend on the contents of framebuffers in previous +passes, for example a sequence of post-processing effects that are applied one +after another. If you group these rendering operations into one render pass, +then Vulkan is able to reorder the operations and conserve memory bandwidth for +possibly better performance. For our very first triangle, however, we'll stick +to a single subpass. + +Every subpass references one or more of the attachments that we've described +using the structure in the previous sections. These references are themselves +`VkAttachmentReference` structs that look like this: + +```c++ +VkAttachmentReference colorAttachmentRef{}; +colorAttachmentRef.attachment = 0; +colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; +``` + +The `attachment` parameter specifies which attachment to reference by its index +in the attachment descriptions array. Our array consists of a single +`VkAttachmentDescription`, so its index is `0`. The `layout` specifies which +layout we would like the attachment to have during a subpass that uses this +reference. Vulkan will automatically transition the attachment to this layout +when the subpass is started. We intend to use the attachment to function as a +color buffer and the `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL` layout will give +us the best performance, as its name implies. + +The subpass is described using a `VkSubpassDescription` structure: + +```c++ +VkSubpassDescription subpass{}; +subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; +``` + +Vulkan may also support compute subpasses in the future, so we have to be +explicit about this being a graphics subpass. Next, we specify the reference to +the color attachment: + +```c++ +subpass.colorAttachmentCount = 1; +subpass.pColorAttachments = &colorAttachmentRef; +``` + +The index of the attachment in this array is directly referenced from the +fragment shader with the `layout(location = 0) out vec4 outColor` directive! + +The following other types of attachments can be referenced by a subpass: + +* `pInputAttachments`: Attachments that are read from a shader +* `pResolveAttachments`: Attachments used for multisampling color attachments +* `pDepthStencilAttachment`: Attachment for depth and stencil data +* `pPreserveAttachments`: Attachments that are not used by this subpass, but for +which the data must be preserved + +## Render pass + +Now that the attachment and a basic subpass referencing it have been described, +we can create the render pass itself. Create a new class member variable to hold +the `VkRenderPass` object right above the `pipelineLayout` variable: + +```c++ +VkRenderPass renderPass; +VkPipelineLayout pipelineLayout; +``` + +The render pass object can then be created by filling in the +`VkRenderPassCreateInfo` structure with an array of attachments and subpasses. +The `VkAttachmentReference` objects reference attachments using the indices of +this array. + +```c++ +VkRenderPassCreateInfo renderPassInfo{}; +renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; +renderPassInfo.attachmentCount = 1; +renderPassInfo.pAttachments = &colorAttachment; +renderPassInfo.subpassCount = 1; +renderPassInfo.pSubpasses = &subpass; + +if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) { + throw std::runtime_error("failed to create render pass!"); +} +``` + +Just like the pipeline layout, the render pass will be referenced throughout the +program, so it should only be cleaned up at the end: + +```c++ +void cleanup() { + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + vkDestroyRenderPass(device, renderPass, nullptr); + ... +} +``` + +That was a lot of work, but in the next chapter it all comes together to finally +create the graphics pipeline object! + +[C++ code](/code/11_render_passes.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md new file mode 100644 index 00000000..4a16585e --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md @@ -0,0 +1,122 @@ +We can now combine all of the structures and objects from the previous chapters +to create the graphics pipeline! Here's the types of objects we have now, as a +quick recap: + +* Shader stages: the shader modules that define the functionality of the +programmable stages of the graphics pipeline +* Fixed-function state: all of the structures that define the fixed-function +stages of the pipeline, like input assembly, rasterizer, viewport and color +blending +* Pipeline layout: the uniform and push values referenced by the shader that can +be updated at draw time +* Render pass: the attachments referenced by the pipeline stages and their usage + +All of these combined fully define the functionality of the graphics pipeline, +so we can now begin filling in the `VkGraphicsPipelineCreateInfo` structure at +the end of the `createGraphicsPipeline` function. But before the calls to +`vkDestroyShaderModule` because these are still to be used during the creation. + +```c++ +VkGraphicsPipelineCreateInfo pipelineInfo{}; +pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; +pipelineInfo.stageCount = 2; +pipelineInfo.pStages = shaderStages; +``` + +We start by referencing the array of `VkPipelineShaderStageCreateInfo` structs. + +```c++ +pipelineInfo.pVertexInputState = &vertexInputInfo; +pipelineInfo.pInputAssemblyState = &inputAssembly; +pipelineInfo.pViewportState = &viewportState; +pipelineInfo.pRasterizationState = &rasterizer; +pipelineInfo.pMultisampleState = &multisampling; +pipelineInfo.pDepthStencilState = nullptr; // Optional +pipelineInfo.pColorBlendState = &colorBlending; +pipelineInfo.pDynamicState = &dynamicState; +``` + +Then we reference all of the structures describing the fixed-function stage. + +```c++ +pipelineInfo.layout = pipelineLayout; +``` + +After that comes the pipeline layout, which is a Vulkan handle rather than a +struct pointer. + +```c++ +pipelineInfo.renderPass = renderPass; +pipelineInfo.subpass = 0; +``` + +And finally we have the reference to the render pass and the index of the sub +pass where this graphics pipeline will be used. It is also possible to use other +render passes with this pipeline instead of this specific instance, but they +have to be *compatible* with `renderPass`. The requirements for compatibility +are described [here](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap8.html#renderpass-compatibility), +but we won't be using that feature in this tutorial. + +```c++ +pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional +pipelineInfo.basePipelineIndex = -1; // Optional +``` + +There are actually two more parameters: `basePipelineHandle` and +`basePipelineIndex`. Vulkan allows you to create a new graphics pipeline by +deriving from an existing pipeline. The idea of pipeline derivatives is that it +is less expensive to set up pipelines when they have much functionality in +common with an existing pipeline and switching between pipelines from the same +parent can also be done quicker. You can either specify the handle of an +existing pipeline with `basePipelineHandle` or reference another pipeline that +is about to be created by index with `basePipelineIndex`. Right now there is +only a single pipeline, so we'll simply specify a null handle and an invalid +index. These values are only used if the `VK_PIPELINE_CREATE_DERIVATIVE_BIT` +flag is also specified in the `flags` field of `VkGraphicsPipelineCreateInfo`. + +Now prepare for the final step by creating a class member to hold the +`VkPipeline` object: + +```c++ +VkPipeline graphicsPipeline; +``` + +And finally create the graphics pipeline: + +```c++ +if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) { + throw std::runtime_error("failed to create graphics pipeline!"); +} +``` + +The `vkCreateGraphicsPipelines` function actually has more parameters than the +usual object creation functions in Vulkan. It is designed to take multiple +`VkGraphicsPipelineCreateInfo` objects and create multiple `VkPipeline` objects +in a single call. + +The second parameter, for which we've passed the `VK_NULL_HANDLE` argument, +references an optional `VkPipelineCache` object. A pipeline cache can be used to +store and reuse data relevant to pipeline creation across multiple calls to +`vkCreateGraphicsPipelines` and even across program executions if the cache is +stored to a file. This makes it possible to significantly speed up pipeline +creation at a later time. We'll get into this in the pipeline cache chapter. + +The graphics pipeline is required for all common drawing operations, so it +should also only be destroyed at the end of the program: + +```c++ +void cleanup() { + vkDestroyPipeline(device, graphicsPipeline, nullptr); + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + ... +} +``` + +Now run your program to confirm that all this hard work has resulted in a +successful pipeline creation! We are already getting quite close to seeing +something pop up on the screen. In the next couple of chapters we'll set up the +actual framebuffers from the swap chain images and prepare the drawing commands. + +[C++ code](/code/12_graphics_pipeline_complete.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko-rust/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md b/ko-rust/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md new file mode 100644 index 00000000..bf7f84a7 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md @@ -0,0 +1,107 @@ +We've talked a lot about framebuffers in the past few chapters and we've set up +the render pass to expect a single framebuffer with the same format as the swap +chain images, but we haven't actually created any yet. + +The attachments specified during render pass creation are bound by wrapping them +into a `VkFramebuffer` object. A framebuffer object references all of the +`VkImageView` objects that represent the attachments. In our case that will be +only a single one: the color attachment. However, the image that we have to use +for the attachment depends on which image the swap chain returns when we retrieve one +for presentation. That means that we have to create a framebuffer for all of the +images in the swap chain and use the one that corresponds to the retrieved image +at drawing time. + +To that end, create another `std::vector` class member to hold the framebuffers: + +```c++ +std::vector swapChainFramebuffers; +``` + +We'll create the objects for this array in a new function `createFramebuffers` +that is called from `initVulkan` right after creating the graphics pipeline: + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); +} + +... + +void createFramebuffers() { + +} +``` + +Start by resizing the container to hold all of the framebuffers: + +```c++ +void createFramebuffers() { + swapChainFramebuffers.resize(swapChainImageViews.size()); +} +``` + +We'll then iterate through the image views and create framebuffers from them: + +```c++ +for (size_t i = 0; i < swapChainImageViews.size(); i++) { + VkImageView attachments[] = { + swapChainImageViews[i] + }; + + VkFramebufferCreateInfo framebufferInfo{}; + framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + framebufferInfo.renderPass = renderPass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = attachments; + framebufferInfo.width = swapChainExtent.width; + framebufferInfo.height = swapChainExtent.height; + framebufferInfo.layers = 1; + + if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &swapChainFramebuffers[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create framebuffer!"); + } +} +``` + +As you can see, creation of framebuffers is quite straightforward. We first need +to specify with which `renderPass` the framebuffer needs to be compatible. You +can only use a framebuffer with the render passes that it is compatible with, +which roughly means that they use the same number and type of attachments. + +The `attachmentCount` and `pAttachments` parameters specify the `VkImageView` +objects that should be bound to the respective attachment descriptions in +the render pass `pAttachment` array. + +The `width` and `height` parameters are self-explanatory and `layers` refers to +the number of layers in image arrays. Our swap chain images are single images, +so the number of layers is `1`. + +We should delete the framebuffers before the image views and render pass that +they are based on, but only after we've finished rendering: + +```c++ +void cleanup() { + for (auto framebuffer : swapChainFramebuffers) { + vkDestroyFramebuffer(device, framebuffer, nullptr); + } + + ... +} +``` + +We've now reached the milestone where we have all of the objects that are +required for rendering. In the next chapter we're going to write the first +actual drawing commands. + +[C++ code](/code/13_framebuffers.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko-rust/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md b/ko-rust/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md new file mode 100644 index 00000000..61a40b4f --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md @@ -0,0 +1,344 @@ +Commands in Vulkan, like drawing operations and memory transfers, are not +executed directly using function calls. You have to record all of the operations +you want to perform in command buffer objects. The advantage of this is that when +we are ready to tell the Vulkan what we want to do, all of the commands are +submitted together and Vulkan can more efficiently process the commands since all +of them are available together. In addition, this allows command recording to +happen in multiple threads if so desired. + +## Command pools + +We have to create a command pool before we can create command buffers. Command +pools manage the memory that is used to store the buffers and command buffers +are allocated from them. Add a new class member to store a `VkCommandPool`: + +```c++ +VkCommandPool commandPool; +``` + +Then create a new function `createCommandPool` and call it from `initVulkan` +after the framebuffers were created. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); +} + +... + +void createCommandPool() { + +} +``` + +Command pool creation only takes two parameters: + +```c++ +QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice); + +VkCommandPoolCreateInfo poolInfo{}; +poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; +poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; +poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value(); +``` + +There are two possible flags for command pools: + +* `VK_COMMAND_POOL_CREATE_TRANSIENT_BIT`: Hint that command buffers are +rerecorded with new commands very often (may change memory allocation behavior) +* `VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT`: Allow command buffers to be +rerecorded individually, without this flag they all have to be reset together + +We will be recording a command buffer every frame, so we want to be able to +reset and rerecord over it. Thus, we need to set the +`VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT` flag bit for our command pool. + +Command buffers are executed by submitting them on one of the device queues, +like the graphics and presentation queues we retrieved. Each command pool can +only allocate command buffers that are submitted on a single type of queue. +We're going to record commands for drawing, which is why we've chosen the +graphics queue family. + + +```c++ +if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) { + throw std::runtime_error("failed to create command pool!"); +} +``` + +Finish creating the command pool using the `vkCreateCommandPool` function. It +doesn't have any special parameters. Commands will be used throughout the +program to draw things on the screen, so the pool should only be destroyed at +the end: + +```c++ +void cleanup() { + vkDestroyCommandPool(device, commandPool, nullptr); + + ... +} +``` + +## Command buffer allocation + +We can now start allocating command buffers. + +Create a `VkCommandBuffer` object as a class member. Command buffers +will be automatically freed when their command pool is destroyed, so we don't +need explicit cleanup. + +```c++ +VkCommandBuffer commandBuffer; +``` + +We'll now start working on a `createCommandBuffer` function to allocate a single +command buffer from the command pool. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); + createCommandBuffer(); +} + +... + +void createCommandBuffer() { + +} +``` + +Command buffers are allocated with the `vkAllocateCommandBuffers` function, +which takes a `VkCommandBufferAllocateInfo` struct as parameter that specifies +the command pool and number of buffers to allocate: + +```c++ +VkCommandBufferAllocateInfo allocInfo{}; +allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; +allocInfo.commandPool = commandPool; +allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; +allocInfo.commandBufferCount = 1; + +if (vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate command buffers!"); +} +``` + +The `level` parameter specifies if the allocated command buffers are primary or +secondary command buffers. + +* `VK_COMMAND_BUFFER_LEVEL_PRIMARY`: Can be submitted to a queue for execution, +but cannot be called from other command buffers. +* `VK_COMMAND_BUFFER_LEVEL_SECONDARY`: Cannot be submitted directly, but can be +called from primary command buffers. + +We won't make use of the secondary command buffer functionality here, but you +can imagine that it's helpful to reuse common operations from primary command +buffers. + +Since we are only allocating one command buffer, the `commandBufferCount` parameter +is just one. + +## Command buffer recording + +We'll now start working on the `recordCommandBuffer` function that writes the +commands we want to execute into a command buffer. The `VkCommandBuffer` used +will be passed in as a parameter, as well as the index of the current swapchain +image we want to write to. + +```c++ +void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) { + +} +``` + +We always begin recording a command buffer by calling `vkBeginCommandBuffer` +with a small `VkCommandBufferBeginInfo` structure as argument that specifies +some details about the usage of this specific command buffer. + +```c++ +VkCommandBufferBeginInfo beginInfo{}; +beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; +beginInfo.flags = 0; // Optional +beginInfo.pInheritanceInfo = nullptr; // Optional + +if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) { + throw std::runtime_error("failed to begin recording command buffer!"); +} +``` + +The `flags` parameter specifies how we're going to use the command buffer. The +following values are available: + +* `VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT`: The command buffer will be +rerecorded right after executing it once. +* `VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT`: This is a secondary +command buffer that will be entirely within a single render pass. +* `VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT`: The command buffer can be +resubmitted while it is also already pending execution. + +None of these flags are applicable for us right now. + +The `pInheritanceInfo` parameter is only relevant for secondary command buffers. +It specifies which state to inherit from the calling primary command buffers. + +If the command buffer was already recorded once, then a call to +`vkBeginCommandBuffer` will implicitly reset it. It's not possible to append +commands to a buffer at a later time. + +## Starting a render pass + +Drawing starts by beginning the render pass with `vkCmdBeginRenderPass`. The +render pass is configured using some parameters in a `VkRenderPassBeginInfo` +struct. + +```c++ +VkRenderPassBeginInfo renderPassInfo{}; +renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; +renderPassInfo.renderPass = renderPass; +renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex]; +``` + +The first parameters are the render pass itself and the attachments to bind. We +created a framebuffer for each swap chain image where it is specified as a color +attachment. Thus we need to bind the framebuffer for the swapchain image we want +to draw to. Using the imageIndex parameter which was passed in, we can pick the +right framebuffer for the current swapchain image. + +```c++ +renderPassInfo.renderArea.offset = {0, 0}; +renderPassInfo.renderArea.extent = swapChainExtent; +``` + +The next two parameters define the size of the render area. The render area +defines where shader loads and stores will take place. The pixels outside this +region will have undefined values. It should match the size of the attachments +for best performance. + +```c++ +VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}}; +renderPassInfo.clearValueCount = 1; +renderPassInfo.pClearValues = &clearColor; +``` + +The last two parameters define the clear values to use for +`VK_ATTACHMENT_LOAD_OP_CLEAR`, which we used as load operation for the color +attachment. I've defined the clear color to simply be black with 100% opacity. + +```c++ +vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); +``` + +The render pass can now begin. All of the functions that record commands can be +recognized by their `vkCmd` prefix. They all return `void`, so there will be no +error handling until we've finished recording. + +The first parameter for every command is always the command buffer to record the +command to. The second parameter specifies the details of the render pass we've +just provided. The final parameter controls how the drawing commands within the +render pass will be provided. It can have one of two values: + +* `VK_SUBPASS_CONTENTS_INLINE`: The render pass commands will be embedded in +the primary command buffer itself and no secondary command buffers will be +executed. +* `VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS`: The render pass commands will +be executed from secondary command buffers. + +We will not be using secondary command buffers, so we'll go with the first +option. + +## Basic drawing commands + +We can now bind the graphics pipeline: + +```c++ +vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); +``` + +The second parameter specifies if the pipeline object is a graphics or compute +pipeline. We've now told Vulkan which operations to execute in the graphics +pipeline and which attachment to use in the fragment shader. + +As noted in the [fixed functions chapter](../02_Graphics_pipeline_basics/02_Fixed_functions.md#dynamic-state), +we did specify viewport and scissor state for this pipeline to be dynamic. +So we need to set them in the command buffer before issuing our draw command: + +```c++ +VkViewport viewport{}; +viewport.x = 0.0f; +viewport.y = 0.0f; +viewport.width = static_cast(swapChainExtent.width); +viewport.height = static_cast(swapChainExtent.height); +viewport.minDepth = 0.0f; +viewport.maxDepth = 1.0f; +vkCmdSetViewport(commandBuffer, 0, 1, &viewport); + +VkRect2D scissor{}; +scissor.offset = {0, 0}; +scissor.extent = swapChainExtent; +vkCmdSetScissor(commandBuffer, 0, 1, &scissor); +``` + +Now we are ready to issue the draw command for the triangle: + +```c++ +vkCmdDraw(commandBuffer, 3, 1, 0, 0); +``` + +The actual `vkCmdDraw` function is a bit anticlimactic, but it's so simple +because of all the information we specified in advance. It has the following +parameters, aside from the command buffer: + +* `vertexCount`: Even though we don't have a vertex buffer, we technically still +have 3 vertices to draw. +* `instanceCount`: Used for instanced rendering, use `1` if you're not doing +that. +* `firstVertex`: Used as an offset into the vertex buffer, defines the lowest +value of `gl_VertexIndex`. +* `firstInstance`: Used as an offset for instanced rendering, defines the lowest +value of `gl_InstanceIndex`. + +## Finishing up + +The render pass can now be ended: + +```c++ +vkCmdEndRenderPass(commandBuffer); +``` + +And we've finished recording the command buffer: + +```c++ +if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to record command buffer!"); +} +``` + + + +In the next chapter we'll write the code for the main loop, which will acquire +an image from the swap chain, record and execute a command buffer, then return the +finished image to the swap chain. + +[C++ code](/code/14_command_buffers.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko-rust/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md b/ko-rust/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md new file mode 100644 index 00000000..233c059d --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md @@ -0,0 +1,577 @@ + +This is the chapter where everything is going to come together. We're going to +write the `drawFrame` function that will be called from the main loop to put the +triangle on the screen. Let's start by creating the function and call it from +`mainLoop`: + +```c++ +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + drawFrame(); + } +} + +... + +void drawFrame() { + +} +``` + +## Outline of a frame + +At a high level, rendering a frame in Vulkan consists of a common set of steps: + +* Wait for the previous frame to finish +* Acquire an image from the swap chain +* Record a command buffer which draws the scene onto that image +* Submit the recorded command buffer +* Present the swap chain image + +While we will expand the drawing function in later chapters, for now this is the +core of our render loop. + + + +## Synchronization + + + +A core design philosophy in Vulkan is that synchronization of execution on +the GPU is explicit. The order of operations is up to us to define using various +synchronization primitives which tell the driver the order we want things to run +in. This means that many Vulkan API calls which start executing work on the GPU +are asynchronous, the functions will return before the operation has finished. + +In this chapter there are a number of events that we need to order explicitly +because they happen on the GPU, such as: + +* Acquire an image from the swap chain +* Execute commands that draw onto the acquired image +* Present that image to the screen for presentation, returning it to the swapchain + +Each of these events is set in motion using a single function call, but are all +executed asynchronously. The function calls will return before the operations +are actually finished and the order of execution is also undefined. That is +unfortunate, because each of the operations depends on the previous one +finishing. Thus we need to explore which primitives we can use to achieve +the desired ordering. + +### Semaphores + +A semaphore is used to add order between queue operations. Queue operations +refer to the work we submit to a queue, either in a command buffer or from +within a function as we will see later. Examples of queues are the graphics +queue and the presentation queue. Semaphores are used both to order work inside +the same queue and between different queues. + +There happens to be two kinds of semaphores in Vulkan, binary and timeline. +Because only binary semaphores will be used in this tutorial, we will not +discuss timeline semaphores. Further mention of the term semaphore exclusively +refers to binary semaphores. + +A semaphore is either unsignaled or signaled. It begins life as unsignaled. The +way we use a semaphore to order queue operations is by providing the same +semaphore as a 'signal' semaphore in one queue operation and as a 'wait' +semaphore in another queue operation. For example, lets say we have semaphore S +and queue operations A and B that we want to execute in order. What we tell +Vulkan is that operation A will 'signal' semaphore S when it finishes executing, +and operation B will 'wait' on semaphore S before it begins executing. When +operation A finishes, semaphore S will be signaled, while operation B wont +start until S is signaled. After operation B begins executing, semaphore S +is automatically reset back to being unsignaled, allowing it to be used again. + +Pseudo-code of what was just described: +``` +VkCommandBuffer A, B = ... // record command buffers +VkSemaphore S = ... // create a semaphore + +// enqueue A, signal S when done - starts executing immediately +vkQueueSubmit(work: A, signal: S, wait: None) + +// enqueue B, wait on S to start +vkQueueSubmit(work: B, signal: None, wait: S) +``` + +Note that in this code snippet, both calls to `vkQueueSubmit()` return +immediately - the waiting only happens on the GPU. The CPU continues running +without blocking. To make the CPU wait, we need a different synchronization +primitive, which we will now describe. + +### Fences + +A fence has a similar purpose, in that it is used to synchronize execution, but +it is for ordering the execution on the CPU, otherwise known as the host. +Simply put, if the host needs to know when the GPU has finished something, we +use a fence. + +Similar to semaphores, fences are either in a signaled or unsignaled state. +Whenever we submit work to execute, we can attach a fence to that work. When +the work is finished, the fence will be signaled. Then we can make the host +wait for the fence to be signaled, guaranteeing that the work has finished +before the host continues. + +A concrete example is taking a screenshot. Say we have already done the +necessary work on the GPU. Now need to transfer the image from the GPU over +to the host and then save the memory to a file. We have command buffer A which +executes the transfer and fence F. We submit command buffer A with fence F, +then immediately tell the host to wait for F to signal. This causes the host to +block until command buffer A finishes execution. Thus we are safe to let the +host save the file to disk, as the memory transfer has completed. + +Pseudo-code for what was described: +``` +VkCommandBuffer A = ... // record command buffer with the transfer +VkFence F = ... // create the fence + +// enqueue A, start work immediately, signal F when done +vkQueueSubmit(work: A, fence: F) + +vkWaitForFence(F) // blocks execution until A has finished executing + +save_screenshot_to_disk() // can't run until the transfer has finished +``` + +Unlike the semaphore example, this example *does* block host execution. This +means the host won't do anything except wait until execution has finished. For +this case, we had to make sure the transfer was complete before we could save +the screenshot to disk. + +In general, it is preferable to not block the host unless necessary. We want to +feed the GPU and the host with useful work to do. Waiting on fences to signal +is not useful work. Thus we prefer semaphores, or other synchronization +primitives not yet covered, to synchronize our work. + +Fences must be reset manually to put them back into the unsignaled state. This +is because fences are used to control the execution of the host, and so the +host gets to decide when to reset the fence. Contrast this to semaphores which +are used to order work on the GPU without the host being involved. + +In summary, semaphores are used to specify the execution order of operations on +the GPU while fences are used to keep the CPU and GPU in sync with each-other. + +### What to choose? + +We have two synchronization primitives to use and conveniently two places to +apply synchronization: Swapchain operations and waiting for the previous frame +to finish. We want to use semaphores for swapchain operations because they +happen on the GPU, thus we don't want to make the host wait around if we can +help it. For waiting on the previous frame to finish, we want to use fences +for the opposite reason, because we need the host to wait. This is so we don't +draw more than one frame at a time. Because we re-record the command buffer +every frame, we cannot record the next frame's work to the command buffer +until the current frame has finished executing, as we don't want to overwrite +the current contents of the command buffer while the GPU is using it. + +## Creating the synchronization objects + +We'll need one semaphore to signal that an image has been acquired from the +swapchain and is ready for rendering, another one to signal that rendering has +finished and presentation can happen, and a fence to make sure only one frame +is rendering at a time. + +Create three class members to store these semaphore objects and fence object: + +```c++ +VkSemaphore imageAvailableSemaphore; +VkSemaphore renderFinishedSemaphore; +VkFence inFlightFence; +``` + +To create the semaphores, we'll add the last `create` function for this part of +the tutorial: `createSyncObjects`: + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); + createCommandBuffer(); + createSyncObjects(); +} + +... + +void createSyncObjects() { + +} +``` + +Creating semaphores requires filling in the `VkSemaphoreCreateInfo`, but in the +current version of the API it doesn't actually have any required fields besides +`sType`: + +```c++ +void createSyncObjects() { + VkSemaphoreCreateInfo semaphoreInfo{}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; +} +``` + +Future versions of the Vulkan API or extensions may add functionality for the +`flags` and `pNext` parameters like it does for the other structures. + +Creating a fence requires filling in the `VkFenceCreateInfo`: + +```c++ +VkFenceCreateInfo fenceInfo{}; +fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; +``` + +Creating the semaphores and fence follows the familiar pattern with +`vkCreateSemaphore` & `vkCreateFence`: + +```c++ +if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphore) != VK_SUCCESS || + vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphore) != VK_SUCCESS || + vkCreateFence(device, &fenceInfo, nullptr, &inFlightFence) != VK_SUCCESS) { + throw std::runtime_error("failed to create semaphores!"); +} +``` + +The semaphores and fence should be cleaned up at the end of the program, when +all commands have finished and no more synchronization is necessary: + +```c++ +void cleanup() { + vkDestroySemaphore(device, imageAvailableSemaphore, nullptr); + vkDestroySemaphore(device, renderFinishedSemaphore, nullptr); + vkDestroyFence(device, inFlightFence, nullptr); +``` + +Onto the main drawing function! + +## Waiting for the previous frame + +At the start of the frame, we want to wait until the previous frame has +finished, so that the command buffer and semaphores are available to use. To do +that, we call `vkWaitForFences`: + +```c++ +void drawFrame() { + vkWaitForFences(device, 1, &inFlightFence, VK_TRUE, UINT64_MAX); +} +``` + +The `vkWaitForFences` function takes an array of fences and waits on the host +for either any or all of the fences to be signaled before returning. The +`VK_TRUE` we pass here indicates that we want to wait for all fences, but in +the case of a single one it doesn't matter. This function also has a timeout +parameter that we set to the maximum value of a 64 bit unsigned integer, +`UINT64_MAX`, which effectively disables the timeout. + +After waiting, we need to manually reset the fence to the unsignaled state with +the `vkResetFences` call: +```c++ + vkResetFences(device, 1, &inFlightFence); +``` + +Before we can proceed, there is a slight hiccup in our design. On the first +frame we call `drawFrame()`, which immediately waits on `inFlightFence` to +be signaled. `inFlightFence` is only signaled after a frame has finished +rendering, yet since this is the first frame, there are no previous frames in +which to signal the fence! Thus `vkWaitForFences()` blocks indefinitely, +waiting on something which will never happen. + +Of the many solutions to this dilemma, there is a clever workaround built into +the API. Create the fence in the signaled state, so that the first call to +`vkWaitForFences()` returns immediately since the fence is already signaled. + +To do this, we add the `VK_FENCE_CREATE_SIGNALED_BIT` flag to the `VkFenceCreateInfo`: + +```c++ +void createSyncObjects() { + ... + + VkFenceCreateInfo fenceInfo{}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + + ... +} +``` + +## Acquiring an image from the swap chain + +The next thing we need to do in the `drawFrame` function is acquire an image +from the swap chain. Recall that the swap chain is an extension feature, so we +must use a function with the `vk*KHR` naming convention: + +```c++ +void drawFrame() { + ... + + uint32_t imageIndex; + vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex); +} +``` + +The first two parameters of `vkAcquireNextImageKHR` are the logical device and +the swap chain from which we wish to acquire an image. The third parameter +specifies a timeout in nanoseconds for an image to become available. Using the +maximum value of a 64 bit unsigned integer means we effectively disable the +timeout. + +The next two parameters specify synchronization objects that are to be signaled +when the presentation engine is finished using the image. That's the point in +time where we can start drawing to it. It is possible to specify a semaphore, +fence or both. We're going to use our `imageAvailableSemaphore` for that purpose +here. + +The last parameter specifies a variable to output the index of the swap chain +image that has become available. The index refers to the `VkImage` in our +`swapChainImages` array. We're going to use that index to pick the `VkFrameBuffer`. + +## Recording the command buffer + +With the imageIndex specifying the swap chain image to use in hand, we can now +record the command buffer. First, we call `vkResetCommandBuffer` on the command +buffer to make sure it is able to be recorded. + +```c++ +vkResetCommandBuffer(commandBuffer, 0); +``` + +The second parameter of `vkResetCommandBuffer` is a `VkCommandBufferResetFlagBits` +flag. Since we don't want to do anything special, we leave it as 0. + +Now call the function `recordCommandBuffer` to record the commands we want. + +```c++ +recordCommandBuffer(commandBuffer, imageIndex); +``` + +With a fully recorded command buffer, we can now submit it. + +## Submitting the command buffer + +Queue submission and synchronization is configured through parameters in the +`VkSubmitInfo` structure. + +```c++ +VkSubmitInfo submitInfo{}; +submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + +VkSemaphore waitSemaphores[] = {imageAvailableSemaphore}; +VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; +submitInfo.waitSemaphoreCount = 1; +submitInfo.pWaitSemaphores = waitSemaphores; +submitInfo.pWaitDstStageMask = waitStages; +``` + +The first three parameters specify which semaphores to wait on before execution +begins and in which stage(s) of the pipeline to wait. We want to wait with +writing colors to the image until it's available, so we're specifying the stage +of the graphics pipeline that writes to the color attachment. That means that +theoretically the implementation can already start executing our vertex shader +and such while the image is not yet available. Each entry in the `waitStages` +array corresponds to the semaphore with the same index in `pWaitSemaphores`. + +```c++ +submitInfo.commandBufferCount = 1; +submitInfo.pCommandBuffers = &commandBuffer; +``` + +The next two parameters specify which command buffers to actually submit for +execution. We simply submit the single command buffer we have. + +```c++ +VkSemaphore signalSemaphores[] = {renderFinishedSemaphore}; +submitInfo.signalSemaphoreCount = 1; +submitInfo.pSignalSemaphores = signalSemaphores; +``` + +The `signalSemaphoreCount` and `pSignalSemaphores` parameters specify which +semaphores to signal once the command buffer(s) have finished execution. In our +case we're using the `renderFinishedSemaphore` for that purpose. + +```c++ +if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFence) != VK_SUCCESS) { + throw std::runtime_error("failed to submit draw command buffer!"); +} +``` + +We can now submit the command buffer to the graphics queue using +`vkQueueSubmit`. The function takes an array of `VkSubmitInfo` structures as +argument for efficiency when the workload is much larger. The last parameter +references an optional fence that will be signaled when the command buffers +finish execution. This allows us to know when it is safe for the command +buffer to be reused, thus we want to give it `inFlightFence`. Now on the next +frame, the CPU will wait for this command buffer to finish executing before it +records new commands into it. + +## Subpass dependencies + +Remember that the subpasses in a render pass automatically take care of image +layout transitions. These transitions are controlled by *subpass dependencies*, +which specify memory and execution dependencies between subpasses. We have only +a single subpass right now, but the operations right before and right after this +subpass also count as implicit "subpasses". + +There are two built-in dependencies that take care of the transition at the +start of the render pass and at the end of the render pass, but the former does +not occur at the right time. It assumes that the transition occurs at the start +of the pipeline, but we haven't acquired the image yet at that point! There are +two ways to deal with this problem. We could change the `waitStages` for the +`imageAvailableSemaphore` to `VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT` to ensure that +the render passes don't begin until the image is available, or we can make the +render pass wait for the `VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT` stage. +I've decided to go with the second option here, because it's a good excuse to +have a look at subpass dependencies and how they work. + +Subpass dependencies are specified in `VkSubpassDependency` structs. Go to the +`createRenderPass` function and add one: + +```c++ +VkSubpassDependency dependency{}; +dependency.srcSubpass = VK_SUBPASS_EXTERNAL; +dependency.dstSubpass = 0; +``` + +The first two fields specify the indices of the dependency and the dependent +subpass. The special value `VK_SUBPASS_EXTERNAL` refers to the implicit subpass +before or after the render pass depending on whether it is specified in +`srcSubpass` or `dstSubpass`. The index `0` refers to our subpass, which is the +first and only one. The `dstSubpass` must always be higher than `srcSubpass` to +prevent cycles in the dependency graph (unless one of the subpasses is +`VK_SUBPASS_EXTERNAL`). + +```c++ +dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; +dependency.srcAccessMask = 0; +``` + +The next two fields specify the operations to wait on and the stages in which +these operations occur. We need to wait for the swap chain to finish reading +from the image before we can access it. This can be accomplished by waiting on +the color attachment output stage itself. + +```c++ +dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; +dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; +``` + +The operations that should wait on this are in the color attachment stage and +involve the writing of the color attachment. These settings will +prevent the transition from happening until it's actually necessary (and +allowed): when we want to start writing colors to it. + +```c++ +renderPassInfo.dependencyCount = 1; +renderPassInfo.pDependencies = &dependency; +``` + +The `VkRenderPassCreateInfo` struct has two fields to specify an array of +dependencies. + +## Presentation + +The last step of drawing a frame is submitting the result back to the swap chain +to have it eventually show up on the screen. Presentation is configured through +a `VkPresentInfoKHR` structure at the end of the `drawFrame` function. + +```c++ +VkPresentInfoKHR presentInfo{}; +presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + +presentInfo.waitSemaphoreCount = 1; +presentInfo.pWaitSemaphores = signalSemaphores; +``` + +The first two parameters specify which semaphores to wait on before presentation +can happen, just like `VkSubmitInfo`. Since we want to wait on the command buffer +to finish execution, thus our triangle being drawn, we take the semaphores +which will be signalled and wait on them, thus we use `signalSemaphores`. + + +```c++ +VkSwapchainKHR swapChains[] = {swapChain}; +presentInfo.swapchainCount = 1; +presentInfo.pSwapchains = swapChains; +presentInfo.pImageIndices = &imageIndex; +``` + +The next two parameters specify the swap chains to present images to and the +index of the image for each swap chain. This will almost always be a single one. + +```c++ +presentInfo.pResults = nullptr; // Optional +``` + +There is one last optional parameter called `pResults`. It allows you to specify +an array of `VkResult` values to check for every individual swap chain if +presentation was successful. It's not necessary if you're only using a single +swap chain, because you can simply use the return value of the present function. + +```c++ +vkQueuePresentKHR(presentQueue, &presentInfo); +``` + +The `vkQueuePresentKHR` function submits the request to present an image to the +swap chain. We'll add error handling for both `vkAcquireNextImageKHR` and +`vkQueuePresentKHR` in the next chapter, because their failure does not +necessarily mean that the program should terminate, unlike the functions we've +seen so far. + +If you did everything correctly up to this point, then you should now see +something resembling the following when you run your program: + +![](/images/triangle.png) + +>This colored triangle may look a bit different from the one you're used to seeing in graphics tutorials. That's because this tutorial lets the shader interpolate in linear color space and converts to sRGB color space afterwards. See [this blog post](https://medium.com/@heypete/hello-triangle-meet-swift-and-wide-color-6f9e246616d9) for a discussion of the difference. + +Yay! Unfortunately, you'll see that when validation layers are enabled, the +program crashes as soon as you close it. The messages printed to the terminal +from `debugCallback` tell us why: + +![](/images/semaphore_in_use.png) + +Remember that all of the operations in `drawFrame` are asynchronous. That means +that when we exit the loop in `mainLoop`, drawing and presentation operations +may still be going on. Cleaning up resources while that is happening is a bad +idea. + +To fix that problem, we should wait for the logical device to finish operations +before exiting `mainLoop` and destroying the window: + +```c++ +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + drawFrame(); + } + + vkDeviceWaitIdle(device); +} +``` + +You can also wait for operations in a specific command queue to be finished with +`vkQueueWaitIdle`. These functions can be used as a very rudimentary way to +perform synchronization. You'll see that the program now exits without problems +when closing the window. + +## Conclusion + +A little over 900 lines of code later, we've finally gotten to the stage of seeing +something pop up on the screen! Bootstrapping a Vulkan program is definitely a +lot of work, but the take-away message is that Vulkan gives you an immense +amount of control through its explicitness. I recommend you to take some time +now to reread the code and build a mental model of the purpose of all of the +Vulkan objects in the program and how they relate to each other. We'll be +building on top of that knowledge to extend the functionality of the program +from this point on. + +The next chapter will expand the render loop to handle multiple frames in flight. + +[C++ code](/code/15_hello_triangle.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko-rust/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md b/ko-rust/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md new file mode 100644 index 00000000..e2345e31 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md @@ -0,0 +1,176 @@ +## Frames in flight + +Right now our render loop has one glaring flaw. We are required to wait on the +previous frame to finish before we can start rendering the next which results +in unnecessary idling of the host. + + + +The way to fix this is to allow multiple frames to be *in-flight* at once, that +is to say, allow the rendering of one frame to not interfere with the recording +of the next. How do we do this? Any resource that is accessed and modified +during rendering must be duplicated. Thus, we need multiple command buffers, +semaphores, and fences. In later chapters we will also add multiple instances +of other resources, so we will see this concept reappear. + +Start by adding a constant at the top of the program that defines how many +frames should be processed concurrently: + +```c++ +const int MAX_FRAMES_IN_FLIGHT = 2; +``` + +We choose the number 2 because we don't want the CPU to get *too* far ahead of +the GPU. With 2 frames in flight, the CPU and the GPU can be working on their +own tasks at the same time. If the CPU finishes early, it will wait till the +GPU finishes rendering before submitting more work. With 3 or more frames in +flight, the CPU could get ahead of the GPU, adding frames of latency. +Generally, extra latency isn't desired. But giving the application control over +the number of frames in flight is another example of Vulkan being explicit. + +Each frame should have its own command buffer, set of semaphores, and fence. +Rename and then change them to be `std::vector`s of the objects: + +```c++ +std::vector commandBuffers; + +... + +std::vector imageAvailableSemaphores; +std::vector renderFinishedSemaphores; +std::vector inFlightFences; +``` + +Then we need to create multiple command buffers. Rename `createCommandBuffer` +to `createCommandBuffers`. Next we need to resize the command buffers vector +to the size of `MAX_FRAMES_IN_FLIGHT`, alter the `VkCommandBufferAllocateInfo` +to contain that many command buffers, and then change the destination to our +vector of command buffers: + +```c++ +void createCommandBuffers() { + commandBuffers.resize(MAX_FRAMES_IN_FLIGHT); + ... + allocInfo.commandBufferCount = (uint32_t) commandBuffers.size(); + + if (vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate command buffers!"); + } +} +``` + +The `createSyncObjects` function should be changed to create all of the objects: + +```c++ +void createSyncObjects() { + imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT); + renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT); + inFlightFences.resize(MAX_FRAMES_IN_FLIGHT); + + VkSemaphoreCreateInfo semaphoreInfo{}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + + VkFenceCreateInfo fenceInfo{}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS || + vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS || + vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) != VK_SUCCESS) { + + throw std::runtime_error("failed to create synchronization objects for a frame!"); + } + } +} +``` + +Similarly, they should also all be cleaned up: + +```c++ +void cleanup() { + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr); + vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr); + vkDestroyFence(device, inFlightFences[i], nullptr); + } + + ... +} +``` + +Remember, because command buffers are freed for us when we free the command +pool, there is nothing extra to do for command buffer cleanup. + +To use the right objects every frame, we need to keep track of the current +frame. We will use a frame index for that purpose: + +```c++ +uint32_t currentFrame = 0; +``` + +The `drawFrame` function can now be modified to use the right objects: + +```c++ +void drawFrame() { + vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); + vkResetFences(device, 1, &inFlightFences[currentFrame]); + + vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); + + ... + + vkResetCommandBuffer(commandBuffers[currentFrame], 0); + recordCommandBuffer(commandBuffers[currentFrame], imageIndex); + + ... + + submitInfo.pCommandBuffers = &commandBuffers[currentFrame]; + + ... + + VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]}; + + ... + + VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]}; + + ... + + if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) { +} +``` + +Of course, we shouldn't forget to advance to the next frame every time: + +```c++ +void drawFrame() { + ... + + currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +} +``` + +By using the modulo (%) operator, we ensure that the frame index loops around +after every `MAX_FRAMES_IN_FLIGHT` enqueued frames. + + + +We've now implemented all the needed synchronization to ensure that there are +no more than `MAX_FRAMES_IN_FLIGHT` frames of work enqueued and that these +frames are not stepping over eachother. Note that it is fine for other parts of +the code, like the final cleanup, to rely on more rough synchronization like +`vkDeviceWaitIdle`. You should decide on which approach to use based on +performance requirements. + +To learn more about synchronization through examples, have a look at [this extensive overview](https://github.com/KhronosGroup/Vulkan-Docs/wiki/Synchronization-Examples#swapchain-image-acquire-and-present) by Khronos. + + +In the next chapter we'll deal with one more small thing that is required for a +well-behaved Vulkan program. + + +[C++ code](/code/16_frames_in_flight.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko-rust/03_Drawing_a_triangle/04_Swap_chain_recreation.md b/ko-rust/03_Drawing_a_triangle/04_Swap_chain_recreation.md new file mode 100644 index 00000000..ce58528b --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/04_Swap_chain_recreation.md @@ -0,0 +1,280 @@ +## Introduction + +The application we have now successfully draws a triangle, but there are some +circumstances that it isn't handling properly yet. It is possible for the window +surface to change such that the swap chain is no longer compatible with it. One +of the reasons that could cause this to happen is the size of the window +changing. We have to catch these events and recreate the swap chain. + +## Recreating the swap chain + +Create a new `recreateSwapChain` function that calls `createSwapChain` and all +of the creation functions for the objects that depend on the swap chain or the +window size. + +```c++ +void recreateSwapChain() { + vkDeviceWaitIdle(device); + + createSwapChain(); + createImageViews(); + createFramebuffers(); +} +``` + +We first call `vkDeviceWaitIdle`, because just like in the last chapter, we +shouldn't touch resources that may still be in use. Obviously, we'll have to recreate +the swap chain itself. The image views need to be recreated because they are based +directly on the swap chain images. Finally, the framebuffers directly depend on the +swap chain images, and thus must be recreated as well. + +To make sure that the old versions of these objects are cleaned up before +recreating them, we should move some of the cleanup code to a separate function +that we can call from the `recreateSwapChain` function. Let's call it +`cleanupSwapChain`: + +```c++ +void cleanupSwapChain() { + +} + +void recreateSwapChain() { + vkDeviceWaitIdle(device); + + cleanupSwapChain(); + + createSwapChain(); + createImageViews(); + createFramebuffers(); +} +``` + +Note that we don't recreate the renderpass here for simplicity. In theory it can be possible for the swap chain image format to change during an applications' lifetime, e.g. when moving a window from a standard range to a high dynamic range monitor. This may require the application to recreate the renderpass to make sure the change between dynamic ranges is properly reflected. + +We'll move the cleanup code of all objects that are recreated as part of a swap +chain refresh from `cleanup` to `cleanupSwapChain`: + +```c++ +void cleanupSwapChain() { + for (auto framebuffer : swapChainFramebuffers) { + vkDestroyFramebuffer(device, framebuffer, nullptr); + } + + for (auto imageView : swapChainImageViews) { + vkDestroyImageView(device, imageView, nullptr); + } + + vkDestroySwapchainKHR(device, swapChain, nullptr); +} + +void cleanup() { + cleanupSwapChain(); + + vkDestroyPipeline(device, graphicsPipeline, nullptr); + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + + vkDestroyRenderPass(device, renderPass, nullptr); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr); + vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr); + vkDestroyFence(device, inFlightFences[i], nullptr); + } + + vkDestroyCommandPool(device, commandPool, nullptr); + + vkDestroyDevice(device, nullptr); + + if (enableValidationLayers) { + DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr); + } + + vkDestroySurfaceKHR(instance, surface, nullptr); + vkDestroyInstance(instance, nullptr); + + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +Note that in `chooseSwapExtent` we already query the new window resolution to +make sure that the swap chain images have the (new) right size, so there's no +need to modify `chooseSwapExtent` (remember that we already had to use +`glfwGetFramebufferSize` to get the resolution of the surface in pixels when +creating the swap chain). + +That's all it takes to recreate the swap chain! However, the disadvantage of +this approach is that we need to stop all rendering before creating the new swap +chain. It is possible to create a new swap chain while drawing commands on an +image from the old swap chain are still in-flight. You need to pass the previous +swap chain to the `oldSwapChain` field in the `VkSwapchainCreateInfoKHR` struct +and destroy the old swap chain as soon as you've finished using it. + +## Suboptimal or out-of-date swap chain + +Now we just need to figure out when swap chain recreation is necessary and call +our new `recreateSwapChain` function. Luckily, Vulkan will usually just tell us that the swap chain is no longer adequate during presentation. The `vkAcquireNextImageKHR` and +`vkQueuePresentKHR` functions can return the following special values to +indicate this. + +* `VK_ERROR_OUT_OF_DATE_KHR`: The swap chain has become incompatible with the +surface and can no longer be used for rendering. Usually happens after a window resize. +* `VK_SUBOPTIMAL_KHR`: The swap chain can still be used to successfully present +to the surface, but the surface properties are no longer matched exactly. + +```c++ +VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); + +if (result == VK_ERROR_OUT_OF_DATE_KHR) { + recreateSwapChain(); + return; +} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { + throw std::runtime_error("failed to acquire swap chain image!"); +} +``` + +If the swap chain turns out to be out of date when attempting to acquire an +image, then it is no longer possible to present to it. Therefore we should +immediately recreate the swap chain and try again in the next `drawFrame` call. + +You could also decide to do that if the swap chain is suboptimal, but I've +chosen to proceed anyway in that case because we've already acquired an image. +Both `VK_SUCCESS` and `VK_SUBOPTIMAL_KHR` are considered "success" return codes. + +```c++ +result = vkQueuePresentKHR(presentQueue, &presentInfo); + +if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { + recreateSwapChain(); +} else if (result != VK_SUCCESS) { + throw std::runtime_error("failed to present swap chain image!"); +} + +currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +``` + +The `vkQueuePresentKHR` function returns the same values with the same meaning. +In this case we will also recreate the swap chain if it is suboptimal, because +we want the best possible result. + +## Fixing a deadlock + +If we try to run the code now, it is possible to encounter a deadlock. +Debugging the code, we find that the application reaches `vkWaitForFences` but +never continues past it. This is because when `vkAcquireNextImageKHR` returns +`VK_ERROR_OUT_OF_DATE_KHR`, we recreate the swapchain and then return from +`drawFrame`. But before that happens, the current frame's fence was waited upon +and reset. Since we return immediately, no work is submitted for execution and +the fence will never be signaled, causing `vkWaitForFences` to halt forever. + +There is a simple fix thankfully. Delay resetting the fence until after we +know for sure we will be submitting work with it. Thus, if we return early, the +fence is still signaled and `vkWaitForFences` wont deadlock the next time we +use the same fence object. + +The beginning of `drawFrame` should now look like this: +```c++ +vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); + +uint32_t imageIndex; +VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); + +if (result == VK_ERROR_OUT_OF_DATE_KHR) { + recreateSwapChain(); + return; +} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { + throw std::runtime_error("failed to acquire swap chain image!"); +} + +// Only reset the fence if we are submitting work +vkResetFences(device, 1, &inFlightFences[currentFrame]); +``` + +## Handling resizes explicitly + +Although many drivers and platforms trigger `VK_ERROR_OUT_OF_DATE_KHR` automatically after a window resize, it is not guaranteed to happen. That's why we'll add some extra code to also handle resizes explicitly. First add a new member variable that flags that a resize has happened: + +```c++ +std::vector inFlightFences; + +bool framebufferResized = false; +``` + +The `drawFrame` function should then be modified to also check for this flag: + +```c++ +if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) { + framebufferResized = false; + recreateSwapChain(); +} else if (result != VK_SUCCESS) { + ... +} +``` + +It is important to do this after `vkQueuePresentKHR` to ensure that the semaphores are in a consistent state, otherwise a signaled semaphore may never be properly waited upon. Now to actually detect resizes we can use the `glfwSetFramebufferSizeCallback` function in the GLFW framework to set up a callback: + +```c++ +void initWindow() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + + window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); + glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); +} + +static void framebufferResizeCallback(GLFWwindow* window, int width, int height) { + +} +``` + +The reason that we're creating a `static` function as a callback is because GLFW does not know how to properly call a member function with the right `this` pointer to our `HelloTriangleApplication` instance. + +However, we do get a reference to the `GLFWwindow` in the callback and there is another GLFW function that allows you to store an arbitrary pointer inside of it: `glfwSetWindowUserPointer`: + +```c++ +window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); +glfwSetWindowUserPointer(window, this); +glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); +``` + +This value can now be retrieved from within the callback with `glfwGetWindowUserPointer` to properly set the flag: + +```c++ +static void framebufferResizeCallback(GLFWwindow* window, int width, int height) { + auto app = reinterpret_cast(glfwGetWindowUserPointer(window)); + app->framebufferResized = true; +} +``` + +Now try to run the program and resize the window to see if the framebuffer is indeed resized properly with the window. + +## Handling minimization + +There is another case where a swap chain may become out of date and that is a special kind of window resizing: window minimization. This case is special because it will result in a frame buffer size of `0`. In this tutorial we will handle that by pausing until the window is in the foreground again by extending the `recreateSwapChain` function: + +```c++ +void recreateSwapChain() { + int width = 0, height = 0; + glfwGetFramebufferSize(window, &width, &height); + while (width == 0 || height == 0) { + glfwGetFramebufferSize(window, &width, &height); + glfwWaitEvents(); + } + + vkDeviceWaitIdle(device); + + ... +} +``` + +The initial call to `glfwGetFramebufferSize` handles the case where the size is already correct and `glfwWaitEvents` would have nothing to wait on. + +Congratulations, you've now finished your very first well-behaved Vulkan +program! In the next chapter we're going to get rid of the hardcoded vertices in +the vertex shader and actually use a vertex buffer. + +[C++ code](/code/17_swap_chain_recreation.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko-rust/04_Vertex_buffers/00_Vertex_input_description.md b/ko-rust/04_Vertex_buffers/00_Vertex_input_description.md new file mode 100644 index 00000000..e7da3e4f --- /dev/null +++ b/ko-rust/04_Vertex_buffers/00_Vertex_input_description.md @@ -0,0 +1,225 @@ +## Introduction + +In the next few chapters, we're going to replace the hardcoded vertex data in +the vertex shader with a vertex buffer in memory. We'll start with the easiest +approach of creating a CPU visible buffer and using `memcpy` to copy the vertex +data into it directly, and after that we'll see how to use a staging buffer to +copy the vertex data to high performance memory. + +## Vertex shader + +First change the vertex shader to no longer include the vertex data in the +shader code itself. The vertex shader takes input from a vertex buffer using the +`in` keyword. + +```glsl +#version 450 + +layout(location = 0) in vec2 inPosition; +layout(location = 1) in vec3 inColor; + +layout(location = 0) out vec3 fragColor; + +void main() { + gl_Position = vec4(inPosition, 0.0, 1.0); + fragColor = inColor; +} +``` + +The `inPosition` and `inColor` variables are *vertex attributes*. They're +properties that are specified per-vertex in the vertex buffer, just like we +manually specified a position and color per vertex using the two arrays. Make +sure to recompile the vertex shader! + +Just like `fragColor`, the `layout(location = x)` annotations assign indices to +the inputs that we can later use to reference them. It is important to know that +some types, like `dvec3` 64 bit vectors, use multiple *slots*. That means that +the index after it must be at least 2 higher: + +```glsl +layout(location = 0) in dvec3 inPosition; +layout(location = 2) in vec3 inColor; +``` + +You can find more info about the layout qualifier in the [OpenGL wiki](https://www.khronos.org/opengl/wiki/Layout_Qualifier_(GLSL)). + +## Vertex data + +We're moving the vertex data from the shader code to an array in the code of our +program. Start by including the GLM library, which provides us with linear +algebra related types like vectors and matrices. We're going to use these types +to specify the position and color vectors. + +```c++ +#include +``` + +Create a new structure called `Vertex` with the two attributes that we're going +to use in the vertex shader inside it: + +```c++ +struct Vertex { + glm::vec2 pos; + glm::vec3 color; +}; +``` + +GLM conveniently provides us with C++ types that exactly match the vector types +used in the shader language. + +```c++ +const std::vector vertices = { + {{0.0f, -0.5f}, {1.0f, 0.0f, 0.0f}}, + {{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}}, + {{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}} +}; +``` + +Now use the `Vertex` structure to specify an array of vertex data. We're using +exactly the same position and color values as before, but now they're combined +into one array of vertices. This is known as *interleaving* vertex attributes. + +## Binding descriptions + +The next step is to tell Vulkan how to pass this data format to the vertex +shader once it's been uploaded into GPU memory. There are two types of +structures needed to convey this information. + +The first structure is `VkVertexInputBindingDescription` and we'll add a member +function to the `Vertex` struct to populate it with the right data. + +```c++ +struct Vertex { + glm::vec2 pos; + glm::vec3 color; + + static VkVertexInputBindingDescription getBindingDescription() { + VkVertexInputBindingDescription bindingDescription{}; + + return bindingDescription; + } +}; +``` + +A vertex binding describes at which rate to load data from memory throughout the +vertices. It specifies the number of bytes between data entries and whether to +move to the next data entry after each vertex or after each instance. + +```c++ +VkVertexInputBindingDescription bindingDescription{}; +bindingDescription.binding = 0; +bindingDescription.stride = sizeof(Vertex); +bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; +``` + +All of our per-vertex data is packed together in one array, so we're only going +to have one binding. The `binding` parameter specifies the index of the binding +in the array of bindings. The `stride` parameter specifies the number of bytes +from one entry to the next, and the `inputRate` parameter can have one of the +following values: + +* `VK_VERTEX_INPUT_RATE_VERTEX`: Move to the next data entry after each vertex +* `VK_VERTEX_INPUT_RATE_INSTANCE`: Move to the next data entry after each +instance + +We're not going to use instanced rendering, so we'll stick to per-vertex data. + +## Attribute descriptions + +The second structure that describes how to handle vertex input is +`VkVertexInputAttributeDescription`. We're going to add another helper function +to `Vertex` to fill in these structs. + +```c++ +#include + +... + +static std::array getAttributeDescriptions() { + std::array attributeDescriptions{}; + + return attributeDescriptions; +} +``` + +As the function prototype indicates, there are going to be two of these +structures. An attribute description struct describes how to extract a vertex +attribute from a chunk of vertex data originating from a binding description. We +have two attributes, position and color, so we need two attribute description +structs. + +```c++ +attributeDescriptions[0].binding = 0; +attributeDescriptions[0].location = 0; +attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; +attributeDescriptions[0].offset = offsetof(Vertex, pos); +``` + +The `binding` parameter tells Vulkan from which binding the per-vertex data +comes. The `location` parameter references the `location` directive of the +input in the vertex shader. The input in the vertex shader with location `0` is +the position, which has two 32-bit float components. + +The `format` parameter describes the type of data for the attribute. A bit +confusingly, the formats are specified using the same enumeration as color +formats. The following shader types and formats are commonly used together: + +* `float`: `VK_FORMAT_R32_SFLOAT` +* `vec2`: `VK_FORMAT_R32G32_SFLOAT` +* `vec3`: `VK_FORMAT_R32G32B32_SFLOAT` +* `vec4`: `VK_FORMAT_R32G32B32A32_SFLOAT` + +As you can see, you should use the format where the amount of color channels +matches the number of components in the shader data type. It is allowed to use +more channels than the number of components in the shader, but they will be +silently discarded. If the number of channels is lower than the number of +components, then the BGA components will use default values of `(0, 0, 1)`. The +color type (`SFLOAT`, `UINT`, `SINT`) and bit width should also match the type +of the shader input. See the following examples: + +* `ivec2`: `VK_FORMAT_R32G32_SINT`, a 2-component vector of 32-bit signed +integers +* `uvec4`: `VK_FORMAT_R32G32B32A32_UINT`, a 4-component vector of 32-bit +unsigned integers +* `double`: `VK_FORMAT_R64_SFLOAT`, a double-precision (64-bit) float + +The `format` parameter implicitly defines the byte size of attribute data and +the `offset` parameter specifies the number of bytes since the start of the +per-vertex data to read from. The binding is loading one `Vertex` at a time and +the position attribute (`pos`) is at an offset of `0` bytes from the beginning +of this struct. This is automatically calculated using the `offsetof` macro. + +```c++ +attributeDescriptions[1].binding = 0; +attributeDescriptions[1].location = 1; +attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; +attributeDescriptions[1].offset = offsetof(Vertex, color); +``` + +The color attribute is described in much the same way. + +## Pipeline vertex input + +We now need to set up the graphics pipeline to accept vertex data in this format +by referencing the structures in `createGraphicsPipeline`. Find the +`vertexInputInfo` struct and modify it to reference the two descriptions: + +```c++ +auto bindingDescription = Vertex::getBindingDescription(); +auto attributeDescriptions = Vertex::getAttributeDescriptions(); + +vertexInputInfo.vertexBindingDescriptionCount = 1; +vertexInputInfo.vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()); +vertexInputInfo.pVertexBindingDescriptions = &bindingDescription; +vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data(); +``` + +The pipeline is now ready to accept vertex data in the format of the `vertices` +container and pass it on to our vertex shader. If you run the program now with +validation layers enabled, you'll see that it complains that there is no vertex +buffer bound to the binding. The next step is to create a vertex buffer and move +the vertex data to it so the GPU is able to access it. + +[C++ code](/code/18_vertex_input.cpp) / +[Vertex shader](/code/18_shader_vertexbuffer.vert) / +[Fragment shader](/code/18_shader_vertexbuffer.frag) diff --git a/ko-rust/04_Vertex_buffers/01_Vertex_buffer_creation.md b/ko-rust/04_Vertex_buffers/01_Vertex_buffer_creation.md new file mode 100644 index 00000000..77122c50 --- /dev/null +++ b/ko-rust/04_Vertex_buffers/01_Vertex_buffer_creation.md @@ -0,0 +1,342 @@ +## Introduction + +Buffers in Vulkan are regions of memory used for storing arbitrary data that can +be read by the graphics card. They can be used to store vertex data, which we'll +do in this chapter, but they can also be used for many other purposes that we'll +explore in future chapters. Unlike the Vulkan objects we've been dealing with so +far, buffers do not automatically allocate memory for themselves. The work from +the previous chapters has shown that the Vulkan API puts the programmer in +control of almost everything and memory management is one of those things. + +## Buffer creation + +Create a new function `createVertexBuffer` and call it from `initVulkan` right +before `createCommandBuffers`. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); + createVertexBuffer(); + createCommandBuffers(); + createSyncObjects(); +} + +... + +void createVertexBuffer() { + +} +``` + +Creating a buffer requires us to fill a `VkBufferCreateInfo` structure. + +```c++ +VkBufferCreateInfo bufferInfo{}; +bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; +bufferInfo.size = sizeof(vertices[0]) * vertices.size(); +``` + +The first field of the struct is `size`, which specifies the size of the buffer +in bytes. Calculating the byte size of the vertex data is straightforward with +`sizeof`. + +```c++ +bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; +``` + +The second field is `usage`, which indicates for which purposes the data in the +buffer is going to be used. It is possible to specify multiple purposes using a +bitwise or. Our use case will be a vertex buffer, we'll look at other types of +usage in future chapters. + +```c++ +bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; +``` + +Just like the images in the swap chain, buffers can also be owned by a specific +queue family or be shared between multiple at the same time. The buffer will +only be used from the graphics queue, so we can stick to exclusive access. + +The `flags` parameter is used to configure sparse buffer memory, which is not +relevant right now. We'll leave it at the default value of `0`. + +We can now create the buffer with `vkCreateBuffer`. Define a class member to +hold the buffer handle and call it `vertexBuffer`. + +```c++ +VkBuffer vertexBuffer; + +... + +void createVertexBuffer() { + VkBufferCreateInfo bufferInfo{}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = sizeof(vertices[0]) * vertices.size(); + bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateBuffer(device, &bufferInfo, nullptr, &vertexBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to create vertex buffer!"); + } +} +``` + +The buffer should be available for use in rendering commands until the end of +the program and it does not depend on the swap chain, so we'll clean it up in +the original `cleanup` function: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyBuffer(device, vertexBuffer, nullptr); + + ... +} +``` + +## Memory requirements + +The buffer has been created, but it doesn't actually have any memory assigned to +it yet. The first step of allocating memory for the buffer is to query its +memory requirements using the aptly named `vkGetBufferMemoryRequirements` +function. + +```c++ +VkMemoryRequirements memRequirements; +vkGetBufferMemoryRequirements(device, vertexBuffer, &memRequirements); +``` + +The `VkMemoryRequirements` struct has three fields: + +* `size`: The size of the required amount of memory in bytes, may differ from +`bufferInfo.size`. +* `alignment`: The offset in bytes where the buffer begins in the allocated +region of memory, depends on `bufferInfo.usage` and `bufferInfo.flags`. +* `memoryTypeBits`: Bit field of the memory types that are suitable for the +buffer. + +Graphics cards can offer different types of memory to allocate from. Each type +of memory varies in terms of allowed operations and performance characteristics. +We need to combine the requirements of the buffer and our own application +requirements to find the right type of memory to use. Let's create a new +function `findMemoryType` for this purpose. + +```c++ +uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) { + +} +``` + +First we need to query info about the available types of memory using +`vkGetPhysicalDeviceMemoryProperties`. + +```c++ +VkPhysicalDeviceMemoryProperties memProperties; +vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties); +``` + +The `VkPhysicalDeviceMemoryProperties` structure has two arrays `memoryTypes` +and `memoryHeaps`. Memory heaps are distinct memory resources like dedicated +VRAM and swap space in RAM for when VRAM runs out. The different types of memory +exist within these heaps. Right now we'll only concern ourselves with the type +of memory and not the heap it comes from, but you can imagine that this can +affect performance. + +Let's first find a memory type that is suitable for the buffer itself: + +```c++ +for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if (typeFilter & (1 << i)) { + return i; + } +} + +throw std::runtime_error("failed to find suitable memory type!"); +``` + +The `typeFilter` parameter will be used to specify the bit field of memory types +that are suitable. That means that we can find the index of a suitable memory +type by simply iterating over them and checking if the corresponding bit is set +to `1`. + +However, we're not just interested in a memory type that is suitable for the +vertex buffer. We also need to be able to write our vertex data to that memory. +The `memoryTypes` array consists of `VkMemoryType` structs that specify the heap +and properties of each type of memory. The properties define special features +of the memory, like being able to map it so we can write to it from the CPU. +This property is indicated with `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT`, but we +also need to use the `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` property. We'll see +why when we map the memory. + +We can now modify the loop to also check for the support of this property: + +```c++ +for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } +} +``` + +We may have more than one desirable property, so we should check if the result +of the bitwise AND is not just non-zero, but equal to the desired properties bit +field. If there is a memory type suitable for the buffer that also has all of +the properties we need, then we return its index, otherwise we throw an +exception. + +## Memory allocation + +We now have a way to determine the right memory type, so we can actually +allocate the memory by filling in the `VkMemoryAllocateInfo` structure. + +```c++ +VkMemoryAllocateInfo allocInfo{}; +allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; +allocInfo.allocationSize = memRequirements.size; +allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); +``` + +Memory allocation is now as simple as specifying the size and type, both of +which are derived from the memory requirements of the vertex buffer and the +desired property. Create a class member to store the handle to the memory and +allocate it with `vkAllocateMemory`. + +```c++ +VkBuffer vertexBuffer; +VkDeviceMemory vertexBufferMemory; + +... + +if (vkAllocateMemory(device, &allocInfo, nullptr, &vertexBufferMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate vertex buffer memory!"); +} +``` + +If memory allocation was successful, then we can now associate this memory with +the buffer using `vkBindBufferMemory`: + +```c++ +vkBindBufferMemory(device, vertexBuffer, vertexBufferMemory, 0); +``` + +The first three parameters are self-explanatory and the fourth parameter is the +offset within the region of memory. Since this memory is allocated specifically +for this the vertex buffer, the offset is simply `0`. If the offset is non-zero, +then it is required to be divisible by `memRequirements.alignment`. + +Of course, just like dynamic memory allocation in C++, the memory should be +freed at some point. Memory that is bound to a buffer object may be freed once +the buffer is no longer used, so let's free it after the buffer has been +destroyed: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyBuffer(device, vertexBuffer, nullptr); + vkFreeMemory(device, vertexBufferMemory, nullptr); +``` + +## Filling the vertex buffer + +It is now time to copy the vertex data to the buffer. This is done by [mapping +the buffer memory](https://en.wikipedia.org/wiki/Memory-mapped_I/O) into CPU +accessible memory with `vkMapMemory`. + +```c++ +void* data; +vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data); +``` + +This function allows us to access a region of the specified memory resource +defined by an offset and size. The offset and size here are `0` and +`bufferInfo.size`, respectively. It is also possible to specify the special +value `VK_WHOLE_SIZE` to map all of the memory. The second to last parameter can +be used to specify flags, but there aren't any available yet in the current API. +It must be set to the value `0`. The last parameter specifies the output for the +pointer to the mapped memory. + +```c++ +void* data; +vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data); + memcpy(data, vertices.data(), (size_t) bufferInfo.size); +vkUnmapMemory(device, vertexBufferMemory); +``` + +You can now simply `memcpy` the vertex data to the mapped memory and unmap it +again using `vkUnmapMemory`. Unfortunately the driver may not immediately copy +the data into the buffer memory, for example because of caching. It is also +possible that writes to the buffer are not visible in the mapped memory yet. +There are two ways to deal with that problem: + +* Use a memory heap that is host coherent, indicated with +`VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` +* Call `vkFlushMappedMemoryRanges` after writing to the mapped memory, and +call `vkInvalidateMappedMemoryRanges` before reading from the mapped memory + +We went for the first approach, which ensures that the mapped memory always +matches the contents of the allocated memory. Do keep in mind that this may lead +to slightly worse performance than explicit flushing, but we'll see why that +doesn't matter in the next chapter. + +Flushing memory ranges or using a coherent memory heap means that the driver will be aware of our writes to the buffer, but it doesn't mean that they are actually visible on the GPU yet. The transfer of data to the GPU is an operation that happens in the background and the specification simply [tells us](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-submission-host-writes) that it is guaranteed to be complete as of the next call to `vkQueueSubmit`. + +## Binding the vertex buffer + +All that remains now is binding the vertex buffer during rendering operations. +We're going to extend the `recordCommandBuffer` function to do that. + +```c++ +vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); + +VkBuffer vertexBuffers[] = {vertexBuffer}; +VkDeviceSize offsets[] = {0}; +vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets); + +vkCmdDraw(commandBuffer, static_cast(vertices.size()), 1, 0, 0); +``` + +The `vkCmdBindVertexBuffers` function is used to bind vertex buffers to +bindings, like the one we set up in the previous chapter. The first two +parameters, besides the command buffer, specify the offset and number of +bindings we're going to specify vertex buffers for. The last two parameters +specify the array of vertex buffers to bind and the byte offsets to start +reading vertex data from. You should also change the call to `vkCmdDraw` to pass +the number of vertices in the buffer as opposed to the hardcoded number `3`. + +Now run the program and you should see the familiar triangle again: + +![](/images/triangle.png) + +Try changing the color of the top vertex to white by modifying the `vertices` +array: + +```c++ +const std::vector vertices = { + {{0.0f, -0.5f}, {1.0f, 1.0f, 1.0f}}, + {{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}}, + {{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}} +}; +``` + +Run the program again and you should see the following: + +![](/images/triangle_white.png) + +In the next chapter we'll look at a different way to copy vertex data to a +vertex buffer that results in better performance, but takes some more work. + +[C++ code](/code/19_vertex_buffer.cpp) / +[Vertex shader](/code/18_shader_vertexbuffer.vert) / +[Fragment shader](/code/18_shader_vertexbuffer.frag) diff --git a/ko-rust/04_Vertex_buffers/02_Staging_buffer.md b/ko-rust/04_Vertex_buffers/02_Staging_buffer.md new file mode 100644 index 00000000..289e74d4 --- /dev/null +++ b/ko-rust/04_Vertex_buffers/02_Staging_buffer.md @@ -0,0 +1,267 @@ +## Introduction + +The vertex buffer we have right now works correctly, but the memory type that +allows us to access it from the CPU may not be the most optimal memory type for +the graphics card itself to read from. The most optimal memory has the +`VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT` flag and is usually not accessible by the +CPU on dedicated graphics cards. In this chapter we're going to create two +vertex buffers. One *staging buffer* in CPU accessible memory to upload the data +from the vertex array to, and the final vertex buffer in device local memory. +We'll then use a buffer copy command to move the data from the staging buffer to +the actual vertex buffer. + +## Transfer queue + +The buffer copy command requires a queue family that supports transfer +operations, which is indicated using `VK_QUEUE_TRANSFER_BIT`. The good news is +that any queue family with `VK_QUEUE_GRAPHICS_BIT` or `VK_QUEUE_COMPUTE_BIT` +capabilities already implicitly support `VK_QUEUE_TRANSFER_BIT` operations. The +implementation is not required to explicitly list it in `queueFlags` in those +cases. + +If you like a challenge, then you can still try to use a different queue family +specifically for transfer operations. It will require you to make the following +modifications to your program: + +* Modify `QueueFamilyIndices` and `findQueueFamilies` to explicitly look for a +queue family with the `VK_QUEUE_TRANSFER_BIT` bit, but not the +`VK_QUEUE_GRAPHICS_BIT`. +* Modify `createLogicalDevice` to request a handle to the transfer queue +* Create a second command pool for command buffers that are submitted on the +transfer queue family +* Change the `sharingMode` of resources to be `VK_SHARING_MODE_CONCURRENT` and +specify both the graphics and transfer queue families +* Submit any transfer commands like `vkCmdCopyBuffer` (which we'll be using in +this chapter) to the transfer queue instead of the graphics queue + +It's a bit of work, but it'll teach you a lot about how resources are shared +between queue families. + +## Abstracting buffer creation + +Because we're going to create multiple buffers in this chapter, it's a good idea +to move buffer creation to a helper function. Create a new function +`createBuffer` and move the code in `createVertexBuffer` (except mapping) to it. + +```c++ +void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, VkDeviceMemory& bufferMemory) { + VkBufferCreateInfo bufferInfo{}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = size; + bufferInfo.usage = usage; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) { + throw std::runtime_error("failed to create buffer!"); + } + + VkMemoryRequirements memRequirements; + vkGetBufferMemoryRequirements(device, buffer, &memRequirements); + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); + + if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate buffer memory!"); + } + + vkBindBufferMemory(device, buffer, bufferMemory, 0); +} +``` + +Make sure to add parameters for the buffer size, memory properties and usage so +that we can use this function to create many different types of buffers. The +last two parameters are output variables to write the handles to. + +You can now remove the buffer creation and memory allocation code from +`createVertexBuffer` and just call `createBuffer` instead: + +```c++ +void createVertexBuffer() { + VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); + createBuffer(bufferSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, vertexBuffer, vertexBufferMemory); + + void* data; + vkMapMemory(device, vertexBufferMemory, 0, bufferSize, 0, &data); + memcpy(data, vertices.data(), (size_t) bufferSize); + vkUnmapMemory(device, vertexBufferMemory); +} +``` + +Run your program to make sure that the vertex buffer still works properly. + +## Using a staging buffer + +We're now going to change `createVertexBuffer` to only use a host visible buffer +as temporary buffer and use a device local one as actual vertex buffer. + +```c++ +void createVertexBuffer() { + VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); + + VkBuffer stagingBuffer; + VkDeviceMemory stagingBufferMemory; + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); + + void* data; + vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data); + memcpy(data, vertices.data(), (size_t) bufferSize); + vkUnmapMemory(device, stagingBufferMemory); + + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory); +} +``` + +We're now using a new `stagingBuffer` with `stagingBufferMemory` for mapping and +copying the vertex data. In this chapter we're going to use two new buffer usage +flags: + +* `VK_BUFFER_USAGE_TRANSFER_SRC_BIT`: Buffer can be used as source in a memory +transfer operation. +* `VK_BUFFER_USAGE_TRANSFER_DST_BIT`: Buffer can be used as destination in a +memory transfer operation. + +The `vertexBuffer` is now allocated from a memory type that is device local, +which generally means that we're not able to use `vkMapMemory`. However, we can +copy data from the `stagingBuffer` to the `vertexBuffer`. We have to indicate +that we intend to do that by specifying the transfer source flag for the +`stagingBuffer` and the transfer destination flag for the `vertexBuffer`, along +with the vertex buffer usage flag. + +We're now going to write a function to copy the contents from one buffer to +another, called `copyBuffer`. + +```c++ +void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { + +} +``` + +Memory transfer operations are executed using command buffers, just like drawing +commands. Therefore we must first allocate a temporary command buffer. You may +wish to create a separate command pool for these kinds of short-lived buffers, +because the implementation may be able to apply memory allocation optimizations. +You should use the `VK_COMMAND_POOL_CREATE_TRANSIENT_BIT` flag during command +pool generation in that case. + +```c++ +void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandPool = commandPool; + allocInfo.commandBufferCount = 1; + + VkCommandBuffer commandBuffer; + vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); +} +``` + +And immediately start recording the command buffer: + +```c++ +VkCommandBufferBeginInfo beginInfo{}; +beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; +beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + +vkBeginCommandBuffer(commandBuffer, &beginInfo); +``` + +We're only going to use the command buffer once and wait with returning from the function until the copy +operation has finished executing. It's good practice to tell the driver about +our intent using `VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT`. + +```c++ +VkBufferCopy copyRegion{}; +copyRegion.srcOffset = 0; // Optional +copyRegion.dstOffset = 0; // Optional +copyRegion.size = size; +vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region); +``` + +Contents of buffers are transferred using the `vkCmdCopyBuffer` command. It +takes the source and destination buffers as arguments, and an array of regions +to copy. The regions are defined in `VkBufferCopy` structs and consist of a +source buffer offset, destination buffer offset and size. It is not possible to +specify `VK_WHOLE_SIZE` here, unlike the `vkMapMemory` command. + +```c++ +vkEndCommandBuffer(commandBuffer); +``` + +This command buffer only contains the copy command, so we can stop recording +right after that. Now execute the command buffer to complete the transfer: + +```c++ +VkSubmitInfo submitInfo{}; +submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; +submitInfo.commandBufferCount = 1; +submitInfo.pCommandBuffers = &commandBuffer; + +vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); +vkQueueWaitIdle(graphicsQueue); +``` + +Unlike the draw commands, there are no events we need to wait on this time. We +just want to execute the transfer on the buffers immediately. There are again +two possible ways to wait on this transfer to complete. We could use a fence and +wait with `vkWaitForFences`, or simply wait for the transfer queue to become +idle with `vkQueueWaitIdle`. A fence would allow you to schedule multiple +transfers simultaneously and wait for all of them complete, instead of executing +one at a time. That may give the driver more opportunities to optimize. + +```c++ +vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); +``` + +Don't forget to clean up the command buffer used for the transfer operation. + +We can now call `copyBuffer` from the `createVertexBuffer` function to move the +vertex data to the device local buffer: + +```c++ +createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory); + +copyBuffer(stagingBuffer, vertexBuffer, bufferSize); +``` + +After copying the data from the staging buffer to the device buffer, we should +clean it up: + +```c++ + ... + + copyBuffer(stagingBuffer, vertexBuffer, bufferSize); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingBufferMemory, nullptr); +} +``` + +Run your program to verify that you're seeing the familiar triangle again. The +improvement may not be visible right now, but its vertex data is now being +loaded from high performance memory. This will matter when we're going to start +rendering more complex geometry. + +## Conclusion + +It should be noted that in a real world application, you're not supposed to +actually call `vkAllocateMemory` for every individual buffer. The maximum number +of simultaneous memory allocations is limited by the `maxMemoryAllocationCount` +physical device limit, which may be as low as `4096` even on high end hardware +like an NVIDIA GTX 1080. The right way to allocate memory for a large number of +objects at the same time is to create a custom allocator that splits up a single +allocation among many different objects by using the `offset` parameters that +we've seen in many functions. + +You can either implement such an allocator yourself, or use the +[VulkanMemoryAllocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator) +library provided by the GPUOpen initiative. However, for this tutorial it's okay +to use a separate allocation for every resource, because we won't come close to +hitting any of these limits for now. + +[C++ code](/code/20_staging_buffer.cpp) / +[Vertex shader](/code/18_shader_vertexbuffer.vert) / +[Fragment shader](/code/18_shader_vertexbuffer.frag) diff --git a/ko-rust/04_Vertex_buffers/03_Index_buffer.md b/ko-rust/04_Vertex_buffers/03_Index_buffer.md new file mode 100644 index 00000000..088263db --- /dev/null +++ b/ko-rust/04_Vertex_buffers/03_Index_buffer.md @@ -0,0 +1,179 @@ +## Introduction + +The 3D meshes you'll be rendering in a real world application will often share +vertices between multiple triangles. This already happens even with something +simple like drawing a rectangle: + +![](/images/vertex_vs_index.svg) + +Drawing a rectangle takes two triangles, which means that we need a vertex +buffer with 6 vertices. The problem is that the data of two vertices needs to be +duplicated resulting in 50% redundancy. It only gets worse with more complex +meshes, where vertices are reused in an average number of 3 triangles. The +solution to this problem is to use an *index buffer*. + +An index buffer is essentially an array of pointers into the vertex buffer. It +allows you to reorder the vertex data, and reuse existing data for multiple +vertices. The illustration above demonstrates what the index buffer would look +like for the rectangle if we have a vertex buffer containing each of the four +unique vertices. The first three indices define the upper-right triangle and the +last three indices define the vertices for the bottom-left triangle. + +## Index buffer creation + +In this chapter we're going to modify the vertex data and add index data to +draw a rectangle like the one in the illustration. Modify the vertex data to +represent the four corners: + +```c++ +const std::vector vertices = { + {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}}, + {{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}}, + {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}}, + {{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}} +}; +``` + +The top-left corner is red, top-right is green, bottom-right is blue and the +bottom-left is white. We'll add a new array `indices` to represent the contents +of the index buffer. It should match the indices in the illustration to draw the +upper-right triangle and bottom-left triangle. + +```c++ +const std::vector indices = { + 0, 1, 2, 2, 3, 0 +}; +``` + +It is possible to use either `uint16_t` or `uint32_t` for your index buffer +depending on the number of entries in `vertices`. We can stick to `uint16_t` for +now because we're using less than 65535 unique vertices. + +Just like the vertex data, the indices need to be uploaded into a `VkBuffer` for +the GPU to be able to access them. Define two new class members to hold the +resources for the index buffer: + +```c++ +VkBuffer vertexBuffer; +VkDeviceMemory vertexBufferMemory; +VkBuffer indexBuffer; +VkDeviceMemory indexBufferMemory; +``` + +The `createIndexBuffer` function that we'll add now is almost identical to +`createVertexBuffer`: + +```c++ +void initVulkan() { + ... + createVertexBuffer(); + createIndexBuffer(); + ... +} + +void createIndexBuffer() { + VkDeviceSize bufferSize = sizeof(indices[0]) * indices.size(); + + VkBuffer stagingBuffer; + VkDeviceMemory stagingBufferMemory; + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); + + void* data; + vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data); + memcpy(data, indices.data(), (size_t) bufferSize); + vkUnmapMemory(device, stagingBufferMemory); + + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, indexBuffer, indexBufferMemory); + + copyBuffer(stagingBuffer, indexBuffer, bufferSize); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingBufferMemory, nullptr); +} +``` + +There are only two notable differences. The `bufferSize` is now equal to the +number of indices times the size of the index type, either `uint16_t` or +`uint32_t`. The usage of the `indexBuffer` should be +`VK_BUFFER_USAGE_INDEX_BUFFER_BIT` instead of +`VK_BUFFER_USAGE_VERTEX_BUFFER_BIT`, which makes sense. Other than that, the +process is exactly the same. We create a staging buffer to copy the contents of +`indices` to and then copy it to the final device local index buffer. + +The index buffer should be cleaned up at the end of the program, just like the +vertex buffer: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyBuffer(device, indexBuffer, nullptr); + vkFreeMemory(device, indexBufferMemory, nullptr); + + vkDestroyBuffer(device, vertexBuffer, nullptr); + vkFreeMemory(device, vertexBufferMemory, nullptr); + + ... +} +``` + +## Using an index buffer + +Using an index buffer for drawing involves two changes to +`recordCommandBuffer`. We first need to bind the index buffer, just like we did +for the vertex buffer. The difference is that you can only have a single index +buffer. It's unfortunately not possible to use different indices for each vertex +attribute, so we do still have to completely duplicate vertex data even if just +one attribute varies. + +```c++ +vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets); + +vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT16); +``` + +An index buffer is bound with `vkCmdBindIndexBuffer` which has the index buffer, +a byte offset into it, and the type of index data as parameters. As mentioned +before, the possible types are `VK_INDEX_TYPE_UINT16` and +`VK_INDEX_TYPE_UINT32`. + +Just binding an index buffer doesn't change anything yet, we also need to change +the drawing command to tell Vulkan to use the index buffer. Remove the +`vkCmdDraw` line and replace it with `vkCmdDrawIndexed`: + +```c++ +vkCmdDrawIndexed(commandBuffer, static_cast(indices.size()), 1, 0, 0, 0); +``` + +A call to this function is very similar to `vkCmdDraw`. The first two parameters +specify the number of indices and the number of instances. We're not using +instancing, so just specify `1` instance. The number of indices represents the +number of vertices that will be passed to the vertex shader. The next parameter +specifies an offset into the index buffer, using a value of `1` would cause the +graphics card to start reading at the second index. The second to last parameter +specifies an offset to add to the indices in the index buffer. The final +parameter specifies an offset for instancing, which we're not using. + +Now run your program and you should see the following: + +![](/images/indexed_rectangle.png) + +You now know how to save memory by reusing vertices with index buffers. This +will become especially important in a future chapter where we're going to load +complex 3D models. + +The previous chapter already mentioned that you should allocate multiple +resources like buffers from a single memory allocation, but in fact you should +go a step further. [Driver developers recommend](https://developer.nvidia.com/vulkan-memory-management) +that you also store multiple buffers, like the vertex and index buffer, into a +single `VkBuffer` and use offsets in commands like `vkCmdBindVertexBuffers`. The +advantage is that your data is more cache friendly in that case, because it's +closer together. It is even possible to reuse the same chunk of memory for +multiple resources if they are not used during the same render operations, +provided that their data is refreshed, of course. This is known as *aliasing* +and some Vulkan functions have explicit flags to specify that you want to do +this. + +[C++ code](/code/21_index_buffer.cpp) / +[Vertex shader](/code/18_shader_vertexbuffer.vert) / +[Fragment shader](/code/18_shader_vertexbuffer.frag) diff --git a/ko-rust/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md b/ko-rust/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md new file mode 100644 index 00000000..2bdcc2dc --- /dev/null +++ b/ko-rust/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md @@ -0,0 +1,416 @@ +## Introduction + +We're now able to pass arbitrary attributes to the vertex shader for each +vertex, but what about global variables? We're going to move on to 3D graphics +from this chapter on and that requires a model-view-projection matrix. We could +include it as vertex data, but that's a waste of memory and it would require us +to update the vertex buffer whenever the transformation changes. The +transformation could easily change every single frame. + +The right way to tackle this in Vulkan is to use *resource descriptors*. A +descriptor is a way for shaders to freely access resources like buffers and +images. We're going to set up a buffer that contains the transformation matrices +and have the vertex shader access them through a descriptor. Usage of +descriptors consists of three parts: + +* Specify a descriptor set layout during pipeline creation +* Allocate a descriptor set from a descriptor pool +* Bind the descriptor set during rendering + +The *descriptor set layout* specifies the types of resources that are going to be +accessed by the pipeline, just like a render pass specifies the types of +attachments that will be accessed. A *descriptor set* specifies the actual +buffer or image resources that will be bound to the descriptors, just like a +framebuffer specifies the actual image views to bind to render pass attachments. +The descriptor set is then bound for the drawing commands just like the vertex +buffers and framebuffer. + +There are many types of descriptors, but in this chapter we'll work with uniform +buffer objects (UBO). We'll look at other types of descriptors in future +chapters, but the basic process is the same. Let's say we have the data we want +the vertex shader to have in a C struct like this: + +```c++ +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; +``` + +Then we can copy the data to a `VkBuffer` and access it through a uniform buffer +object descriptor from the vertex shader like this: + +```glsl +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; + +void main() { + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0); + fragColor = inColor; +} +``` + +We're going to update the model, view and projection matrices every frame to +make the rectangle from the previous chapter spin around in 3D. + +## Vertex shader + +Modify the vertex shader to include the uniform buffer object like it was +specified above. I will assume that you are familiar with MVP transformations. +If you're not, see [the resource](https://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/) +mentioned in the first chapter. + +```glsl +#version 450 + +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; + +layout(location = 0) in vec2 inPosition; +layout(location = 1) in vec3 inColor; + +layout(location = 0) out vec3 fragColor; + +void main() { + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0); + fragColor = inColor; +} +``` + +Note that the order of the `uniform`, `in` and `out` declarations doesn't +matter. The `binding` directive is similar to the `location` directive for +attributes. We're going to reference this binding in the descriptor set layout. The +line with `gl_Position` is changed to use the transformations to compute the +final position in clip coordinates. Unlike the 2D triangles, the last component +of the clip coordinates may not be `1`, which will result in a division when +converted to the final normalized device coordinates on the screen. This is used +in perspective projection as the *perspective division* and is essential for +making closer objects look larger than objects that are further away. + +## Descriptor set layout + +The next step is to define the UBO on the C++ side and to tell Vulkan about this +descriptor in the vertex shader. + +```c++ +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; +``` + +We can exactly match the definition in the shader using data types in GLM. The +data in the matrices is binary compatible with the way the shader expects it, so +we can later just `memcpy` a `UniformBufferObject` to a `VkBuffer`. + +We need to provide details about every descriptor binding used in the shaders +for pipeline creation, just like we had to do for every vertex attribute and its +`location` index. We'll set up a new function to define all of this information +called `createDescriptorSetLayout`. It should be called right before pipeline +creation, because we're going to need it there. + +```c++ +void initVulkan() { + ... + createDescriptorSetLayout(); + createGraphicsPipeline(); + ... +} + +... + +void createDescriptorSetLayout() { + +} +``` + +Every binding needs to be described through a `VkDescriptorSetLayoutBinding` +struct. + +```c++ +void createDescriptorSetLayout() { + VkDescriptorSetLayoutBinding uboLayoutBinding{}; + uboLayoutBinding.binding = 0; + uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + uboLayoutBinding.descriptorCount = 1; +} +``` + +The first two fields specify the `binding` used in the shader and the type of +descriptor, which is a uniform buffer object. It is possible for the shader +variable to represent an array of uniform buffer objects, and `descriptorCount` +specifies the number of values in the array. This could be used to specify a +transformation for each of the bones in a skeleton for skeletal animation, for +example. Our MVP transformation is in a single uniform buffer object, so we're +using a `descriptorCount` of `1`. + +```c++ +uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; +``` + +We also need to specify in which shader stages the descriptor is going to be +referenced. The `stageFlags` field can be a combination of `VkShaderStageFlagBits` values +or the value `VK_SHADER_STAGE_ALL_GRAPHICS`. In our case, we're only referencing +the descriptor from the vertex shader. + +```c++ +uboLayoutBinding.pImmutableSamplers = nullptr; // Optional +``` + +The `pImmutableSamplers` field is only relevant for image sampling related +descriptors, which we'll look at later. You can leave this to its default value. + +All of the descriptor bindings are combined into a single +`VkDescriptorSetLayout` object. Define a new class member above +`pipelineLayout`: + +```c++ +VkDescriptorSetLayout descriptorSetLayout; +VkPipelineLayout pipelineLayout; +``` + +We can then create it using `vkCreateDescriptorSetLayout`. This function accepts +a simple `VkDescriptorSetLayoutCreateInfo` with the array of bindings: + +```c++ +VkDescriptorSetLayoutCreateInfo layoutInfo{}; +layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; +layoutInfo.bindingCount = 1; +layoutInfo.pBindings = &uboLayoutBinding; + +if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) { + throw std::runtime_error("failed to create descriptor set layout!"); +} +``` + +We need to specify the descriptor set layout during pipeline creation to tell +Vulkan which descriptors the shaders will be using. Descriptor set layouts are +specified in the pipeline layout object. Modify the `VkPipelineLayoutCreateInfo` +to reference the layout object: + +```c++ +VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; +pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; +pipelineLayoutInfo.setLayoutCount = 1; +pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout; +``` + +You may be wondering why it's possible to specify multiple descriptor set +layouts here, because a single one already includes all of the bindings. We'll +get back to that in the next chapter, where we'll look into descriptor pools and +descriptor sets. + +The descriptor set layout should stick around while we may create new graphics +pipelines i.e. until the program ends: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); + + ... +} +``` + +## Uniform buffer + +In the next chapter we'll specify the buffer that contains the UBO data for the +shader, but we need to create this buffer first. We're going to copy new data to +the uniform buffer every frame, so it doesn't really make any sense to have a +staging buffer. It would just add extra overhead in this case and likely degrade +performance instead of improving it. + +We should have multiple buffers, because multiple frames may be in flight at the same +time and we don't want to update the buffer in preparation of the next frame while a +previous one is still reading from it! Thus, we need to have as many uniform buffers +as we have frames in flight, and write to a uniform buffer that is not currently +being read by the GPU. + +To that end, add new class members for `uniformBuffers`, and `uniformBuffersMemory`: + +```c++ +VkBuffer indexBuffer; +VkDeviceMemory indexBufferMemory; + +std::vector uniformBuffers; +std::vector uniformBuffersMemory; +std::vector uniformBuffersMapped; +``` + +Similarly, create a new function `createUniformBuffers` that is called after +`createIndexBuffer` and allocates the buffers: + +```c++ +void initVulkan() { + ... + createVertexBuffer(); + createIndexBuffer(); + createUniformBuffers(); + ... +} + +... + +void createUniformBuffers() { + VkDeviceSize bufferSize = sizeof(UniformBufferObject); + + uniformBuffers.resize(MAX_FRAMES_IN_FLIGHT); + uniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT); + uniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, uniformBuffers[i], uniformBuffersMemory[i]); + + vkMapMemory(device, uniformBuffersMemory[i], 0, bufferSize, 0, &uniformBuffersMapped[i]); + } +} +``` + +We map the buffer right after creation using `vkMapMemory` to get a pointer to which we can write the data later on. The buffer stays mapped to this pointer for the application's whole lifetime. This technique is called **"persistent mapping"** and works on all Vulkan implementations. Not having to map the buffer every time we need to update it increases performances, as mapping is not free. + +The uniform data will be used for all draw calls, so the buffer containing it should only be destroyed when we stop rendering. + +```c++ +void cleanup() { + ... + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vkDestroyBuffer(device, uniformBuffers[i], nullptr); + vkFreeMemory(device, uniformBuffersMemory[i], nullptr); + } + + vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); + + ... + +} +``` + +## Updating uniform data + +Create a new function `updateUniformBuffer` and add a call to it from the `drawFrame` function before submitting the next frame: + +```c++ +void drawFrame() { + ... + + updateUniformBuffer(currentFrame); + + ... + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + + ... +} + +... + +void updateUniformBuffer(uint32_t currentImage) { + +} +``` + +This function will generate a new transformation every frame to make the +geometry spin around. We need to include two new headers to implement this +functionality: + +```c++ +#define GLM_FORCE_RADIANS +#include +#include + +#include +``` + +The `glm/gtc/matrix_transform.hpp` header exposes functions that can be used to +generate model transformations like `glm::rotate`, view transformations like +`glm::lookAt` and projection transformations like `glm::perspective`. The +`GLM_FORCE_RADIANS` definition is necessary to make sure that functions like +`glm::rotate` use radians as arguments, to avoid any possible confusion. + +The `chrono` standard library header exposes functions to do precise +timekeeping. We'll use this to make sure that the geometry rotates 90 degrees +per second regardless of frame rate. + +```c++ +void updateUniformBuffer(uint32_t currentImage) { + static auto startTime = std::chrono::high_resolution_clock::now(); + + auto currentTime = std::chrono::high_resolution_clock::now(); + float time = std::chrono::duration(currentTime - startTime).count(); +} +``` + +The `updateUniformBuffer` function will start out with some logic to calculate +the time in seconds since rendering has started with floating point accuracy. + +We will now define the model, view and projection transformations in the +uniform buffer object. The model rotation will be a simple rotation around the +Z-axis using the `time` variable: + +```c++ +UniformBufferObject ubo{}; +ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f)); +``` + +The `glm::rotate` function takes an existing transformation, rotation angle and +rotation axis as parameters. The `glm::mat4(1.0f)` constructor returns an +identity matrix. Using a rotation angle of `time * glm::radians(90.0f)` +accomplishes the purpose of rotation 90 degrees per second. + +```c++ +ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)); +``` + +For the view transformation I've decided to look at the geometry from above at a +45 degree angle. The `glm::lookAt` function takes the eye position, center +position and up axis as parameters. + +```c++ +ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f); +``` + +I've chosen to use a perspective projection with a 45 degree vertical +field-of-view. The other parameters are the aspect ratio, near and far +view planes. It is important to use the current swap chain extent to calculate +the aspect ratio to take into account the new width and height of the window +after a resize. + +```c++ +ubo.proj[1][1] *= -1; +``` + +GLM was originally designed for OpenGL, where the Y coordinate of the clip +coordinates is inverted. The easiest way to compensate for that is to flip the +sign on the scaling factor of the Y axis in the projection matrix. If you don't +do this, then the image will be rendered upside down. + +All of the transformations are defined now, so we can copy the data in the +uniform buffer object to the current uniform buffer. This happens in exactly the same +way as we did for vertex buffers, except without a staging buffer. As noted earlier, we only map the uniform buffer once, so we can directly write to it without having to map again: + +```c++ +memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); +``` + +Using a UBO this way is not the most efficient way to pass frequently changing +values to the shader. A more efficient way to pass a small buffer of data to +shaders are *push constants*. We may look at these in a future chapter. + +In the next chapter we'll look at descriptor sets, which will actually bind the +`VkBuffer`s to the uniform buffer descriptors so that the shader can access this +transformation data. + +[C++ code](/code/22_descriptor_set_layout.cpp) / +[Vertex shader](/code/22_shader_ubo.vert) / +[Fragment shader](/code/22_shader_ubo.frag) diff --git a/ko-rust/05_Uniform_buffers/01_Descriptor_pool_and_sets.md b/ko-rust/05_Uniform_buffers/01_Descriptor_pool_and_sets.md new file mode 100644 index 00000000..b204db24 --- /dev/null +++ b/ko-rust/05_Uniform_buffers/01_Descriptor_pool_and_sets.md @@ -0,0 +1,391 @@ +## Introduction + +The descriptor set layout from the previous chapter describes the type of +descriptors that can be bound. In this chapter we're going to create +a descriptor set for each `VkBuffer` resource to bind it to the +uniform buffer descriptor. + +## Descriptor pool + +Descriptor sets can't be created directly, they must be allocated from a pool +like command buffers. The equivalent for descriptor sets is unsurprisingly +called a *descriptor pool*. We'll write a new function `createDescriptorPool` +to set it up. + +```c++ +void initVulkan() { + ... + createUniformBuffers(); + createDescriptorPool(); + ... +} + +... + +void createDescriptorPool() { + +} +``` + +We first need to describe which descriptor types our descriptor sets are going +to contain and how many of them, using `VkDescriptorPoolSize` structures. + +```c++ +VkDescriptorPoolSize poolSize{}; +poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +poolSize.descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); +``` + +We will allocate one of these descriptors for every frame. This +pool size structure is referenced by the main `VkDescriptorPoolCreateInfo`: + +```c++ +VkDescriptorPoolCreateInfo poolInfo{}; +poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; +poolInfo.poolSizeCount = 1; +poolInfo.pPoolSizes = &poolSize; +``` + +Aside from the maximum number of individual descriptors that are available, we +also need to specify the maximum number of descriptor sets that may be +allocated: + +```c++ +poolInfo.maxSets = static_cast(MAX_FRAMES_IN_FLIGHT); +``` + +The structure has an optional flag similar to command pools that determines if +individual descriptor sets can be freed or not: +`VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT`. We're not going to touch +the descriptor set after creating it, so we don't need this flag. You can leave +`flags` to its default value of `0`. + +```c++ +VkDescriptorPool descriptorPool; + +... + +if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) { + throw std::runtime_error("failed to create descriptor pool!"); +} +``` + +Add a new class member to store the handle of the descriptor pool and call +`vkCreateDescriptorPool` to create it. + +## Descriptor set + +We can now allocate the descriptor sets themselves. Add a `createDescriptorSets` +function for that purpose: + +```c++ +void initVulkan() { + ... + createDescriptorPool(); + createDescriptorSets(); + ... +} + +... + +void createDescriptorSets() { + +} +``` + +A descriptor set allocation is described with a `VkDescriptorSetAllocateInfo` +struct. You need to specify the descriptor pool to allocate from, the number of +descriptor sets to allocate, and the descriptor set layout to base them on: + +```c++ +std::vector layouts(MAX_FRAMES_IN_FLIGHT, descriptorSetLayout); +VkDescriptorSetAllocateInfo allocInfo{}; +allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; +allocInfo.descriptorPool = descriptorPool; +allocInfo.descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT); +allocInfo.pSetLayouts = layouts.data(); +``` + +In our case we will create one descriptor set for each frame in flight, all with the same layout. +Unfortunately we do need all the copies of the layout because the next function expects an array matching the number of sets. + +Add a class member to hold the descriptor set handles and allocate them with +`vkAllocateDescriptorSets`: + +```c++ +VkDescriptorPool descriptorPool; +std::vector descriptorSets; + +... + +descriptorSets.resize(MAX_FRAMES_IN_FLIGHT); +if (vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate descriptor sets!"); +} +``` + +You don't need to explicitly clean up descriptor sets, because they will be +automatically freed when the descriptor pool is destroyed. The call to +`vkAllocateDescriptorSets` will allocate descriptor sets, each with one uniform +buffer descriptor. + +```c++ +void cleanup() { + ... + vkDestroyDescriptorPool(device, descriptorPool, nullptr); + + vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); + ... +} +``` + +The descriptor sets have been allocated now, but the descriptors within still need +to be configured. We'll now add a loop to populate every descriptor: + +```c++ +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + +} +``` + +Descriptors that refer to buffers, like our uniform buffer +descriptor, are configured with a `VkDescriptorBufferInfo` struct. This +structure specifies the buffer and the region within it that contains the data +for the descriptor. + +```c++ +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + VkDescriptorBufferInfo bufferInfo{}; + bufferInfo.buffer = uniformBuffers[i]; + bufferInfo.offset = 0; + bufferInfo.range = sizeof(UniformBufferObject); +} +``` + +If you're overwriting the whole buffer, like we are in this case, then it is also possible to use the `VK_WHOLE_SIZE` value for the range. The configuration of descriptors is updated using the `vkUpdateDescriptorSets` +function, which takes an array of `VkWriteDescriptorSet` structs as parameter. + +```c++ +VkWriteDescriptorSet descriptorWrite{}; +descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; +descriptorWrite.dstSet = descriptorSets[i]; +descriptorWrite.dstBinding = 0; +descriptorWrite.dstArrayElement = 0; +``` + +The first two fields specify the descriptor set to update and the binding. We +gave our uniform buffer binding index `0`. Remember that descriptors can be +arrays, so we also need to specify the first index in the array that we want to +update. We're not using an array, so the index is simply `0`. + +```c++ +descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +descriptorWrite.descriptorCount = 1; +``` + +We need to specify the type of descriptor again. It's possible to update +multiple descriptors at once in an array, starting at index `dstArrayElement`. +The `descriptorCount` field specifies how many array elements you want to +update. + +```c++ +descriptorWrite.pBufferInfo = &bufferInfo; +descriptorWrite.pImageInfo = nullptr; // Optional +descriptorWrite.pTexelBufferView = nullptr; // Optional +``` + +The last field references an array with `descriptorCount` structs that actually +configure the descriptors. It depends on the type of descriptor which one of the +three you actually need to use. The `pBufferInfo` field is used for descriptors +that refer to buffer data, `pImageInfo` is used for descriptors that refer to +image data, and `pTexelBufferView` is used for descriptors that refer to buffer +views. Our descriptor is based on buffers, so we're using `pBufferInfo`. + +```c++ +vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr); +``` + +The updates are applied using `vkUpdateDescriptorSets`. It accepts two kinds of +arrays as parameters: an array of `VkWriteDescriptorSet` and an array of +`VkCopyDescriptorSet`. The latter can be used to copy descriptors to each other, +as its name implies. + +## Using descriptor sets + +We now need to update the `recordCommandBuffer` function to actually bind the +right descriptor set for each frame to the descriptors in the shader with `vkCmdBindDescriptorSets`. This needs to be done before the `vkCmdDrawIndexed` call: + +```c++ +vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSets[currentFrame], 0, nullptr); +vkCmdDrawIndexed(commandBuffer, static_cast(indices.size()), 1, 0, 0, 0); +``` + +Unlike vertex and index buffers, descriptor sets are not unique to graphics +pipelines. Therefore we need to specify if we want to bind descriptor sets to +the graphics or compute pipeline. The next parameter is the layout that the +descriptors are based on. The next three parameters specify the index of the +first descriptor set, the number of sets to bind, and the array of sets to bind. +We'll get back to this in a moment. The last two parameters specify an array of +offsets that are used for dynamic descriptors. We'll look at these in a future +chapter. + +If you run your program now, then you'll notice that unfortunately nothing is +visible. The problem is that because of the Y-flip we did in the projection +matrix, the vertices are now being drawn in counter-clockwise order instead of +clockwise order. This causes backface culling to kick in and prevents +any geometry from being drawn. Go to the `createGraphicsPipeline` function and +modify the `frontFace` in `VkPipelineRasterizationStateCreateInfo` to correct +this: + +```c++ +rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; +rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; +``` + +Run your program again and you should now see the following: + +![](/images/spinning_quad.png) + +The rectangle has changed into a square because the projection matrix now +corrects for aspect ratio. The `updateUniformBuffer` takes care of screen +resizing, so we don't need to recreate the descriptor set in +`recreateSwapChain`. + +## Alignment requirements + +One thing we've glossed over so far is how exactly the data in the C++ structure should match with the uniform definition in the shader. It seems obvious enough to simply use the same types in both: + +```c++ +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; + +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; +``` + +However, that's not all there is to it. For example, try modifying the struct and shader to look like this: + +```c++ +struct UniformBufferObject { + glm::vec2 foo; + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; + +layout(binding = 0) uniform UniformBufferObject { + vec2 foo; + mat4 model; + mat4 view; + mat4 proj; +} ubo; +``` + +Recompile your shader and your program and run it and you'll find that the colorful square you worked so far has disappeared! That's because we haven't taken into account the *alignment requirements*. + +Vulkan expects the data in your structure to be aligned in memory in a specific way, for example: + +* Scalars have to be aligned by N (= 4 bytes given 32 bit floats). +* A `vec2` must be aligned by 2N (= 8 bytes) +* A `vec3` or `vec4` must be aligned by 4N (= 16 bytes) +* A nested structure must be aligned by the base alignment of its members rounded up to a multiple of 16. +* A `mat4` matrix must have the same alignment as a `vec4`. + +You can find the full list of alignment requirements in [the specification](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap15.html#interfaces-resources-layout). + +Our original shader with just three `mat4` fields already met the alignment requirements. As each `mat4` is 4 x 4 x 4 = 64 bytes in size, `model` has an offset of `0`, `view` has an offset of 64 and `proj` has an offset of 128. All of these are multiples of 16 and that's why it worked fine. + +The new structure starts with a `vec2` which is only 8 bytes in size and therefore throws off all of the offsets. Now `model` has an offset of `8`, `view` an offset of `72` and `proj` an offset of `136`, none of which are multiples of 16. To fix this problem we can use the [`alignas`](https://en.cppreference.com/w/cpp/language/alignas) specifier introduced in C++11: + +```c++ +struct UniformBufferObject { + glm::vec2 foo; + alignas(16) glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; +``` + +If you now compile and run your program again you should see that the shader correctly receives its matrix values once again. + +Luckily there is a way to not have to think about these alignment requirements *most* of the time. We can define `GLM_FORCE_DEFAULT_ALIGNED_GENTYPES` right before including GLM: + +```c++ +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES +#include +``` + +This will force GLM to use a version of `vec2` and `mat4` that has the alignment requirements already specified for us. If you add this definition then you can remove the `alignas` specifier and your program should still work. + +Unfortunately this method can break down if you start using nested structures. Consider the following definition in the C++ code: + +```c++ +struct Foo { + glm::vec2 v; +}; + +struct UniformBufferObject { + Foo f1; + Foo f2; +}; +``` + +And the following shader definition: + +```c++ +struct Foo { + vec2 v; +}; + +layout(binding = 0) uniform UniformBufferObject { + Foo f1; + Foo f2; +} ubo; +``` + +In this case `f2` will have an offset of `8` whereas it should have an offset of `16` since it is a nested structure. In this case you must specify the alignment yourself: + +```c++ +struct UniformBufferObject { + Foo f1; + alignas(16) Foo f2; +}; +``` + +These gotchas are a good reason to always be explicit about alignment. That way you won't be caught offguard by the strange symptoms of alignment errors. + +```c++ +struct UniformBufferObject { + alignas(16) glm::mat4 model; + alignas(16) glm::mat4 view; + alignas(16) glm::mat4 proj; +}; +``` + +Don't forget to recompile your shader after removing the `foo` field. + +## Multiple descriptor sets + +As some of the structures and function calls hinted at, it is actually possible +to bind multiple descriptor sets simultaneously. You need to specify a descriptor set layout for +each descriptor set when creating the pipeline layout. Shaders can then +reference specific descriptor sets like this: + +```c++ +layout(set = 0, binding = 0) uniform UniformBufferObject { ... } +``` + +You can use this feature to put descriptors that vary per-object and descriptors +that are shared into separate descriptor sets. In that case you avoid rebinding +most of the descriptors across draw calls which is potentially more efficient. + +[C++ code](/code/23_descriptor_sets.cpp) / +[Vertex shader](/code/22_shader_ubo.vert) / +[Fragment shader](/code/22_shader_ubo.frag) diff --git a/ko-rust/06_Texture_mapping/00_Images.md b/ko-rust/06_Texture_mapping/00_Images.md new file mode 100644 index 00000000..8c9967f6 --- /dev/null +++ b/ko-rust/06_Texture_mapping/00_Images.md @@ -0,0 +1,769 @@ +## Introduction + +The geometry has been colored using per-vertex colors so far, which is a rather +limited approach. In this part of the tutorial we're going to implement texture +mapping to make the geometry look more interesting. This will also allow us to +load and draw basic 3D models in a future chapter. + +Adding a texture to our application will involve the following steps: + +* Create an image object backed by device memory +* Fill it with pixels from an image file +* Create an image sampler +* Add a combined image sampler descriptor to sample colors from the texture + +We've already worked with image objects before, but those were automatically +created by the swap chain extension. This time we'll have to create one by +ourselves. Creating an image and filling it with data is similar to vertex +buffer creation. We'll start by creating a staging resource and filling it with +pixel data and then we copy this to the final image object that we'll use for +rendering. Although it is possible to create a staging image for this purpose, +Vulkan also allows you to copy pixels from a `VkBuffer` to an image and the API +for this is actually [faster on some hardware](https://developer.nvidia.com/vulkan-memory-management). +We'll first create this buffer and fill it with pixel values, and then we'll +create an image to copy the pixels to. Creating an image is not very different +from creating buffers. It involves querying the memory requirements, allocating +device memory and binding it, just like we've seen before. + +However, there is something extra that we'll have to take care of when working +with images. Images can have different *layouts* that affect how the pixels are +organized in memory. Due to the way graphics hardware works, simply storing the +pixels row by row may not lead to the best performance, for example. When +performing any operation on images, you must make sure that they have the layout +that is optimal for use in that operation. We've actually already seen some of +these layouts when we specified the render pass: + +* `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`: Optimal for presentation +* `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`: Optimal as attachment for writing +colors from the fragment shader +* `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`: Optimal as source in a transfer +operation, like `vkCmdCopyImageToBuffer` +* `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`: Optimal as destination in a transfer +operation, like `vkCmdCopyBufferToImage` +* `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`: Optimal for sampling from a shader + +One of the most common ways to transition the layout of an image is a *pipeline +barrier*. Pipeline barriers are primarily used for synchronizing access to +resources, like making sure that an image was written to before it is read, but +they can also be used to transition layouts. In this chapter we'll see how +pipeline barriers are used for this purpose. Barriers can additionally be used +to transfer queue family ownership when using `VK_SHARING_MODE_EXCLUSIVE`. + +## Image library + +There are many libraries available for loading images, and you can even write +your own code to load simple formats like BMP and PPM. In this tutorial we'll be +using the stb_image library from the [stb collection](https://github.com/nothings/stb). +The advantage of it is that all of the code is in a single file, so it doesn't +require any tricky build configuration. Download `stb_image.h` and store it in a +convenient location, like the directory where you saved GLFW and GLM. Add the +location to your include path. + +**Visual Studio** + +Add the directory with `stb_image.h` in it to the `Additional Include +Directories` paths. + +![](/images/include_dirs_stb.png) + +**Makefile** + +Add the directory with `stb_image.h` to the include directories for GCC: + +```text +VULKAN_SDK_PATH = /home/user/VulkanSDK/x.x.x.x/x86_64 +STB_INCLUDE_PATH = /home/user/libraries/stb + +... + +CFLAGS = -std=c++17 -I$(VULKAN_SDK_PATH)/include -I$(STB_INCLUDE_PATH) +``` + +## Loading an image + +Include the image library like this: + +```c++ +#define STB_IMAGE_IMPLEMENTATION +#include +``` + +The header only defines the prototypes of the functions by default. One code +file needs to include the header with the `STB_IMAGE_IMPLEMENTATION` definition +to include the function bodies, otherwise we'll get linking errors. + +```c++ +void initVulkan() { + ... + createCommandPool(); + createTextureImage(); + createVertexBuffer(); + ... +} + +... + +void createTextureImage() { + +} +``` + +Create a new function `createTextureImage` where we'll load an image and upload +it into a Vulkan image object. We're going to use command buffers, so it should +be called after `createCommandPool`. + +Create a new directory `textures` next to the `shaders` directory to store +texture images in. We're going to load an image called `texture.jpg` from that +directory. I've chosen to use the following +[CC0 licensed image](https://pixabay.com/en/statue-sculpture-fig-historically-1275469/) +resized to 512 x 512 pixels, but feel free to pick any image you want. The +library supports most common image file formats, like JPEG, PNG, BMP and GIF. + +![](/images/texture.jpg) + +Loading an image with this library is really easy: + +```c++ +void createTextureImage() { + int texWidth, texHeight, texChannels; + stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); + VkDeviceSize imageSize = texWidth * texHeight * 4; + + if (!pixels) { + throw std::runtime_error("failed to load texture image!"); + } +} +``` + +The `stbi_load` function takes the file path and number of channels to load as +arguments. The `STBI_rgb_alpha` value forces the image to be loaded with an +alpha channel, even if it doesn't have one, which is nice for consistency with +other textures in the future. The middle three parameters are outputs for the +width, height and actual number of channels in the image. The pointer that is +returned is the first element in an array of pixel values. The pixels are laid +out row by row with 4 bytes per pixel in the case of `STBI_rgb_alpha` for a +total of `texWidth * texHeight * 4` values. + +## Staging buffer + +We're now going to create a buffer in host visible memory so that we can use +`vkMapMemory` and copy the pixels to it. Add variables for this temporary buffer +to the `createTextureImage` function: + +```c++ +VkBuffer stagingBuffer; +VkDeviceMemory stagingBufferMemory; +``` + +The buffer should be in host visible memory so that we can map it and it should +be usable as a transfer source so that we can copy it to an image later on: + +```c++ +createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); +``` + +We can then directly copy the pixel values that we got from the image loading +library to the buffer: + +```c++ +void* data; +vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data); + memcpy(data, pixels, static_cast(imageSize)); +vkUnmapMemory(device, stagingBufferMemory); +``` + +Don't forget to clean up the original pixel array now: + +```c++ +stbi_image_free(pixels); +``` + +## Texture Image + +Although we could set up the shader to access the pixel values in the buffer, +it's better to use image objects in Vulkan for this purpose. Image objects will +make it easier and faster to retrieve colors by allowing us to use 2D +coordinates, for one. Pixels within an image object are known as texels and +we'll use that name from this point on. Add the following new class members: + +```c++ +VkImage textureImage; +VkDeviceMemory textureImageMemory; +``` + +The parameters for an image are specified in a `VkImageCreateInfo` struct: + +```c++ +VkImageCreateInfo imageInfo{}; +imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; +imageInfo.imageType = VK_IMAGE_TYPE_2D; +imageInfo.extent.width = static_cast(texWidth); +imageInfo.extent.height = static_cast(texHeight); +imageInfo.extent.depth = 1; +imageInfo.mipLevels = 1; +imageInfo.arrayLayers = 1; +``` + +The image type, specified in the `imageType` field, tells Vulkan with what kind +of coordinate system the texels in the image are going to be addressed. It is +possible to create 1D, 2D and 3D images. One dimensional images can be used to +store an array of data or gradient, two dimensional images are mainly used for +textures, and three dimensional images can be used to store voxel volumes, for +example. The `extent` field specifies the dimensions of the image, basically how +many texels there are on each axis. That's why `depth` must be `1` instead of +`0`. Our texture will not be an array and we won't be using mipmapping for now. + +```c++ +imageInfo.format = VK_FORMAT_R8G8B8A8_SRGB; +``` + +Vulkan supports many possible image formats, but we should use the same format +for the texels as the pixels in the buffer, otherwise the copy operation will +fail. + +```c++ +imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; +``` + +The `tiling` field can have one of two values: + +* `VK_IMAGE_TILING_LINEAR`: Texels are laid out in row-major order like our +`pixels` array +* `VK_IMAGE_TILING_OPTIMAL`: Texels are laid out in an implementation defined +order for optimal access + +Unlike the layout of an image, the tiling mode cannot be changed at a later +time. If you want to be able to directly access texels in the memory of the +image, then you must use `VK_IMAGE_TILING_LINEAR`. We will be using a staging +buffer instead of a staging image, so this won't be necessary. We will be using +`VK_IMAGE_TILING_OPTIMAL` for efficient access from the shader. + +```c++ +imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; +``` + +There are only two possible values for the `initialLayout` of an image: + +* `VK_IMAGE_LAYOUT_UNDEFINED`: Not usable by the GPU and the very first +transition will discard the texels. +* `VK_IMAGE_LAYOUT_PREINITIALIZED`: Not usable by the GPU, but the first +transition will preserve the texels. + +There are few situations where it is necessary for the texels to be preserved +during the first transition. One example, however, would be if you wanted to use +an image as a staging image in combination with the `VK_IMAGE_TILING_LINEAR` +layout. In that case, you'd want to upload the texel data to it and then +transition the image to be a transfer source without losing the data. In our +case, however, we're first going to transition the image to be a transfer +destination and then copy texel data to it from a buffer object, so we don't +need this property and can safely use `VK_IMAGE_LAYOUT_UNDEFINED`. + +```c++ +imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; +``` + +The `usage` field has the same semantics as the one during buffer creation. The +image is going to be used as destination for the buffer copy, so it should be +set up as a transfer destination. We also want to be able to access the image +from the shader to color our mesh, so the usage should include +`VK_IMAGE_USAGE_SAMPLED_BIT`. + +```c++ +imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; +``` + +The image will only be used by one queue family: the one that supports graphics +(and therefore also) transfer operations. + +```c++ +imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; +imageInfo.flags = 0; // Optional +``` + +The `samples` flag is related to multisampling. This is only relevant for images +that will be used as attachments, so stick to one sample. There are some +optional flags for images that are related to sparse images. Sparse images are +images where only certain regions are actually backed by memory. If you were +using a 3D texture for a voxel terrain, for example, then you could use this to +avoid allocating memory to store large volumes of "air" values. We won't be +using it in this tutorial, so leave it to its default value of `0`. + +```c++ +if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) { + throw std::runtime_error("failed to create image!"); +} +``` + +The image is created using `vkCreateImage`, which doesn't have any particularly +noteworthy parameters. It is possible that the `VK_FORMAT_R8G8B8A8_SRGB` format +is not supported by the graphics hardware. You should have a list of acceptable +alternatives and go with the best one that is supported. However, support for +this particular format is so widespread that we'll skip this step. Using +different formats would also require annoying conversions. We will get back to +this in the depth buffer chapter, where we'll implement such a system. + +```c++ +VkMemoryRequirements memRequirements; +vkGetImageMemoryRequirements(device, textureImage, &memRequirements); + +VkMemoryAllocateInfo allocInfo{}; +allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; +allocInfo.allocationSize = memRequirements.size; +allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + +if (vkAllocateMemory(device, &allocInfo, nullptr, &textureImageMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate image memory!"); +} + +vkBindImageMemory(device, textureImage, textureImageMemory, 0); +``` + +Allocating memory for an image works in exactly the same way as allocating +memory for a buffer. Use `vkGetImageMemoryRequirements` instead of +`vkGetBufferMemoryRequirements`, and use `vkBindImageMemory` instead of +`vkBindBufferMemory`. + +This function is already getting quite large and there'll be a need to create +more images in later chapters, so we should abstract image creation into a +`createImage` function, like we did for buffers. Create the function and move +the image object creation and memory allocation to it: + +```c++ +void createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) { + VkImageCreateInfo imageInfo{}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.extent.width = width; + imageInfo.extent.height = height; + imageInfo.extent.depth = 1; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.format = format; + imageInfo.tiling = tiling; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageInfo.usage = usage; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateImage(device, &imageInfo, nullptr, &image) != VK_SUCCESS) { + throw std::runtime_error("failed to create image!"); + } + + VkMemoryRequirements memRequirements; + vkGetImageMemoryRequirements(device, image, &memRequirements); + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); + + if (vkAllocateMemory(device, &allocInfo, nullptr, &imageMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate image memory!"); + } + + vkBindImageMemory(device, image, imageMemory, 0); +} +``` + +I've made the width, height, format, tiling mode, usage, and memory properties +parameters, because these will all vary between the images we'll be creating +throughout this tutorial. + +The `createTextureImage` function can now be simplified to: + +```c++ +void createTextureImage() { + int texWidth, texHeight, texChannels; + stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); + VkDeviceSize imageSize = texWidth * texHeight * 4; + + if (!pixels) { + throw std::runtime_error("failed to load texture image!"); + } + + VkBuffer stagingBuffer; + VkDeviceMemory stagingBufferMemory; + createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); + + void* data; + vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data); + memcpy(data, pixels, static_cast(imageSize)); + vkUnmapMemory(device, stagingBufferMemory); + + stbi_image_free(pixels); + + createImage(texWidth, texHeight, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory); +} +``` + +## Layout transitions + +The function we're going to write now involves recording and executing a command +buffer again, so now's a good time to move that logic into a helper function or +two: + +```c++ +VkCommandBuffer beginSingleTimeCommands() { + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandPool = commandPool; + allocInfo.commandBufferCount = 1; + + VkCommandBuffer commandBuffer; + vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); + + VkCommandBufferBeginInfo beginInfo{}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + + vkBeginCommandBuffer(commandBuffer, &beginInfo); + + return commandBuffer; +} + +void endSingleTimeCommands(VkCommandBuffer commandBuffer) { + vkEndCommandBuffer(commandBuffer); + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &commandBuffer; + + vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); + vkQueueWaitIdle(graphicsQueue); + + vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); +} +``` + +The code for these functions is based on the existing code in `copyBuffer`. You +can now simplify that function to: + +```c++ +void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { + VkCommandBuffer commandBuffer = beginSingleTimeCommands(); + + VkBufferCopy copyRegion{}; + copyRegion.size = size; + vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region); + + endSingleTimeCommands(commandBuffer); +} +``` + +If we were still using buffers, then we could now write a function to record and +execute `vkCmdCopyBufferToImage` to finish the job, but this command requires +the image to be in the right layout first. Create a new function to handle +layout transitions: + +```c++ +void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout) { + VkCommandBuffer commandBuffer = beginSingleTimeCommands(); + + endSingleTimeCommands(commandBuffer); +} +``` + +One of the most common ways to perform layout transitions is using an *image +memory barrier*. A pipeline barrier like that is generally used to synchronize +access to resources, like ensuring that a write to a buffer completes before +reading from it, but it can also be used to transition image layouts and +transfer queue family ownership when `VK_SHARING_MODE_EXCLUSIVE` is used. There +is an equivalent *buffer memory barrier* to do this for buffers. + +```c++ +VkImageMemoryBarrier barrier{}; +barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; +barrier.oldLayout = oldLayout; +barrier.newLayout = newLayout; +``` + +The first two fields specify layout transition. It is possible to use +`VK_IMAGE_LAYOUT_UNDEFINED` as `oldLayout` if you don't care about the existing +contents of the image. + +```c++ +barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; +barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; +``` + +If you are using the barrier to transfer queue family ownership, then these two +fields should be the indices of the queue families. They must be set to +`VK_QUEUE_FAMILY_IGNORED` if you don't want to do this (not the default value!). + +```c++ +barrier.image = image; +barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +barrier.subresourceRange.baseMipLevel = 0; +barrier.subresourceRange.levelCount = 1; +barrier.subresourceRange.baseArrayLayer = 0; +barrier.subresourceRange.layerCount = 1; +``` + +The `image` and `subresourceRange` specify the image that is affected and the +specific part of the image. Our image is not an array and does not have mipmapping +levels, so only one level and layer are specified. + +```c++ +barrier.srcAccessMask = 0; // TODO +barrier.dstAccessMask = 0; // TODO +``` + +Barriers are primarily used for synchronization purposes, so you must specify +which types of operations that involve the resource must happen before the +barrier, and which operations that involve the resource must wait on the +barrier. We need to do that despite already using `vkQueueWaitIdle` to manually +synchronize. The right values depend on the old and new layout, so we'll get +back to this once we've figured out which transitions we're going to use. + +```c++ +vkCmdPipelineBarrier( + commandBuffer, + 0 /* TODO */, 0 /* TODO */, + 0, + 0, nullptr, + 0, nullptr, + 1, &barrier +); +``` + +All types of pipeline barriers are submitted using the same function. The first +parameter after the command buffer specifies in which pipeline stage the +operations occur that should happen before the barrier. The second parameter +specifies the pipeline stage in which operations will wait on the barrier. The +pipeline stages that you are allowed to specify before and after the barrier +depend on how you use the resource before and after the barrier. The allowed +values are listed in [this table](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-access-types-supported) +of the specification. For example, if you're going to read from a uniform after +the barrier, you would specify a usage of `VK_ACCESS_UNIFORM_READ_BIT` and the +earliest shader that will read from the uniform as pipeline stage, for example +`VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT`. It would not make sense to specify +a non-shader pipeline stage for this type of usage and the validation layers +will warn you when you specify a pipeline stage that does not match the type of +usage. + +The third parameter is either `0` or `VK_DEPENDENCY_BY_REGION_BIT`. The latter +turns the barrier into a per-region condition. That means that the +implementation is allowed to already begin reading from the parts of a resource +that were written so far, for example. + +The last three pairs of parameters reference arrays of pipeline barriers of the +three available types: memory barriers, buffer memory barriers, and image memory +barriers like the one we're using here. Note that we're not using the `VkFormat` +parameter yet, but we'll be using that one for special transitions in the depth +buffer chapter. + +## Copying buffer to image + +Before we get back to `createTextureImage`, we're going to write one more helper +function: `copyBufferToImage`: + +```c++ +void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) { + VkCommandBuffer commandBuffer = beginSingleTimeCommands(); + + endSingleTimeCommands(commandBuffer); +} +``` + +Just like with buffer copies, you need to specify which part of the buffer is +going to be copied to which part of the image. This happens through +`VkBufferImageCopy` structs: + +```c++ +VkBufferImageCopy region{}; +region.bufferOffset = 0; +region.bufferRowLength = 0; +region.bufferImageHeight = 0; + +region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +region.imageSubresource.mipLevel = 0; +region.imageSubresource.baseArrayLayer = 0; +region.imageSubresource.layerCount = 1; + +region.imageOffset = {0, 0, 0}; +region.imageExtent = { + width, + height, + 1 +}; +``` + +Most of these fields are self-explanatory. The `bufferOffset` specifies the byte +offset in the buffer at which the pixel values start. The `bufferRowLength` and +`bufferImageHeight` fields specify how the pixels are laid out in memory. For +example, you could have some padding bytes between rows of the image. Specifying +`0` for both indicates that the pixels are simply tightly packed like they are +in our case. The `imageSubresource`, `imageOffset` and `imageExtent` fields +indicate to which part of the image we want to copy the pixels. + +Buffer to image copy operations are enqueued using the `vkCmdCopyBufferToImage` +function: + +```c++ +vkCmdCopyBufferToImage( + commandBuffer, + buffer, + image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, + ®ion +); +``` + +The fourth parameter indicates which layout the image is currently using. I'm +assuming here that the image has already been transitioned to the layout that is +optimal for copying pixels to. Right now we're only copying one chunk of pixels +to the whole image, but it's possible to specify an array of `VkBufferImageCopy` +to perform many different copies from this buffer to the image in one operation. + +## Preparing the texture image + +We now have all of the tools we need to finish setting up the texture image, so +we're going back to the `createTextureImage` function. The last thing we did +there was creating the texture image. The next step is to copy the staging +buffer to the texture image. This involves two steps: + +* Transition the texture image to `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL` +* Execute the buffer to image copy operation + +This is easy to do with the functions we just created: + +```c++ +transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); +copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast(texHeight)); +``` + +The image was created with the `VK_IMAGE_LAYOUT_UNDEFINED` layout, so that one +should be specified as old layout when transitioning `textureImage`. Remember +that we can do this because we don't care about its contents before performing +the copy operation. + +To be able to start sampling from the texture image in the shader, we need one +last transition to prepare it for shader access: + +```c++ +transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); +``` + +## Transition barrier masks + +If you run your application with validation layers enabled now, then you'll see that +it complains about the access masks and pipeline stages in +`transitionImageLayout` being invalid. We still need to set those based on the +layouts in the transition. + +There are two transitions we need to handle: + +* Undefined → transfer destination: transfer writes that don't need to wait on +anything +* Transfer destination → shader reading: shader reads should wait on transfer +writes, specifically the shader reads in the fragment shader, because that's +where we're going to use the texture + +These rules are specified using the following access masks and pipeline stages: + +```c++ +VkPipelineStageFlags sourceStage; +VkPipelineStageFlags destinationStage; + +if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) { + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + + sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; +} else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) { + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; +} else { + throw std::invalid_argument("unsupported layout transition!"); +} + +vkCmdPipelineBarrier( + commandBuffer, + sourceStage, destinationStage, + 0, + 0, nullptr, + 0, nullptr, + 1, &barrier +); +``` + +As you can see in the aforementioned table, transfer writes must occur in the +pipeline transfer stage. Since the writes don't have to wait on anything, you +may specify an empty access mask and the earliest possible pipeline stage +`VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT` for the pre-barrier operations. It should be +noted that `VK_PIPELINE_STAGE_TRANSFER_BIT` is not a *real* stage within the +graphics and compute pipelines. It is more of a pseudo-stage where transfers +happen. See [the documentation](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#VkPipelineStageFlagBits) +for more information and other examples of pseudo-stages. + +The image will be written in the same pipeline stage and subsequently read by +the fragment shader, which is why we specify shader reading access in the +fragment shader pipeline stage. + +If we need to do more transitions in the future, then we'll extend the function. +The application should now run successfully, although there are of course no +visual changes yet. + +One thing to note is that command buffer submission results in implicit +`VK_ACCESS_HOST_WRITE_BIT` synchronization at the beginning. Since the +`transitionImageLayout` function executes a command buffer with only a single +command, you could use this implicit synchronization and set `srcAccessMask` to +`0` if you ever needed a `VK_ACCESS_HOST_WRITE_BIT` dependency in a layout +transition. It's up to you if you want to be explicit about it or not, but I'm +personally not a fan of relying on these OpenGL-like "hidden" operations. + +There is actually a special type of image layout that supports all operations, +`VK_IMAGE_LAYOUT_GENERAL`. The problem with it, of course, is that it doesn't +necessarily offer the best performance for any operation. It is required for +some special cases, like using an image as both input and output, or for reading +an image after it has left the preinitialized layout. + +All of the helper functions that submit commands so far have been set up to +execute synchronously by waiting for the queue to become idle. For practical +applications it is recommended to combine these operations in a single command +buffer and execute them asynchronously for higher throughput, especially the +transitions and copy in the `createTextureImage` function. Try to experiment +with this by creating a `setupCommandBuffer` that the helper functions record +commands into, and add a `flushSetupCommands` to execute the commands that have +been recorded so far. It's best to do this after the texture mapping works to +check if the texture resources are still set up correctly. + +## Cleanup + +Finish the `createTextureImage` function by cleaning up the staging buffer and +its memory at the end: + +```c++ + transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingBufferMemory, nullptr); +} +``` + +The main texture image is used until the end of the program: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyImage(device, textureImage, nullptr); + vkFreeMemory(device, textureImageMemory, nullptr); + + ... +} +``` + +The image now contains the texture, but we still need a way to access it from +the graphics pipeline. We'll work on that in the next chapter. + +[C++ code](/code/24_texture_image.cpp) / +[Vertex shader](/code/22_shader_ubo.vert) / +[Fragment shader](/code/22_shader_ubo.frag) diff --git a/ko-rust/06_Texture_mapping/01_Image_view_and_sampler.md b/ko-rust/06_Texture_mapping/01_Image_view_and_sampler.md new file mode 100644 index 00000000..9d98c9e4 --- /dev/null +++ b/ko-rust/06_Texture_mapping/01_Image_view_and_sampler.md @@ -0,0 +1,369 @@ +In this chapter we're going to create two more resources that are needed for the +graphics pipeline to sample an image. The first resource is one that we've +already seen before while working with the swap chain images, but the second one +is new - it relates to how the shader will read texels from the image. + +## Texture image view + +We've seen before, with the swap chain images and the framebuffer, that images +are accessed through image views rather than directly. We will also need to +create such an image view for the texture image. + +Add a class member to hold a `VkImageView` for the texture image and create a +new function `createTextureImageView` where we'll create it: + +```c++ +VkImageView textureImageView; + +... + +void initVulkan() { + ... + createTextureImage(); + createTextureImageView(); + createVertexBuffer(); + ... +} + +... + +void createTextureImageView() { + +} +``` + +The code for this function can be based directly on `createImageViews`. The only +two changes you have to make are the `format` and the `image`: + +```c++ +VkImageViewCreateInfo viewInfo{}; +viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; +viewInfo.image = textureImage; +viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; +viewInfo.format = VK_FORMAT_R8G8B8A8_SRGB; +viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +viewInfo.subresourceRange.baseMipLevel = 0; +viewInfo.subresourceRange.levelCount = 1; +viewInfo.subresourceRange.baseArrayLayer = 0; +viewInfo.subresourceRange.layerCount = 1; +``` + +I've left out the explicit `viewInfo.components` initialization, because +`VK_COMPONENT_SWIZZLE_IDENTITY` is defined as `0` anyway. Finish creating the +image view by calling `vkCreateImageView`: + +```c++ +if (vkCreateImageView(device, &viewInfo, nullptr, &textureImageView) != VK_SUCCESS) { + throw std::runtime_error("failed to create texture image view!"); +} +``` + +Because so much of the logic is duplicated from `createImageViews`, you may wish +to abstract it into a new `createImageView` function: + +```c++ +VkImageView createImageView(VkImage image, VkFormat format) { + VkImageViewCreateInfo viewInfo{}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + viewInfo.subresourceRange.baseMipLevel = 0; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.baseArrayLayer = 0; + viewInfo.subresourceRange.layerCount = 1; + + VkImageView imageView; + if (vkCreateImageView(device, &viewInfo, nullptr, &imageView) != VK_SUCCESS) { + throw std::runtime_error("failed to create image view!"); + } + + return imageView; +} +``` + +The `createTextureImageView` function can now be simplified to: + +```c++ +void createTextureImageView() { + textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_SRGB); +} +``` + +And `createImageViews` can be simplified to: + +```c++ +void createImageViews() { + swapChainImageViews.resize(swapChainImages.size()); + + for (uint32_t i = 0; i < swapChainImages.size(); i++) { + swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat); + } +} +``` + +Make sure to destroy the image view at the end of the program, right before +destroying the image itself: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyImageView(device, textureImageView, nullptr); + + vkDestroyImage(device, textureImage, nullptr); + vkFreeMemory(device, textureImageMemory, nullptr); +``` + +## Samplers + +It is possible for shaders to read texels directly from images, but that is not +very common when they are used as textures. Textures are usually accessed +through samplers, which will apply filtering and transformations to compute the +final color that is retrieved. + +These filters are helpful to deal with problems like oversampling. Consider a +texture that is mapped to geometry with more fragments than texels. If you +simply took the closest texel for the texture coordinate in each fragment, then +you would get a result like the first image: + +![](/images/texture_filtering.png) + +If you combined the 4 closest texels through linear interpolation, then you +would get a smoother result like the one on the right. Of course your +application may have art style requirements that fit the left style more (think +Minecraft), but the right is preferred in conventional graphics applications. A +sampler object automatically applies this filtering for you when reading a color +from the texture. + +Undersampling is the opposite problem, where you have more texels than +fragments. This will lead to artifacts when sampling high frequency patterns +like a checkerboard texture at a sharp angle: + +![](/images/anisotropic_filtering.png) + +As shown in the left image, the texture turns into a blurry mess in the +distance. The solution to this is [anisotropic filtering](https://en.wikipedia.org/wiki/Anisotropic_filtering), +which can also be applied automatically by a sampler. + +Aside from these filters, a sampler can also take care of transformations. It +determines what happens when you try to read texels outside the image through +its *addressing mode*. The image below displays some of the possibilities: + +![](/images/texture_addressing.png) + +We will now create a function `createTextureSampler` to set up such a sampler +object. We'll be using that sampler to read colors from the texture in the +shader later on. + +```c++ +void initVulkan() { + ... + createTextureImage(); + createTextureImageView(); + createTextureSampler(); + ... +} + +... + +void createTextureSampler() { + +} +``` + +Samplers are configured through a `VkSamplerCreateInfo` structure, which +specifies all filters and transformations that it should apply. + +```c++ +VkSamplerCreateInfo samplerInfo{}; +samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; +samplerInfo.magFilter = VK_FILTER_LINEAR; +samplerInfo.minFilter = VK_FILTER_LINEAR; +``` + +The `magFilter` and `minFilter` fields specify how to interpolate texels that +are magnified or minified. Magnification concerns the oversampling problem +describes above, and minification concerns undersampling. The choices are +`VK_FILTER_NEAREST` and `VK_FILTER_LINEAR`, corresponding to the modes +demonstrated in the images above. + +```c++ +samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; +samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; +samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; +``` + +The addressing mode can be specified per axis using the `addressMode` fields. +The available values are listed below. Most of these are demonstrated in the +image above. Note that the axes are called U, V and W instead of X, Y and Z. +This is a convention for texture space coordinates. + +* `VK_SAMPLER_ADDRESS_MODE_REPEAT`: Repeat the texture when going beyond the +image dimensions. +* `VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT`: Like repeat, but inverts the +coordinates to mirror the image when going beyond the dimensions. +* `VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE`: Take the color of the edge closest to +the coordinate beyond the image dimensions. +* `VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE`: Like clamp to edge, but +instead uses the edge opposite to the closest edge. +* `VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER`: Return a solid color when sampling +beyond the dimensions of the image. + +It doesn't really matter which addressing mode we use here, because we're not +going to sample outside of the image in this tutorial. However, the repeat mode +is probably the most common mode, because it can be used to tile textures like +floors and walls. + +```c++ +samplerInfo.anisotropyEnable = VK_TRUE; +samplerInfo.maxAnisotropy = ???; +``` + +These two fields specify if anisotropic filtering should be used. There is no +reason not to use this unless performance is a concern. The `maxAnisotropy` +field limits the amount of texel samples that can be used to calculate the final +color. A lower value results in better performance, but lower quality results. +To figure out which value we can use, we need to retrieve the properties of the physical device like so: + +```c++ +VkPhysicalDeviceProperties properties{}; +vkGetPhysicalDeviceProperties(physicalDevice, &properties); +``` + +If you look at the documentation for the `VkPhysicalDeviceProperties` structure, you'll see that it contains a `VkPhysicalDeviceLimits` member named `limits`. This struct in turn has a member called `maxSamplerAnisotropy` and this is the maximum value we can specify for `maxAnisotropy`. If we want to go for maximum quality, we can simply use that value directly: + +```c++ +samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy; +``` + +You can either query the properties at the beginning of your program and pass them around to the functions that need them, or query them in the `createTextureSampler` function itself. + +```c++ +samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; +``` + +The `borderColor` field specifies which color is returned when sampling beyond +the image with clamp to border addressing mode. It is possible to return black, +white or transparent in either float or int formats. You cannot specify an +arbitrary color. + +```c++ +samplerInfo.unnormalizedCoordinates = VK_FALSE; +``` + +The `unnormalizedCoordinates` field specifies which coordinate system you want +to use to address texels in an image. If this field is `VK_TRUE`, then you can +simply use coordinates within the `[0, texWidth)` and `[0, texHeight)` range. If +it is `VK_FALSE`, then the texels are addressed using the `[0, 1)` range on all +axes. Real-world applications almost always use normalized coordinates, because +then it's possible to use textures of varying resolutions with the exact same +coordinates. + +```c++ +samplerInfo.compareEnable = VK_FALSE; +samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS; +``` + +If a comparison function is enabled, then texels will first be compared to a +value, and the result of that comparison is used in filtering operations. This +is mainly used for [percentage-closer filtering](https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch11.html) +on shadow maps. We'll look at this in a future chapter. + +```c++ +samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; +samplerInfo.mipLodBias = 0.0f; +samplerInfo.minLod = 0.0f; +samplerInfo.maxLod = 0.0f; +``` + +All of these fields apply to mipmapping. We will look at mipmapping in a [later +chapter](/Generating_Mipmaps), but basically it's another type of filter that can be applied. + +The functioning of the sampler is now fully defined. Add a class member to +hold the handle of the sampler object and create the sampler with +`vkCreateSampler`: + +```c++ +VkImageView textureImageView; +VkSampler textureSampler; + +... + +void createTextureSampler() { + ... + + if (vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler) != VK_SUCCESS) { + throw std::runtime_error("failed to create texture sampler!"); + } +} +``` + +Note the sampler does not reference a `VkImage` anywhere. The sampler is a +distinct object that provides an interface to extract colors from a texture. It +can be applied to any image you want, whether it is 1D, 2D or 3D. This is +different from many older APIs, which combined texture images and filtering into +a single state. + +Destroy the sampler at the end of the program when we'll no longer be accessing +the image: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroySampler(device, textureSampler, nullptr); + vkDestroyImageView(device, textureImageView, nullptr); + + ... +} +``` + +## Anisotropy device feature + +If you run your program right now, you'll see a validation layer message like +this: + +![](/images/validation_layer_anisotropy.png) + +That's because anisotropic filtering is actually an optional device feature. We +need to update the `createLogicalDevice` function to request it: + +```c++ +VkPhysicalDeviceFeatures deviceFeatures{}; +deviceFeatures.samplerAnisotropy = VK_TRUE; +``` + +And even though it is very unlikely that a modern graphics card will not support +it, we should update `isDeviceSuitable` to check if it is available: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + ... + + VkPhysicalDeviceFeatures supportedFeatures; + vkGetPhysicalDeviceFeatures(device, &supportedFeatures); + + return indices.isComplete() && extensionsSupported && swapChainAdequate && supportedFeatures.samplerAnisotropy; +} +``` + +The `vkGetPhysicalDeviceFeatures` repurposes the `VkPhysicalDeviceFeatures` +struct to indicate which features are supported rather than requested by setting +the boolean values. + +Instead of enforcing the availability of anisotropic filtering, it's also +possible to simply not use it by conditionally setting: + +```c++ +samplerInfo.anisotropyEnable = VK_FALSE; +samplerInfo.maxAnisotropy = 1.0f; +``` + +In the next chapter we will expose the image and sampler objects to the shaders +to draw the texture onto the square. + +[C++ code](/code/25_sampler.cpp) / +[Vertex shader](/code/22_shader_ubo.vert) / +[Fragment shader](/code/22_shader_ubo.frag) diff --git a/ko-rust/06_Texture_mapping/02_Combined_image_sampler.md b/ko-rust/06_Texture_mapping/02_Combined_image_sampler.md new file mode 100644 index 00000000..0f1e5496 --- /dev/null +++ b/ko-rust/06_Texture_mapping/02_Combined_image_sampler.md @@ -0,0 +1,296 @@ +## Introduction + +We looked at descriptors for the first time in the uniform buffers part of the +tutorial. In this chapter we will look at a new type of descriptor: *combined +image sampler*. This descriptor makes it possible for shaders to access an image +resource through a sampler object like the one we created in the previous +chapter. + +We'll start by modifying the descriptor set layout, descriptor pool and descriptor +set to include such a combined image sampler descriptor. After that, we're going +to add texture coordinates to `Vertex` and modify the fragment shader to read +colors from the texture instead of just interpolating the vertex colors. + +## Updating the descriptors + +Browse to the `createDescriptorSetLayout` function and add a +`VkDescriptorSetLayoutBinding` for a combined image sampler descriptor. We'll +simply put it in the binding after the uniform buffer: + +```c++ +VkDescriptorSetLayoutBinding samplerLayoutBinding{}; +samplerLayoutBinding.binding = 1; +samplerLayoutBinding.descriptorCount = 1; +samplerLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; +samplerLayoutBinding.pImmutableSamplers = nullptr; +samplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + +std::array bindings = {uboLayoutBinding, samplerLayoutBinding}; +VkDescriptorSetLayoutCreateInfo layoutInfo{}; +layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; +layoutInfo.bindingCount = static_cast(bindings.size()); +layoutInfo.pBindings = bindings.data(); +``` + +Make sure to set the `stageFlags` to indicate that we intend to use the combined +image sampler descriptor in the fragment shader. That's where the color of the +fragment is going to be determined. It is possible to use texture sampling in +the vertex shader, for example to dynamically deform a grid of vertices by a +[heightmap](https://en.wikipedia.org/wiki/Heightmap). + +We must also create a larger descriptor pool to make room for the allocation +of the combined image sampler by adding another `VkPoolSize` of type +`VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER` to the +`VkDescriptorPoolCreateInfo`. Go to the `createDescriptorPool` function and +modify it to include a `VkDescriptorPoolSize` for this descriptor: + +```c++ +std::array poolSizes{}; +poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +poolSizes[0].descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); +poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; +poolSizes[1].descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); + +VkDescriptorPoolCreateInfo poolInfo{}; +poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; +poolInfo.poolSizeCount = static_cast(poolSizes.size()); +poolInfo.pPoolSizes = poolSizes.data(); +poolInfo.maxSets = static_cast(MAX_FRAMES_IN_FLIGHT); +``` + +Inadequate descriptor pools are a good example of a problem that the validation +layers will not catch: As of Vulkan 1.1, `vkAllocateDescriptorSets` may fail +with the error code `VK_ERROR_POOL_OUT_OF_MEMORY` if the pool is not +sufficiently large, but the driver may also try to solve the problem internally. +This means that sometimes (depending on hardware, pool size and allocation size) +the driver will let us get away with an allocation that exceeds the limits of +our descriptor pool. Other times, `vkAllocateDescriptorSets` will fail and +return `VK_ERROR_POOL_OUT_OF_MEMORY`. This can be particularly frustrating if +the allocation succeeds on some machines, but fails on others. + +Since Vulkan shifts the responsiblity for the allocation to the driver, it is no +longer a strict requirement to only allocate as many descriptors of a certain +type (`VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER`, etc.) as specified by the +corresponding `descriptorCount` members for the creation of the descriptor pool. +However, it remains best practise to do so, and in the future, +`VK_LAYER_KHRONOS_validation` will warn about this type of problem if you enable +[Best Practice Validation](https://vulkan.lunarg.com/doc/view/1.4.304.0/linux/best_practices.html). + +The final step is to bind the actual image and sampler resources to the +descriptors in the descriptor set. Go to the `createDescriptorSets` function. + +```c++ +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + VkDescriptorBufferInfo bufferInfo{}; + bufferInfo.buffer = uniformBuffers[i]; + bufferInfo.offset = 0; + bufferInfo.range = sizeof(UniformBufferObject); + + VkDescriptorImageInfo imageInfo{}; + imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imageInfo.imageView = textureImageView; + imageInfo.sampler = textureSampler; + + ... +} +``` + +The resources for a combined image sampler structure must be specified in a +`VkDescriptorImageInfo` struct, just like the buffer resource for a uniform +buffer descriptor is specified in a `VkDescriptorBufferInfo` struct. This is +where the objects from the previous chapter come together. + +```c++ +std::array descriptorWrites{}; + +descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; +descriptorWrites[0].dstSet = descriptorSets[i]; +descriptorWrites[0].dstBinding = 0; +descriptorWrites[0].dstArrayElement = 0; +descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +descriptorWrites[0].descriptorCount = 1; +descriptorWrites[0].pBufferInfo = &bufferInfo; + +descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; +descriptorWrites[1].dstSet = descriptorSets[i]; +descriptorWrites[1].dstBinding = 1; +descriptorWrites[1].dstArrayElement = 0; +descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; +descriptorWrites[1].descriptorCount = 1; +descriptorWrites[1].pImageInfo = &imageInfo; + +vkUpdateDescriptorSets(device, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); +``` + +The descriptors must be updated with this image info, just like the buffer. This +time we're using the `pImageInfo` array instead of `pBufferInfo`. The descriptors +are now ready to be used by the shaders! + +## Texture coordinates + +There is one important ingredient for texture mapping that is still missing, and +that's the actual texture coordinates for each vertex. The texture coordinates determine how the +image is actually mapped to the geometry. + +```c++ +struct Vertex { + glm::vec2 pos; + glm::vec3 color; + glm::vec2 texCoord; + + static VkVertexInputBindingDescription getBindingDescription() { + VkVertexInputBindingDescription bindingDescription{}; + bindingDescription.binding = 0; + bindingDescription.stride = sizeof(Vertex); + bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + return bindingDescription; + } + + static std::array getAttributeDescriptions() { + std::array attributeDescriptions{}; + + attributeDescriptions[0].binding = 0; + attributeDescriptions[0].location = 0; + attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; + attributeDescriptions[0].offset = offsetof(Vertex, pos); + + attributeDescriptions[1].binding = 0; + attributeDescriptions[1].location = 1; + attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; + attributeDescriptions[1].offset = offsetof(Vertex, color); + + attributeDescriptions[2].binding = 0; + attributeDescriptions[2].location = 2; + attributeDescriptions[2].format = VK_FORMAT_R32G32_SFLOAT; + attributeDescriptions[2].offset = offsetof(Vertex, texCoord); + + return attributeDescriptions; + } +}; +``` + +Modify the `Vertex` struct to include a `vec2` for texture coordinates. Make +sure to also add a `VkVertexInputAttributeDescription` so that we can use access +texture coordinates as input in the vertex shader. That is necessary to be able +to pass them to the fragment shader for interpolation across the surface of the +square. + +```c++ +const std::vector vertices = { + {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}}, + {{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}}, + {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}}, + {{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}} +}; +``` + +In this tutorial, I will simply fill the square with the texture by using +coordinates from `0, 0` in the top-left corner to `1, 1` in the bottom-right +corner. Feel free to experiment with different coordinates. Try using +coordinates below `0` or above `1` to see the addressing modes in action! + +## Shaders + +The final step is modifying the shaders to sample colors from the texture. We +first need to modify the vertex shader to pass through the texture coordinates +to the fragment shader: + +```glsl +layout(location = 0) in vec2 inPosition; +layout(location = 1) in vec3 inColor; +layout(location = 2) in vec2 inTexCoord; + +layout(location = 0) out vec3 fragColor; +layout(location = 1) out vec2 fragTexCoord; + +void main() { + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0); + fragColor = inColor; + fragTexCoord = inTexCoord; +} +``` + +Just like the per vertex colors, the `fragTexCoord` values will be smoothly +interpolated across the area of the square by the rasterizer. We can visualize +this by having the fragment shader output the texture coordinates as colors: + +```glsl +#version 450 + +layout(location = 0) in vec3 fragColor; +layout(location = 1) in vec2 fragTexCoord; + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(fragTexCoord, 0.0, 1.0); +} +``` + +You should see something like the image below. Don't forget to recompile the +shaders! + +![](/images/texcoord_visualization.png) + +The green channel represents the horizontal coordinates and the red channel the +vertical coordinates. The black and yellow corners confirm that the texture +coordinates are correctly interpolated from `0, 0` to `1, 1` across the square. +Visualizing data using colors is the shader programming equivalent of `printf` +debugging, for lack of a better option! + +A combined image sampler descriptor is represented in GLSL by a sampler uniform. +Add a reference to it in the fragment shader: + +```glsl +layout(binding = 1) uniform sampler2D texSampler; +``` + +There are equivalent `sampler1D` and `sampler3D` types for other types of +images. Make sure to use the correct binding here. + +```glsl +void main() { + outColor = texture(texSampler, fragTexCoord); +} +``` + +Textures are sampled using the built-in `texture` function. It takes a `sampler` +and coordinate as arguments. The sampler automatically takes care of the +filtering and transformations in the background. You should now see the texture +on the square when you run the application: + +![](/images/texture_on_square.png) + +Try experimenting with the addressing modes by scaling the texture coordinates +to values higher than `1`. For example, the following fragment shader produces +the result in the image below when using `VK_SAMPLER_ADDRESS_MODE_REPEAT`: + +```glsl +void main() { + outColor = texture(texSampler, fragTexCoord * 2.0); +} +``` + +![](/images/texture_on_square_repeated.png) + +You can also manipulate the texture colors using the vertex colors: + +```glsl +void main() { + outColor = vec4(fragColor * texture(texSampler, fragTexCoord).rgb, 1.0); +} +``` + +I've separated the RGB and alpha channels here to not scale the alpha channel. + +![](/images/texture_on_square_colorized.png) + +You now know how to access images in shaders! This is a very powerful technique +when combined with images that are also written to in framebuffers. You can use +these images as inputs to implement cool effects like post-processing and camera +displays within the 3D world. + +[C++ code](/code/26_texture_mapping.cpp) / +[Vertex shader](/code/26_shader_textures.vert) / +[Fragment shader](/code/26_shader_textures.frag) diff --git a/ko-rust/07_Depth_buffering.md b/ko-rust/07_Depth_buffering.md new file mode 100644 index 00000000..c3b4cf8e --- /dev/null +++ b/ko-rust/07_Depth_buffering.md @@ -0,0 +1,445 @@ +## 소개 + +지금까지 우리가 다루었던 지오메트리는 3D로 투영되었지만, 실제로는 완전히 평면이었습니다. 이번 장에서는 3D 메시를 준비하기 위해 위치(position)에 Z 좌표를 추가할 것입니다. 이 세 번째 좌표를 사용하여 현재 사각형 위에 또 다른 사각형을 배치함으로써, 지오메트리가 깊이 순으로 정렬되지 않았을 때 발생하는 문제를 직접 확인해 보겠습니다. + +## 3D 지오메트리 + +먼저 `Vertex` 구조체를 변경하여 위치에 3D 벡터를 사용하고, 그에 맞춰 `get_attribute_descriptions`의 `format`을 업데이트합니다. Rust에서는 `glam` 크레이트의 `Vec3`를 사용합니다. + +```rust +#[repr(C)] +#[derive(Clone, Debug, Copy)] +struct Vertex { + pos: glam::Vec3, + color: glam::Vec3, + tex_coord: glam::Vec2, +} + +impl Vertex { + // ... get_binding_description ... + + fn get_attribute_descriptions() -> [vk::VertexInputAttributeDescription; 3] { + [ + vk::VertexInputAttributeDescription { + binding: 0, + location: 0, + format: vk::Format::R32G32B32_SFLOAT, + offset: memoffset::offset_of!(Vertex, pos) as u32, + }, + // ... color and tex_coord attributes ... + ] + } +} +``` + +다음으로, 정점 셰이더가 3D 좌표를 입력으로 받아 변환하도록 수정합니다. 이 코드는 C++ 버전과 동일합니다. 수정 후에는 반드시 셰이더를 다시 컴파일해야 합니다! + +```glsl +layout(location = 0) in vec3 inPosition; + +... + +void main() { + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0); + fragColor = inColor; + fragTexCoord = inTexCoord; +} +``` + +마지막으로, `VERTICES` 상수를 Z 좌표를 포함하도록 업데이트합니다. + +```rust +const VERTICES: [Vertex; 4] = [ + Vertex { pos: glam::vec3(-0.5, -0.5, 0.0), color: glam::vec3(1.0, 0.0, 0.0), tex_coord: glam::vec2(0.0, 0.0) }, + Vertex { pos: glam::vec3(0.5, -0.5, 0.0), color: glam::vec3(0.0, 1.0, 0.0), tex_coord: glam::vec2(1.0, 0.0) }, + Vertex { pos: glam::vec3(0.5, 0.5, 0.0), color: glam::vec3(0.0, 0.0, 1.0), tex_coord: glam::vec2(1.0, 1.0) }, + Vertex { pos: glam::vec3(-0.5, 0.5, 0.0), color: glam::vec3(1.0, 1.0, 1.0), tex_coord: glam::vec2(0.0, 1.0) }, +]; +``` + +지금 애플리케이션을 실행하면 이전과 완전히 동일한 결과를 볼 수 있습니다. 이제 장면을 더 흥미롭게 만들고 이번 장에서 다룰 문제를 보여주기 위해 지오메트리를 추가할 시간입니다. 현재 사각형 바로 아래에 위치할 사각형을 정의하기 위해 정점들을 복제합니다. + +![](/images/extra_square.svg) + +새 사각형의 Z 좌표는 `-0.5f`로 설정하고, 추가된 사각형에 대한 인덱스도 추가합니다. + +```rust +const VERTICES: [Vertex; 8] = [ + Vertex { pos: glam::vec3(-0.5, -0.5, 0.0), color: glam::vec3(1.0, 0.0, 0.0), tex_coord: glam::vec2(0.0, 0.0) }, + Vertex { pos: glam::vec3(0.5, -0.5, 0.0), color: glam::vec3(0.0, 1.0, 0.0), tex_coord: glam::vec2(1.0, 0.0) }, + Vertex { pos: glam::vec3(0.5, 0.5, 0.0), color: glam::vec3(0.0, 0.0, 1.0), tex_coord: glam::vec2(1.0, 1.0) }, + Vertex { pos: glam::vec3(-0.5, 0.5, 0.0), color: glam::vec3(1.0, 1.0, 1.0), tex_coord: glam::vec2(0.0, 1.0) }, + + Vertex { pos: glam::vec3(-0.5, -0.5, -0.5), color: glam::vec3(1.0, 0.0, 0.0), tex_coord: glam::vec2(0.0, 0.0) }, + Vertex { pos: glam::vec3(0.5, -0.5, -0.5), color: glam::vec3(0.0, 1.0, 0.0), tex_coord: glam::vec2(1.0, 0.0) }, + Vertex { pos: glam::vec3(0.5, 0.5, -0.5), color: glam::vec3(0.0, 0.0, 1.0), tex_coord: glam::vec2(1.0, 1.0) }, + Vertex { pos: glam::vec3(-0.5, 0.5, -0.5), color: glam::vec3(1.0, 1.0, 1.0), tex_coord: glam::vec2(0.0, 1.0) }, +]; + +const INDICES: [u16; 12] = [ + 0, 1, 2, 2, 3, 0, + 4, 5, 6, 6, 7, 4, +]; +``` + +이제 프로그램을 실행하면 마치 에셔(Escher)의 그림과 같은 이상한 결과물을 보게 될 것입니다. + +![](/images/depth_issues.png) + +문제의 원인과 해결책(깊이 정렬 또는 깊이 버퍼 사용)은 C++ 버전과 동일합니다. 우리는 **깊이 버퍼**를 사용하여 이 문제를 해결할 것입니다. + +C++에서 `GLM_FORCE_DEPTH_ZERO_TO_ONE` 매크로를 정의했던 것과 달리, `glam`에서는 `perspective_rh_zo` (Right-Handed, Zero-to-One) 함수를 사용하여 Vulkan의 `0.0`에서 `1.0` 깊이 범위를 사용하는 원근 투영 행렬을 직접 생성할 수 있습니다. `update_uniform_buffer`에서 이 함수를 사용하도록 수정해야 합니다. + +```rust +// in update_uniform_buffer +let proj = glam::Mat4::perspective_rh_zo( + 45.0f32.to_radians(), + self.swapchain_extent.width as f32 / self.swapchain_extent.height as f32, + 0.1, + 10.0, +); +``` + +## 깊이 이미지와 이미지 뷰 + +깊이 첨부는 이미지, 메모리, 이미지 뷰 세 가지 리소스가 필요합니다. 애플리케이션 구조체에 관련 필드를 추가합니다. + +```rust +struct HelloTriangleApplication { + // ... + depth_image: vk::Image, + depth_image_memory: vk::DeviceMemory, + depth_image_view: vk::ImageView, +} +``` + +이 리소스들을 설정하기 위해 `create_depth_resources`라는 새 메서드를 만듭니다. + +```rust +// in init_vulkan +// ... +self.create_command_pool(); +self.create_depth_resources(); // 텍스처 이미지 생성 전에 호출 +self.create_texture_image(); +// ... + +// ... + +impl HelloTriangleApplication { + // ... + fn create_depth_resources(&mut self) { + // ... + } +} +``` + +깊이 이미지 포맷을 찾기 위해, C++ 버전과 유사한 `find_supported_format` 헬퍼 함수를 만듭니다. + +```rust +fn find_supported_format( + instance: &ash::Instance, + physical_device: vk::PhysicalDevice, + candidates: &[vk::Format], + tiling: vk::ImageTiling, + features: vk::FormatFeatureFlags, +) -> vk::Format { + candidates.iter().cloned().find(|format| { + let props = unsafe { + instance.get_physical_device_format_properties(physical_device, *format) + }; + if tiling == vk::ImageTiling::LINEAR { + props.linear_tiling_features.contains(features) + } else { // tiling == vk::ImageTiling::OPTIMAL + props.optimal_tiling_features.contains(features) + } + }).expect("failed to find supported format!") +} +``` + +이 함수를 사용하여 깊이 포맷을 찾는 `find_depth_format` 함수를 만듭니다. + +```rust +fn find_depth_format( + instance: &ash::Instance, + physical_device: vk::PhysicalDevice, +) -> vk::Format { + find_supported_format( + instance, + physical_device, + &[ + vk::Format::D32_SFLOAT, + vk::Format::D32_SFLOAT_S8_UINT, + vk::Format::D24_UNORM_S8_UINT, + ], + vk::ImageTiling::OPTIMAL, + vk::FormatFeatureFlags::DEPTH_STENCIL_ATTACHMENT, + ) +} + +fn has_stencil_component(format: vk::Format) -> bool { + format == vk::Format::D32_SFLOAT_S8_UINT || format == vk::Format::D24_UNORM_S8_UINT +} +``` + +이제 `create_depth_resources` 메서드 내부를 채웁니다. + +```rust +fn create_depth_resources(&mut self) { + let depth_format = find_depth_format(&self.instance, self.physical_device); + + let (depth_image, depth_image_memory) = self.create_image( + self.swapchain_extent.width, + self.swapchain_extent.height, + depth_format, + vk::ImageTiling::OPTIMAL, + vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + ); + self.depth_image = depth_image; + self.depth_image_memory = depth_image_memory; + + self.depth_image_view = self.create_image_view( + self.depth_image, + depth_format, + vk::ImageAspectFlags::DEPTH, + ); + + // 레이아웃 전환은 선택사항이며 렌더 패스에서 처리됩니다. + // 명시적으로 전환하려면 아래 코드를 사용합니다. + // self.transition_image_layout( + // self.depth_image, + // depth_format, + // vk::ImageLayout::UNDEFINED, + // vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + // ); +} +``` + +`create_image_view` 함수가 `aspect_flags`를 인자로 받도록 수정해야 합니다. + +```rust +fn create_image_view( + &self, + image: vk::Image, + format: vk::Format, + aspect_flags: vk::ImageAspectFlags, +) -> vk::ImageView { + let view_info = vk::ImageViewCreateInfo::builder() + .image(image) + .view_type(vk::ImageViewType::TYPE_2D) + .format(format) + .subresource_range( + vk::ImageSubresourceRange::builder() + .aspect_mask(aspect_flags) + .base_mip_level(0) + .level_count(1) + .base_array_layer(0) + .layer_count(1) + .build(), + ); + + unsafe { + self.device + .create_image_view(&view_info, None) + .expect("Failed to create image view!") + } +} +``` + +모든 `create_image_view` 호출을 새로운 시그니처에 맞게 업데이트해야 합니다. + +```rust +// in create_image_views +self.swapchain_image_views[i] = self.create_image_view( + self.swapchain_images[i], + self.swapchain_image_format, + vk::ImageAspectFlags::COLOR, +); + +// in create_texture_image +self.texture_image_view = self.create_image_view( + self.texture_image, + vk::Format::R8G8B8A8_SRGB, + vk::ImageAspectFlags::COLOR, +); +``` + +## 렌더 패스 + +`create_render_pass` 메서드를 수정하여 깊이 첨부를 포함하도록 합니다. 먼저 `VkAttachmentDescription`을 추가합니다. `ash`의 빌더 패턴을 사용합니다. + +```rust +// in create_render_pass +let depth_format = find_depth_format(&self.instance, self.physical_device); +let depth_attachment = vk::AttachmentDescription::builder() + .format(depth_format) + .samples(vk::SampleCountFlags::TYPE_1) + .load_op(vk::AttachmentLoadOp::CLEAR) + .store_op(vk::AttachmentStoreOp::DONT_CARE) + .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE) + .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE) + .initial_layout(vk::ImageLayout::UNDEFINED) + .final_layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL); +``` + +깊이 첨부에 대한 참조를 추가합니다. + +```rust +let depth_attachment_ref = vk::AttachmentReference::builder() + .attachment(1) + .layout(vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL); +``` + +서브패스 설명(`SubpassDescription`)을 업데이트하여 깊이-스텐실 첨부를 참조하도록 합니다. + +```rust +let subpass = vk::SubpassDescription::builder() + .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS) + .color_attachments(std::slice::from_ref(&color_attachment_ref)) + .depth_stencil_attachment(&depth_attachment_ref); +``` + +렌더 패스 생성 정보에 두 첨부를 모두 포함합니다. + +```rust +let attachments = [color_attachment.build(), depth_attachment.build()]; + +let dependency = vk::SubpassDependency::builder() + .src_subpass(vk::SUBPASS_EXTERNAL) + .dst_subpass(0) + .src_stage_mask( + vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT + | vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS, + ) + .src_access_mask(vk::AccessFlags::empty()) + .dst_stage_mask( + vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT + | vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS, + ) + .dst_access_mask( + vk::AccessFlags::COLOR_ATTACHMENT_WRITE + | vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE, + ); + +let render_pass_info = vk::RenderPassCreateInfo::builder() + .attachments(&attachments) + .subpasses(std::slice::from_ref(&subpass)) + .dependencies(std::slice::from_ref(&dependency)); +``` +`SubpassDependency`는 렌더 패스가 시작되기 전에 이미지(색상 및 깊이)가 준비되도록 보장합니다. 깊이 버퍼는 `LOAD_OP_CLEAR`로 인해 `EARLY_FRAGMENT_TESTS` 단계에서 쓰여지므로, `dstStageMask`와 `dstAccessMask`에 관련 플래그를 추가하여 이 쓰기 작업을 허용해야 합니다. + +## 프레임버퍼 + +`create_framebuffers` 메서드를 수정하여 깊이 이미지 뷰를 두 번째 첨부로 바인딩합니다. + +```rust +// in create_framebuffers +let attachments = [self.swapchain_image_views[i], self.depth_image_view]; + +let framebuffer_info = vk::FramebufferCreateInfo::builder() + .render_pass(self.render_pass) + .attachments(&attachments) + .width(self.swapchain_extent.width) + .height(self.swapchain_extent.height) + .layers(1); +``` + +깊이 이미지 뷰가 생성된 후에 프레임버퍼가 생성되도록 `init_vulkan` 내의 호출 순서를 조정해야 합니다. + +```rust +// in init_vulkan +// ... +self.create_depth_resources(); +self.create_framebuffers(); +// ... +``` + +## 소거 값 (Clear values) + +여러 첨부를 소거하므로, `record_command_buffer`에서 여러 소거 값을 지정해야 합니다. + +```rust +// in record_command_buffer +let clear_values = [ + vk::ClearValue { + color: vk::ClearColorValue { + float32: [0.0, 0.0, 0.0, 1.0], + }, + }, + vk::ClearValue { + depth_stencil: vk::ClearDepthStencilValue { + depth: 1.0, + stencil: 0, + }, + }, +]; + +let render_pass_info = vk::RenderPassBeginInfo::builder() + .render_pass(self.render_pass) + .framebuffer(self.swapchain_framebuffers[image_index as usize]) + .render_area(render_area) + .clear_values(&clear_values); // clear_values 슬라이스 전달 +``` +`vk::ClearValue`는 Rust에서 union처럼 동작하는 구조체입니다. 색상에는 `color` 필드를, 깊이/스텐실에는 `depth_stencil` 필드를 사용합니다. 깊이의 초기 값은 가장 먼 거리인 `1.0`으로 설정합니다. `clear_values` 배열의 순서는 렌더 패스의 첨부 순서와 일치해야 합니다. + +## 깊이 및 스텐실 상태 + +파이프라인을 생성할 때 `VkPipelineDepthStencilStateCreateInfo`를 통해 깊이 테스팅을 활성화해야 합니다. + +```rust +// in create_graphics_pipeline +let depth_stencil_state = vk::PipelineDepthStencilStateCreateInfo::builder() + .depth_test_enable(true) + .depth_write_enable(true) + .depth_compare_op(vk::CompareOp::LESS) + .depth_bounds_test_enable(false) + .min_depth_bounds(0.0) // Optional + .max_depth_bounds(1.0) // Optional + .stencil_test_enable(false); + +let pipeline_info = vk::GraphicsPipelineCreateInfo::builder() + // ... + .depth_stencil_state(&depth_stencil_state) + // ... +``` + +이제 `GraphicsPipelineCreateInfo` 빌더에 깊이 스텐실 상태를 연결합니다. 렌더 패스가 깊이 첨부를 포함하면 이 상태는 항상 지정되어야 합니다. + +이제 프로그램을 실행하면, 지오메트리의 프래그먼트가 올바르게 정렬된 것을 볼 수 있습니다. + +![](/images/depth_correct.png) + +## 창 크기 조절 처리 + +창 크기가 조절될 때 깊이 버퍼도 재생성되어야 합니다. `recreate_swapchain` 메서드를 수정합니다. + +```rust +fn recreate_swapchain(&mut self) { + // ... + unsafe { + self.device.device_wait_idle().unwrap(); + } + self.cleanup_swapchain(); + + self.create_swapchain(); + self.create_image_views(); + self.create_depth_resources(); // 여기서 깊이 리소스 재생성 + self.create_framebuffers(); +} +``` + +스왑 체인 정리 함수에 깊이 리소스 정리 코드를 추가합니다. + +```rust +fn cleanup_swapchain(&mut self) { + unsafe { + self.device.destroy_image_view(self.depth_image_view, None); + self.device.destroy_image(self.depth_image, None); + self.device.free_memory(self.depth_image_memory, None); + + // ... 다른 리소스 정리 + } +} +``` + +축하합니다. 이제 여러분의 Rust 애플리케이션은 임의의 3D 지오메트리를 올바르게 렌더링할 준비가 되었습니다. 다음 장에서는 텍스처가 입혀진 모델을 그려보며 이를 시험해 보겠습니다 \ No newline at end of file diff --git a/ko-rust/08_Loading_models.md b/ko-rust/08_Loading_models.md new file mode 100644 index 00000000..ae23a036 --- /dev/null +++ b/ko-rust/08_Loading_models.md @@ -0,0 +1,232 @@ +## 소개 + +이제 여러분의 프로그램은 텍스처가 입혀진 3D 메시를 렌더링할 준비가 되었습니다. 하지만 현재 `vertices`와 `indices` 배열에 있는 지오메트리는 아직 그다지 흥미롭지 않습니다. 이번 챕터에서는 그래픽 카드가 실제로 어떤 작업을 하도록 만들기 위해, 실제 모델 파일에서 정점과 인덱스를 로드하도록 프로그램을 확장할 것입니다. + +많은 그래픽 API 튜토리얼에서는 이와 같은 챕터에서 독자에게 직접 OBJ 로더를 작성하도록 합니다. 하지만 이 방식의 문제점은, 조금이라도 흥미로운 3D 애플리케이션이라면 곧 골격 애니메이션(skeletal animation)과 같이 OBJ 파일 형식이 지원하지 않는 기능이 필요해진다는 것입니다. 이번 챕터에서 OBJ 모델로부터 메시 데이터를 로드하긴 하겠지만, 파일에서 메시 데이터를 로드하는 세부 사항보다는, 메시 데이터를 프로그램 자체에 통합하는 데 더 중점을 둘 것입니다. + +## 라이브러리 + +정점과 면(face)을 OBJ 파일에서 로드하기 위해 [tobj](https://github.com/tobj-rs/tobj) 크레이트를 사용할 것입니다. 이 라이브러리는 널리 사용되며 `Cargo`를 통해 쉽게 통합할 수 있습니다. + +`Cargo.toml` 파일을 열고 의존성 목록에 `tobj`를 추가하세요. 또한 벡터 수학을 위해 `glam` 라이브러리를 사용하고 있으며, 나중에 해싱을 위해 `Hash` 기능이 필요하므로 함께 추가합니다. + +```toml +[dependencies] +ash = "0.37" +# ... 다른 의존성들 +tobj = "4.0" +glam = { version = "0.27", features = ["hash"] } +``` + +## 샘플 메시 + +이번 챕터에서는 아직 조명을 활성화하지 않을 것이므로, 텍스처에 조명이 미리 구워진(baked) 샘플 모델을 사용하는 것이 도움이 됩니다. 이러한 모델을 찾는 쉬운 방법은 [Sketchfab](https://sketchfab.com/)에서 3D 스캔 모델을 찾아보는 것입니다. 해당 사이트의 많은 모델이 허용적인 라이선스와 함께 OBJ 형식으로 제공됩니다. + +이 튜토리얼에서는 [nigelgoh](https://sketchfab.com/nigelgoh)의 [Viking room](https://sketchfab.com/3d-models/viking-room-a49f1b8e4f5c4ecf9e1fe7d81915ad38) 모델([CC BY 4.0](https://web.archive.org/web/20200428202538/https://sketchfab.com/3d-models/viking-room-a49f1b8e4f5c4ecf9e1fe7d81915ad38))을 사용하기로 결정했습니다. 현재 지오메트리를 바로 대체하여 사용할 수 있도록 모델의 크기와 방향을 조정했습니다: + +* [viking_room.obj](/resources/viking_room.obj) +* [viking_room.png](/resources/viking_room.png) + +자신만의 모델을 자유롭게 사용해도 되지만, 해당 모델이 단 하나의 재질(material)로만 구성되어 있고 크기가 약 1.5 x 1.5 x 1.5 단위인지 확인하세요. 이보다 크면 뷰 행렬을 변경해야 합니다. 모델 파일을 `shaders`와 `textures` 옆에 새로운 `models` 디렉터리를 만들어 넣고, 텍스처 이미지는 `textures` 디렉터리에 넣으세요. + +모델과 텍스처 경로를 정의하기 위해 프로그램에 두 개의 새로운 상수를 추가하세요: + +```rust +const WINDOW_WIDTH: u32 = 800; +const WINDOW_HEIGHT: u32 = 600; + +const MODEL_PATH: &str = "models/viking_room.obj"; +const TEXTURE_PATH: &str = "textures/viking_room.png"; +``` + +그리고 `create_texture_image`가 이 경로 상수를 사용하도록 업데이트하세요: + +```rust +let image = image::open(TEXTURE_PATH) + .expect("Failed to open texture image!") + .to_rgba8(); +// ... +``` + +## 정점과 인덱스 로드하기 + +이제 모델 파일에서 정점과 인덱스를 로드할 것이므로, 하드코딩된 `VERTICES`와 `INDICES` 상수를 제거해야 합니다. 이들을 `App` 구조체의 동적 필드로 교체하세요: + +```rust +struct App { + // ... + vertices: Vec, + indices: Vec, + vertex_buffer: vk::Buffer, + vertex_buffer_memory: vk::DeviceMemory, + // ... +} +``` + +정점의 개수가 65,535개를 초과할 것이기 때문에, 인덱스의 타입을 `u16`에서 `u32`로 변경해야 합니다. `vkCmdBindIndexBuffer` 호출 시 사용하는 `vk::IndexType`도 잊지 말고 변경하세요: + +```rust +device.cmd_bind_index_buffer(command_buffer, self.index_buffer, 0, vk::IndexType::UINT32); +``` + +이제 `tobj` 크레이트를 사용하여 `vertices`와 `indices` `Vec`을 메시의 정점 데이터로 채우는 `load_model` 함수를 작성할 것입니다. 이 함수는 정점 및 인덱스 버퍼가 생성되기 전 어딘가에서 호출되어야 합니다: + +```rust +// in App::new() +// ... +self.load_model(); +self.create_vertex_buffer(); +self.create_index_buffer(); +// ... +``` + +`App`에 `load_model` 메서드를 구현합시다: + +```rust +impl App { + // ... + fn load_model(&mut self) { + let (models, _materials) = tobj::load_obj( + MODEL_PATH, + &tobj::LoadOptions { + triangulate: true, + ..Default::default() + }, + ) + .expect("Failed to load OBJ model"); + + // 우리는 모든 모델의 지오메트리를 하나로 합칠 것입니다. + for model in models { + // ... + } + } +} +``` + +`tobj::load_obj` 함수는 모델을 라이브러리의 데이터 구조로 로드합니다. 이 함수는 모델과 재질의 `Result`를 반환합니다. 우리는 간단하게 `expect`를 사용하여 오류를 처리합니다. `LoadOptions`의 `triangulate` 필드를 `true`로 설정하면 사각형이나 다각형 면을 가진 모델도 자동으로 삼각형으로 변환해줍니다. + +`tobj`가 반환하는 `Model` 구조체는 하나의 `Mesh`를 포함하고, 이 `Mesh`는 `positions`, `normals`, `texcoords`, 그리고 `indices`와 같은 `Vec`들을 가지고 있습니다. OBJ 파일의 모든 면을 단일 모델로 결합할 것이므로, 모든 `model`을 순회합니다. 이 튜토리얼에서는 가장 간단한 경우를 다루며, 파일에 하나의 모델만 있다고 가정합니다. + +```rust +// load_model 메서드 내부 +let model = &models[0]; +let mesh = &model.mesh; + +for i in 0..mesh.indices.len() { + let index = mesh.indices[i] as usize; + + let vertex = Vertex { + pos: glam::vec3( + mesh.positions[3 * index], + mesh.positions[3 * index + 1], + mesh.positions[3 * index + 2], + ), + tex_coord: glam::vec2( + mesh.texcoords[2 * index], + mesh.texcoords[2 * index + 1], + ), + color: glam::vec3(1.0, 1.0, 1.0), + }; + + self.vertices.push(vertex); + self.indices.push(i as u32); +} +``` + +`tobj`는 정점 속성을 분리된 `Vec`들로 로드합니다. `mesh.positions`는 `f32`의 `Vec`이며, 3개의 `f32`가 하나의 3D 위치를 구성합니다. `mesh.indices`는 삼각형을 구성하는 정점 인덱스를 담고 있습니다. 우리는 이 인덱스를 사용하여 `positions`와 `texcoords` `Vec`에서 올바른 속성을 조회합니다. + +단순화를 위해, 지금은 모든 정점이 고유하다고 가정하고 `0, 1, 2, ...` 순서로 인덱스를 생성합니다. + +이제 최적화 빌드로 프로그램을 실행하세요 (예: `cargo run --release`). 최적화가 없으면 모델 로딩이 매우 느려지므로 이 과정이 필요합니다. 다음과 같은 화면을 볼 수 있을 것입니다: + +![](/images/inverted_texture_coordinates.png) + +좋습니다, 지오메트리는 올바르게 보이지만 텍스처는 왜 저럴까요? OBJ 형식은 수직 좌표 `0`이 이미지의 하단을 의미하는 좌표계를 가정하지만, 우리는 이미지를 Vulkan에 업로드할 때 `0`이 상단을 의미하는 위에서 아래 방향으로 업로드했습니다. 텍스처 좌표의 수직 성분(`v`)을 뒤집어서 이 문제를 해결하세요: + +```rust +// ... +tex_coord: glam::vec2( + mesh.texcoords[2 * index], + 1.0 - mesh.texcoords[2 * index + 1], // Y 좌표 뒤집기 +), +// ... +``` + +프로그램을 다시 실행하면 이제 올바른 결과를 볼 수 있을 것입니다: + +![](/images/drawing_model.png) + +모든 노력이 마침내 이런 데모로 결실을 맺기 시작했습니다! + +> 모델이 회전할 때 뒷면(벽의 뒷부분)이 다소 이상하게 보일 수 있습니다. 이는 정상이며, 모델이 원래 그쪽에서 보도록 설계되지 않았기 때문입니다. + +## 정점 중복 제거 + +불행히도 아직 인덱스 버퍼를 제대로 활용하고 있지 않습니다. `vertices` `Vec`에는 많은 중복된 정점 데이터가 포함되어 있는데, 이는 많은 정점이 여러 삼각형에 포함되기 때문입니다. 고유한 정점만 유지하고, 이들이 나타날 때마다 인덱스 버퍼를 사용해 재사용해야 합니다. 이를 구현하는 간단한 방법은 `HashMap`을 사용하여 고유한 정점과 각각의 인덱스를 추적하는 것입니다: + +```rust +// load_model 메서드를 수정 +use std::collections::HashMap; + +// ... +fn load_model(&mut self) { + // ... tobj::load_obj 호출 + + let mut unique_vertices = HashMap::new(); + let model = &models[0]; + let mesh = &model.mesh; + + for index in &mesh.indices { + let index = *index as usize; + + let pos = glam::vec3( + mesh.positions[3 * index], + mesh.positions[3 * index + 1], + mesh.positions[3 * index + 2], + ); + let tex_coord = glam::vec2( + mesh.texcoords[2 * index], + 1.0 - mesh.texcoords[2 * index + 1], + ); + + let vertex = Vertex { + pos, + tex_coord, + color: glam::Vec3::ONE, + }; + + if let Some(index) = unique_vertices.get(&vertex) { + self.indices.push(*index); + } else { + let index = self.vertices.len() as u32; + unique_vertices.insert(vertex, index); + self.vertices.push(vertex); + self.indices.push(index); + } + } +} +``` +*Note: `load_model`의 기존 로직을 이 코드로 완전히 교체하세요.* + +OBJ 파일에서 정점을 구성할 때마다, `HashMap`을 사용하여 정확히 동일한 위치와 텍스처 좌표를 가진 정점을 이전에 본 적이 있는지 확인합니다. 만약 본 적이 없다면, `vertices` `Vec`에 추가하고 그 인덱스를 `unique_vertices` 맵에 저장합니다. 그 후 새 정점의 인덱스를 `indices` `Vec`에 추가합니다. 만약 이전에 정확히 동일한 정점을 본 적이 있다면, 맵에서 그 인덱스를 찾아 `indices`에 저장합니다. + +`Vertex` 구조체를 `HashMap`의 키로 사용하려면, `Eq` (그리고 `PartialEq`)와 `Hash` 트레이트를 구현해야 합니다. Rust에서는 `f32`가 `NaN` 값 때문에 기본적으로 `Eq`와 `Hash`를 구현하지 않지만, `glam` 크레이트의 `hash` 기능을 활성화하면 이 문제를 해결해 줍니다. `derive` 매크로를 사용하여 이 트레이트들을 쉽게 구현할 수 있습니다. `Vertex` 구조체 정의를 다음과 같이 수정하세요: + +```rust +use glam::{Vec2, Vec3}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[repr(C)] +struct Vertex { + pos: Vec3, + color: Vec3, + tex_coord: Vec2, +} +``` +*Note: `glam`의 `Vec3`와 `Vec2`는 `Eq`와 `Hash`를 구현하려면 `hash` 기능이 활성화되어 있어야 합니다. 우리는 이미 `Cargo.toml`에 이를 추가했습니다.* + +이제 프로그램을 성공적으로 컴파일하고 실행할 수 있을 것입니다. `vertices`의 길이를 확인해보면, 이 모델의 경우 1,500,000개에 육박하던 것이 265,645개로 줄어든 것을 볼 수 있습니다! 이는 각 정점이 평균적으로 약 6개의 삼각형에서 재사용된다는 것을 의미합니다. 이로써 확실히 많은 GPU 메모리를 절약할 수 있습니다. + +[Rust 코드](/code/28_model_loading.rs) / +[정점 셰이더](/code/27_shader_depth.vert) / +[프래그먼트 셰이더](/code/27_shader_depth.frag) \ No newline at end of file diff --git a/ko-rust/09_Generating_Mipmaps.md b/ko-rust/09_Generating_Mipmaps.md new file mode 100644 index 00000000..ea37e2f5 --- /dev/null +++ b/ko-rust/09_Generating_Mipmaps.md @@ -0,0 +1,466 @@ +## 서론 +이제 우리 프로그램은 3D 모델을 로드하고 렌더링할 수 있습니다. 이번 장에서는 밉맵 생성이라는 기능을 하나 더 추가할 것입니다. 밉맵은 게임과 렌더링 소프트웨어에서 널리 사용되며, Vulkan은 밉맵 생성 방법을 완벽하게 제어할 수 있도록 해줍니다. + +밉맵은 미리 계산된, 축소된 버전의 이미지입니다. 각각의 새 이미지는 이전 이미지의 너비와 높이가 절반입니다. 밉맵은 *디테일 수준(Level of Detail, LOD)*의 한 형태로 사용됩니다. 카메라에서 멀리 떨어진 객체는 더 작은 밉 이미지에서 텍스처를 샘플링합니다. 더 작은 이미지를 사용하면 렌더링 속도가 향상되고 [모아레 패턴](https://ko.wikipedia.org/wiki/%EB%AC%B4%EC%95%84%EB%A0%88_%EB%AC%B4%EB%8A%AC)과 같은 아티팩트를 방지할 수 있습니다. 밉맵이 어떻게 생겼는지 보여주는 예시는 다음과 같습니다: + +![](/images/mipmaps_example.jpg) + +## 이미지 생성 + +Vulkan에서 각 밉 이미지는 `vk::Image`의 서로 다른 *밉 레벨(mip level)*에 저장됩니다. 밉 레벨 0은 원본 이미지이며, 레벨 0 이후의 밉 레벨들은 흔히 *밉 체인(mip chain)*이라고 불립니다. + +밉 레벨의 수는 `vk::Image`를 생성할 때 지정됩니다. 지금까지 우리는 항상 이 값을 1로 설정했습니다. 이제 이미지의 크기로부터 밉 레벨의 수를 계산해야 합니다. 먼저, 이 수를 저장할 구조체 필드를 추가합니다: + +```rust +struct HelloTriangleApplication { + ... + mip_levels: u32, + texture_image: vk::Image, + ... +} +``` + +`mip_levels`의 값은 `create_texture_image`에서 텍스처를 로드한 후에 계산할 수 있습니다: + +```rust +let image = image::load_from_memory(include_bytes!(TEXTURE_PATH)) + .expect("Failed to load texture image") + .to_rgba8(); +let (tex_width, tex_height) = image.dimensions(); +let image_data = image.into_raw(); +... +self.mip_levels = ((tex_width.max(tex_height) as f32).log2().floor() + 1.0) as u32; +``` + +이 코드는 밉 체인의 레벨 수를 계산합니다. `max` 메서드는 가장 큰 차원(너비 또는 높이)을 선택합니다. `log2` 메서드는 해당 차원을 2로 몇 번 나눌 수 있는지 계산합니다. `floor` 메서드는 가장 큰 차원이 2의 거듭제곱이 아닌 경우를 처리합니다. `1`을 더해서 원본 이미지 자체도 밉 레벨을 갖도록 합니다. + +이 값을 사용하려면 `create_image`, `create_image_view`, `transition_image_layout` 함수를 수정하여 밉 레벨 수를 지정할 수 있도록 해야 합니다. 함수들에 `mip_levels` 매개변수를 추가하세요: + +```rust +unsafe fn create_image( + &self, + width: u32, + height: u32, + mip_levels: u32, + format: vk::Format, + tiling: vk::ImageTiling, + usage: vk::ImageUsageFlags, + properties: vk::MemoryPropertyFlags, +) -> (vk::Image, vk::DeviceMemory) { + ... + let image_info = vk::ImageCreateInfo::builder() + ... + .mip_levels(mip_levels) + ...; + ... +} +``` +```rust +unsafe fn create_image_view( + &self, + image: vk::Image, + format: vk::Format, + aspect_flags: vk::ImageAspectFlags, + mip_levels: u32, +) -> vk::ImageView { + ... + let subresource_range = vk::ImageSubresourceRange::builder() + ... + .level_count(mip_levels); + + let view_info = vk::ImageViewCreateInfo::builder() + .subresource_range(*subresource_range); + ... +} +``` +```rust +unsafe fn transition_image_layout( + &self, + image: vk::Image, + format: vk::Format, + old_layout: vk::ImageLayout, + new_layout: vk::ImageLayout, + mip_levels: u32, +) { + ... + let barrier = vk::ImageMemoryBarrier::builder() + ... + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: mip_levels, + base_array_layer: 0, + layer_count: 1, + }); + ... +} +``` + +이 함수들에 대한 모든 호출을 올바른 값을 사용하도록 업데이트합니다 (`unsafe` 블록 안에서 호출해야 합니다): + +```rust +let (depth_image, depth_image_memory) = self.create_image( + self.swapchain_extent.width, + self.swapchain_extent.height, + 1, + depth_format, + vk::ImageTiling::OPTIMAL, + vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT, + vk::MemoryPropertyFlags::DEVICE_LOCAL, +); +... +let (texture_image, texture_image_memory) = self.create_image( + tex_width, + tex_height, + self.mip_levels, + vk::Format::R8G8B8A8_SRGB, + vk::ImageTiling::OPTIMAL, + vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED, + vk::MemoryPropertyFlags::DEVICE_LOCAL, +); +``` +```rust +self.swapchain_image_views = self + .swapchain_images + .iter() + .map(|&image| { + self.create_image_view( + image, + self.swapchain_image_format, + vk::ImageAspectFlags::COLOR, + 1, + ) + }) + .collect(); +... +self.depth_image_view = self.create_image_view( + self.depth_image, + depth_format, + vk::ImageAspectFlags::DEPTH, + 1, +); +... +self.texture_image_view = self.create_image_view( + self.texture_image, + vk::Format::R8G8B8A8_SRGB, + vk::ImageAspectFlags::COLOR, + self.mip_levels, +); +``` +```rust +self.transition_image_layout( + self.depth_image, + depth_format, + vk::ImageLayout::UNDEFINED, + vk::ImageLayout::DEPTH_STENCIL_ATTACHMENT_OPTIMAL, + 1, +); +... +self.transition_image_layout( + self.texture_image, + vk::Format::R8G8B8A8_SRGB, + vk::ImageLayout::UNDEFINED, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + self.mip_levels, +); +``` + +## 밉맵 생성하기 + +이제 우리의 텍스처 이미지는 여러 밉 레벨을 가지지만, 스테이징 버퍼는 밉 레벨 0을 채우는 데만 사용될 수 있습니다. 다른 레벨들은 여전히 정의되지 않은 상태입니다. 이 레벨들을 채우려면 우리가 가진 단일 레벨로부터 데이터를 생성해야 합니다. 이를 위해 `vkCmdBlitImage` 명령을 사용할 것입니다. 이 명령은 복사, 스케일링, 필터링 연산을 수행합니다. 우리는 이 명령을 여러 번 호출하여 우리 텍스처 이미지의 각 레벨로 데이터를 *블릿(blit)*할 것입니다. + +`vkCmdBlitImage`는 전송 작업으로 간주되므로, 텍스처 이미지를 전송의 소스(source)와 대상(destination)으로 모두 사용할 것임을 Vulkan에 알려야 합니다. `create_texture_image`에서 텍스처 이미지의 사용 플래그에 `vk::ImageUsageFlags::TRANSFER_SRC`를 추가합니다: + +```rust +let (texture_image, texture_image_memory) = self.create_image( + tex_width, + tex_height, + self.mip_levels, + vk::Format::R8G8B8A8_SRGB, + vk::ImageTiling::OPTIMAL, + vk::ImageUsageFlags::TRANSFER_SRC | vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED, + vk::MemoryPropertyFlags::DEVICE_LOCAL, +); +``` + +다른 이미지 작업과 마찬가지로, `vkCmdBlitImage`는 작동하는 이미지의 레이아웃에 의존합니다. `transition_image_layout`은 전체 이미지에 대해서만 레이아웃 전환을 수행하므로, 밉맵을 생성하기 위해 파이프라인 배리어 명령을 직접 기록해야 합니다. `create_texture_image`에서 `vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL`로의 기존 전환을 제거합니다. 밉맵 생성 함수가 이 전환을 처리할 것입니다. + +```rust +// In create_texture_image... +self.transition_image_layout( + self.texture_image, + vk::Format::R8G8B8A8_SRGB, + vk::ImageLayout::UNDEFINED, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + self.mip_levels, +); +self.copy_buffer_to_image( + staging_buffer, + self.texture_image, + tex_width, + tex_height, +); +// 밉맵을 생성하는 동안 SHADER_READ_ONLY_OPTIMAL로 전환됨 +``` + +이렇게 하면 텍스처 이미지의 각 레벨이 `vk::ImageLayout::TRANSFER_DST_OPTIMAL` 상태로 남게 됩니다. 이제 밉맵을 생성하는 함수를 작성해 보겠습니다: + +```rust +unsafe fn generate_mipmaps( + &self, + image: vk::Image, + tex_width: u32, + tex_height: u32, + mip_levels: u32, +) { + let command_buffer = self.begin_single_time_commands(); + + let mut barrier = vk::ImageMemoryBarrier::builder() + .image(image) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .subresource_range( + vk::ImageSubresourceRange::builder() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .base_array_layer(0) + .layer_count(1) + .level_count(1) + .build(), + ) + .build(); + + let mut mip_width = tex_width as i32; + let mut mip_height = tex_height as i32; + + for i in 1..mip_levels { + // ... 루프 내용 + } + + // ... 루프 후 배리어 + + self.end_single_time_commands(command_buffer); +} +``` + +여러 번의 전환을 수행할 것이므로 `barrier` 변수를 재사용할 것입니다. `subresource_range.base_mip_level`, `old_layout`, `new_layout`, `src_access_mask`, `dst_access_mask`가 각 전환마다 변경됩니다. + +루프는 각 `vkCmdBlitImage` 명령을 기록합니다. 루프 변수가 0이 아닌 1에서 시작하는 점에 유의하세요. + +```rust +// 루프 내부 +barrier.subresource_range.base_mip_level = i - 1; +barrier.old_layout = vk::ImageLayout::TRANSFER_DST_OPTIMAL; +barrier.new_layout = vk::ImageLayout::TRANSFER_SRC_OPTIMAL; +barrier.src_access_mask = vk::AccessFlags::TRANSFER_WRITE; +barrier.dst_access_mask = vk::AccessFlags::TRANSFER_READ; + +self.device.cmd_pipeline_barrier( + command_buffer, + vk::PipelineStageFlags::TRANSFER, + vk::PipelineStageFlags::TRANSFER, + vk::DependencyFlags::empty(), + &[], + &[], + &[barrier], +); +``` +먼저, `i - 1` 레벨을 `vk::ImageLayout::TRANSFER_SRC_OPTIMAL`로 전환합니다. 이 전환은 이전 블릿 명령이나 `copy_buffer_to_image`로부터 `i - 1` 레벨이 채워질 때까지 기다립니다. 현재 블릿 명령은 이 전환을 기다립니다. + +```rust +// 루프 내부, 첫 번째 배리어 다음 +let blit = vk::ImageBlit::builder() + .src_offsets([ + vk::Offset3D { x: 0, y: 0, z: 0 }, + vk::Offset3D { x: mip_width, y: mip_height, z: 1 }, + ]) + .src_subresource( + vk::ImageSubresourceLayers::builder() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .mip_level(i - 1) + .base_array_layer(0) + .layer_count(1) + .build(), + ) + .dst_offsets([ + vk::Offset3D { x: 0, y: 0, z: 0 }, + vk::Offset3D { + x: if mip_width > 1 { mip_width / 2 } else { 1 }, + y: if mip_height > 1 { mip_height / 2 } else { 1 }, + z: 1, + }, + ]) + .dst_subresource( + vk::ImageSubresourceLayers::builder() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .mip_level(i) + .base_array_layer(0) + .layer_count(1) + .build(), + ) + .build(); + +self.device.cmd_blit_image( + command_buffer, + image, + vk::ImageLayout::TRANSFER_SRC_OPTIMAL, + image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + &[blit], + vk::Filter::LINEAR, +); +``` +이제 블릿 명령을 기록합니다. `src_image`와 `dst_image` 매개변수 모두에 `image`가 사용되는 점에 유의하세요. 소스 밉 레벨은 방금 `vk::ImageLayout::TRANSFER_SRC_OPTIMAL`로 전환되었고, 대상 레벨은 아직 `vk::ImageLayout::TRANSFER_DST_OPTIMAL` 상태입니다. 보간을 위해 필터로 `vk::Filter::LINEAR`를 사용합니다. + +```rust +// 루프 내부, cmd_blit_image 다음 +barrier.old_layout = vk::ImageLayout::TRANSFER_SRC_OPTIMAL; +barrier.new_layout = vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL; +barrier.src_access_mask = vk::AccessFlags::TRANSFER_READ; +barrier.dst_access_mask = vk::AccessFlags::SHADER_READ; + +self.device.cmd_pipeline_barrier( + command_buffer, + vk::PipelineStageFlags::TRANSFER, + vk::PipelineStageFlags::FRAGMENT_SHADER, + vk::DependencyFlags::empty(), + &[], + &[], + &[barrier], +); + +if mip_width > 1 { mip_width /= 2; } +if mip_height > 1 { mip_height /= 2; } +``` +이 배리어는 밉 레벨 `i - 1`을 `vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL`로 전환합니다. 이 전환은 현재 블릿 명령이 완료되기를 기다립니다. 그 후, 다음 이터레이션을 위해 밉 차원을 절반으로 줄입니다. + +```rust +// 루프 이후 +barrier.subresource_range.base_mip_level = mip_levels - 1; +barrier.old_layout = vk::ImageLayout::TRANSFER_DST_OPTIMAL; +barrier.new_layout = vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL; +barrier.src_access_mask = vk::AccessFlags::TRANSFER_WRITE; +barrier.dst_access_mask = vk::AccessFlags::SHADER_READ; + +self.device.cmd_pipeline_barrier( + command_buffer, + vk::PipelineStageFlags::TRANSFER, + vk::PipelineStageFlags::FRAGMENT_SHADER, + vk::DependencyFlags::empty(), + &[], + &[], + &[barrier], +); + +self.end_single_time_commands(command_buffer); +``` +커맨드 버퍼를 종료하기 전에, 마지막 밉 레벨을 `vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL`로 전환하는 배리어를 하나 더 삽입합니다. 마지막 밉 레벨은 블릿의 소스로 사용되지 않았기 때문에 루프에서 처리되지 않았습니다. + +마지막으로, `create_texture_image`에서 `generate_mipmaps`를 호출합니다: + +```rust +// in create_texture_image, after copy_buffer_to_image +self.generate_mipmaps( + self.texture_image, + tex_width, + tex_height, + self.mip_levels +); +``` + +## 선형 필터링 지원 + +`vkCmdBlitImage`는 편리하지만, 모든 하드웨어에서 선형 필터링을 지원하지는 않을 수 있습니다. `vkGetPhysicalDeviceFormatProperties` 함수로 이를 확인할 수 있습니다. + +먼저 `generate_mipmaps` 함수에 `image_format` 매개변수를 추가하고, 호출하는 곳에서도 전달해줍니다. + +```rust +// in create_texture_image +self.generate_mipmaps( + self.texture_image, + vk::Format::R8G8B8A8_SRGB, + tex_width, + tex_height, + self.mip_levels, +); + +// function signature +unsafe fn generate_mipmaps( + &self, + image: vk::Image, + image_format: vk::Format, + tex_width: u32, + tex_height: u32, + mip_levels: u32, +) { + // ... +} +``` + +`generate_mipmaps` 함수 시작 부분에서, `get_physical_device_format_properties`를 사용하여 형식 속성을 확인합니다: + +```rust +// in generate_mipmaps +let format_properties = self + .instance + .get_physical_device_format_properties(self.physical_device, image_format); + +if !format_properties + .optimal_tiling_features + .contains(vk::FormatFeatureFlags::SAMPLED_IMAGE_FILTER_LINEAR) +{ + panic!("Texture image format does not support linear blitting!"); +} +``` + +최적 타일링 이미지를 생성하므로 `optimal_tiling_features`를 확인합니다. 선형 필터링 기능 지원은 `vk::FormatFeatureFlags::SAMPLED_IMAGE_FILTER_LINEAR` 플래그로 확인할 수 있습니다. 지원되지 않는 경우, 다른 이미지 형식을 찾거나 [stb_image_resize](https://github.com/nothings/stb/blob/master/stb_image_resize.h)와 같은 라이브러리를 사용하여 소프트웨어에서 밉맵을 구현할 수 있습니다. + +## 샘플러 + +`vk::Image`가 밉맵 데이터를 보유하는 동안, `vk::Sampler`는 렌더링 중에 해당 데이터를 읽는 방법을 제어합니다. Vulkan은 `min_lod`, `max_lod`, `mip_lod_bias`, `mipmap_mode`를 지정할 수 있게 해줍니다. + +이 장의 결과를 보려면, `texture_sampler`의 설정을 업데이트해야 합니다. 이미 `min_filter`와 `mag_filter`는 `vk::Filter::LINEAR`로 설정했습니다. 이제 밉맵 관련 설정을 추가합니다. + +```rust +// in create_texture_sampler +let sampler_info = vk::SamplerCreateInfo::builder() + .mag_filter(vk::Filter::LINEAR) + .min_filter(vk::Filter::LINEAR) + .address_mode_u(vk::SamplerAddressMode::REPEAT) + .address_mode_v(vk::SamplerAddressMode::REPEAT) + .address_mode_w(vk::SamplerAddressMode::REPEAT) + .anisotropy_enable(true) + .max_anisotropy(properties.limits.max_sampler_anisotropy) + .border_color(vk::BorderColor::INT_OPAQUE_BLACK) + .unnormalized_coordinates(false) + .compare_enable(false) + .compare_op(vk::CompareOp::ALWAYS) + .mipmap_mode(vk::SamplerMipmapMode::LINEAR) + .min_lod(0.0) + .max_lod(self.mip_levels as f32) // 모든 밉 레벨 사용 + .mip_lod_bias(0.0); // 선택 사항 +``` +모든 밉 레벨을 사용하려면 `min_lod`를 0.0으로, `max_lod`를 밉 레벨의 수로 설정합니다. `mip_lod_bias`는 lod 계산에 대한 편향을 추가하는 데 사용되며, 여기서는 0.0으로 둡니다. + +이제 프로그램을 실행하면 다음과 같은 화면을 볼 수 있습니다: + +![](/images/mipmaps.png) + +장면이 단순해서 차이가 극적이지는 않지만, 종이에 적힌 글씨를 자세히 보면 차이점을 발견할 수 있습니다. + +![](/images/mipmaps_comparison.png) + +밉맵을 사용하면 글씨가 부드럽게 처리되지만, 밉맵이 없으면 모아레 아티팩트로 인해 거친 가장자리와 끊김이 보입니다. + +`min_lod`와 같은 샘플러 설정을 변경하여 밉맵 효과를 시험해 볼 수 있습니다. 예를 들어, `min_lod`를 높이면 강제로 더 흐릿한(더 높은 레벨의) 밉맵을 사용하게 됩니다. + +```rust +// in create_texture_sampler +... +.min_lod((self.mip_levels / 2) as f32) +.max_lod(self.mip_levels as f32) +... +``` + +이 설정은 객체가 카메라에서 더 멀리 있을 때 렌더링되는 모습과 유사한 이미지를 생성합니다: + +![](/images/highmipmaps.png) \ No newline at end of file diff --git a/ko-rust/10_Multisampling.md b/ko-rust/10_Multisampling.md new file mode 100644 index 00000000..63397431 --- /dev/null +++ b/ko-rust/10_Multisampling.md @@ -0,0 +1,408 @@ +## 소개 + +이제 우리 프로그램은 텍스처에 대해 여러 디테일 수준(Level of Detail, LOD)을 로드할 수 있게 되어, 뷰어로부터 멀리 떨어진 객체를 렌더링할 때 발생하던 아티팩트(artifact)를 수정합니다. 이미지는 이제 훨씬 부드러워졌지만, 자세히 살펴보면 그려진 기하학적 모양의 가장자리를 따라 들쭉날쭉한 톱니 모양의 패턴을 발견할 수 있습니다. 이는 초기에 사각형 하나를 렌더링했던 프로그램에서 특히 두드러지게 나타납니다. + +![](/images/texcoord_visualization.png) + +이러한 바람직하지 않은 효과를 "앨리어싱(aliasing)"이라고 하며, 이는 렌더링에 사용할 수 있는 픽셀 수가 제한적이기 때문에 발생하는 결과입니다. 무한한 해상도를 가진 디스플레이는 없으므로, 이 현상은 어느 정도 항상 보일 수밖에 없습니다. 이를 해결하는 여러 방법이 있으며, 이 장에서는 가장 널리 사용되는 방법 중 하나인 [멀티샘플 안티-앨리어싱(Multisample anti-aliasing, MSAA)](https://en.wikipedia.org/wiki/Multisample_anti-aliasing)에 초점을 맞출 것입니다. + +일반적인 렌더링에서 픽셀 색상은 단일 샘플 포인트(대부분 화면의 대상 픽셀 중앙)를 기준으로 결정됩니다. 만약 그려진 선의 일부가 특정 픽셀을 통과하지만 샘플 포인트를 덮지 않으면, 그 픽셀은 비어 있게 되어 들쭉날쭉한 "계단 현상"이 발생합니다. + +![](/images/aliasing.png) + +MSAA는 픽셀당 여러 개의 샘플 포인트(이름에서 알 수 있듯이)를 사용하여 최종 색상을 결정합니다. 예상할 수 있듯이, 샘플 수가 많을수록 결과는 좋아지지만, 연산 비용도 더 많이 듭니다. + +![](/images/antialiasing.png) + +우리의 구현에서는 사용 가능한 최대 샘플 수를 사용하는 데 중점을 둘 것입니다. 여러분의 애플리케이션에 따라 이것이 항상 최선의 접근 방식은 아닐 수 있으며, 최종 결과가 품질 요구 사항을 충족한다면 더 높은 성능을 위해 더 적은 샘플을 사용하는 것이 더 나을 수도 있습니다. + +## 사용 가능한 샘플 수 얻기 + +먼저 우리 하드웨어가 사용할 수 있는 샘플 수를 결정하는 것부터 시작하겠습니다. 대부분의 최신 GPU는 최소 8개의 샘플을 지원하지만, 이 숫자가 모든 곳에서 동일하다고 보장할 수는 없습니다. `App` 구조체에 새로운 필드를 추가하여 이 값을 추적하겠습니다. + +```rust +struct App { + ... + msaa_samples: vk::SampleCountFlags, + ... +} +``` + +기본적으로 픽셀당 하나의 샘플만 사용할 것이며, 이는 멀티샘플링을 사용하지 않는 것과 같습니다. 이 경우 최종 이미지는 변경되지 않습니다. `App::new`에서 이 값을 초기화합니다. 정확한 최대 샘플 수는 선택된 물리 디바이스와 연관된 `vk::PhysicalDeviceProperties`에서 추출할 수 있습니다. 우리는 깊이 버퍼를 사용하므로, 컬러와 깊이 버퍼 모두에 대한 샘플 수를 고려해야 합니다. 두 버퍼 모두에서 지원되는(&) 가장 높은 샘플 수가 우리가 지원할 수 있는 최대치가 됩니다. 이 정보를 가져올 헬퍼 메서드를 추가합시다. + +```rust +impl App { + fn get_max_usable_sample_count(&self) -> vk::SampleCountFlags { + let physical_device_properties = unsafe { + self.instance + .get_physical_device_properties(self.physical_device) + }; + + let counts = physical_device_properties.limits.framebuffer_color_sample_counts + & physical_device_properties.limits.framebuffer_depth_sample_counts; + + [ + vk::SampleCountFlags::TYPE_64, + vk::SampleCountFlags::TYPE_32, + vk::SampleCountFlags::TYPE_16, + vk::SampleCountFlags::TYPE_8, + vk::SampleCountFlags::TYPE_4, + vk::SampleCountFlags::TYPE_2, + ] + .into_iter() + .find(|c| counts.contains(*c)) + .unwrap_or(vk::SampleCountFlags::TYPE_1) + } +} +``` + +이제 이 메서드를 사용하여 물리 디바이스 선택 과정에서 `msaa_samples` 필드를 설정할 것입니다. 이를 위해 `pick_physical_device` 메서드를 약간 수정해야 합니다. + +```rust +impl App { + fn pick_physical_device(&mut self) -> Result<()> { + ... + for device in physical_devices { + if self.is_device_suitable(device)? { + self.physical_device = device; + self.msaa_samples = self.get_max_usable_sample_count(); + return Ok(()); + } + } + ... + } +} +``` + +## 렌더 타겟 설정하기 + +MSAA에서는 각 픽셀이 오프스크린 버퍼에 샘플링된 후 화면에 렌더링됩니다. 이 새로운 버퍼는 우리가 지금까지 렌더링해왔던 일반 이미지와는 약간 다릅니다. 픽셀당 하나 이상의 샘플을 저장할 수 있어야 합니다. 멀티샘플링된 버퍼가 생성되면, 기본 프레임버퍼(픽셀당 단일 샘플만 저장)로 리졸브(resolve)되어야 합니다. 이 때문에 추가적인 렌더 타겟을 생성하고 현재의 그리기 프로세스를 수정해야 합니다. 깊이 버퍼와 마찬가지로 한 번에 하나의 그리기 작업만 활성화되므로 렌더 타겟은 하나만 필요합니다. `App` 구조체에 다음 필드를 추가합시다. + +```rust +struct App { + ... + color_image: vk::Image, + color_image_memory: vk::DeviceMemory, + color_image_view: vk::ImageView, + ... +} +``` + +이 새로운 이미지는 픽셀당 원하는 수의 샘플을 저장해야 하므로, 이미지 생성 과정에서 `vk::ImageCreateInfo`에 이 숫자를 전달해야 합니다. `create_image` 메서드에 `num_samples` 파라미터를 추가하여 수정합시다. + +```rust +fn create_image( + &mut self, + width: u32, + height: u32, + mip_levels: u32, + num_samples: vk::SampleCountFlags, + format: vk::Format, + tiling: vk::ImageTiling, + usage: vk::ImageUsageFlags, + properties: vk::MemoryPropertyFlags, +) -> Result<(vk::Image, vk::DeviceMemory)> { + ... + let image_info = vk::ImageCreateInfo::builder() + ... + .samples(num_samples); + ... +} +``` + +이제 구현을 진행하면서 적절한 값으로 대체할 것이므로, 지금은 이 함수에 대한 모든 호출을 `vk::SampleCountFlags::TYPE_1`을 사용하여 업데이트합니다. + +```rust +// In create_depth_resources +let (depth_image, depth_image_memory) = self.create_image( + self.swapchain_extent.width, + self.swapchain_extent.height, + 1, + vk::SampleCountFlags::TYPE_1, // Will be updated later + depth_format, + vk::ImageTiling::OPTIMAL, + vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT, + vk::MemoryPropertyFlags::DEVICE_LOCAL, +)?; + +// In create_texture_image +let (texture_image, texture_image_memory) = self.create_image( + tex_width as u32, + tex_height as u32, + self.mip_levels, + vk::SampleCountFlags::TYPE_1, + vk::Format::R8G8B8A8_SRGB, + vk::ImageTiling::OPTIMAL, + vk::ImageUsageFlags::TRANSFER_SRC | vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED, + vk::MemoryPropertyFlags::DEVICE_LOCAL, +)?; +``` + +이제 멀티샘플링된 컬러 버퍼를 생성하겠습니다. `create_color_resources` 메서드를 추가하고, 여기서 `self.msaa_samples`를 `create_image` 메서드의 파라미터로 사용하고 있음을 주목하세요. 밉 레벨은 하나만 사용하는데, 이는 픽셀당 샘플이 하나 이상인 이미지의 경우 벌칸 명세에 의해 강제되기 때문입니다. 또한, 이 컬러 버퍼는 텍스처로 사용되지 않을 것이므로 밉맵이 필요 없습니다. + +```rust +impl App { + fn create_color_resources(&mut self) -> Result<()> { + let color_format = self.swapchain_format; + + let (color_image, color_image_memory) = self.create_image( + self.swapchain_extent.width, + self.swapchain_extent.height, + 1, + self.msaa_samples, + color_format, + vk::ImageTiling::OPTIMAL, + vk::ImageUsageFlags::TRANSIENT_ATTACHMENT | vk::ImageUsageFlags::COLOR_ATTACHMENT, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + )?; + + self.color_image = color_image; + self.color_image_memory = color_image_memory; + + self.color_image_view = self.create_image_view( + self.color_image, + color_format, + vk::ImageAspectFlags::COLOR, + 1, + )?; + + Ok(()) + } +} +``` + +일관성을 위해, 이 메서드를 `create_depth_resources` 바로 앞에서 호출합니다. + +```rust +// In recreate_swapchain or init +... +self.create_color_resources()?; +self.create_depth_resources()?; +... +``` + +이제 멀티샘플링된 컬러 버퍼가 준비되었으니, 깊이 버퍼를 처리할 차례입니다. `create_depth_resources`를 수정하고 깊이 버퍼에서 사용하는 샘플 수를 업데이트하세요. + +```rust +impl App { + fn create_depth_resources(&mut self) -> Result<()> { + ... + let (depth_image, depth_image_memory) = self.create_image( + self.swapchain_extent.width, + self.swapchain_extent.height, + 1, + self.msaa_samples, // Use MSAA samples + depth_format, + vk::ImageTiling::OPTIMAL, + vk::ImageUsageFlags::DEPTH_STENCIL_ATTACHMENT, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + )?; + ... + } +} +``` + +이제 몇 가지 새로운 벌칸 리소스를 생성했으므로, `cleanup_swapchain`에서 이를 해제하는 것을 잊지 말아야 합니다. + +```rust +impl App { + unsafe fn cleanup_swapchain(&mut self) { + self.device.destroy_image_view(self.color_image_view, None); + self.device.destroy_image(self.color_image, None); + self.device.free_memory(self.color_image_memory, None); + ... + } +} +``` + +그리고 `recreate_swapchain`에서 새로운 컬러 이미지가 창 크기 조절 시 올바른 해상도로 다시 생성되도록 호출을 추가했습니다. + +초기 MSAA 설정을 마쳤습니다. 이제 이 새로운 리소스를 그래픽 파이프라인, 프레임버퍼, 렌더 패스에서 사용하고 결과를 확인해야 합니다! + +## 새로운 어태치먼트 추가하기 + +먼저 렌더 패스부터 처리합시다. `create_render_pass`를 수정하여 컬러 및 깊이 어태치먼트 생성 정보를 업데이트하세요. + +```rust +impl App { + fn create_render_pass(&mut self) -> Result<()> { + ... + let color_attachment = vk::AttachmentDescription::builder() + ... + .samples(self.msaa_samples) + .final_layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL); // Changed + + let depth_attachment = vk::AttachmentDescription::builder() + ... + .samples(self.msaa_samples); + ... + } +} +``` + +`final_layout`을 `vk::ImageLayout::PRESENT_SRC_KHR`에서 `vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL`로 변경한 것을 눈치채셨을 겁니다. 이는 멀티샘플링된 이미지는 직접 화면에 표시(present)할 수 없기 때문입니다. 먼저 일반 이미지로 리졸브해야 합니다. 이 요구사항은 깊이 버퍼에는 적용되지 않는데, 깊이 버퍼는 어떤 시점에도 화면에 표시되지 않기 때문입니다. 따라서 우리는 소위 리졸브 어태치먼트(resolve attachment)라고 불리는, 컬러를 위한 새로운 어태치먼트 하나만 추가하면 됩니다. + +```rust +impl App { + fn create_render_pass(&mut self) -> Result<()> { + ... + let color_attachment_resolve = vk::AttachmentDescription::builder() + .format(self.swapchain_format) + .samples(vk::SampleCountFlags::TYPE_1) + .load_op(vk::AttachmentLoadOp::DONT_CARE) + .store_op(vk::AttachmentStoreOp::STORE) + .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE) + .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE) + .initial_layout(vk::ImageLayout::UNDEFINED) + .final_layout(vk::ImageLayout::PRESENT_SRC_KHR); + ... + } +} +``` + +이제 렌더 패스는 멀티샘플링된 컬러 이미지를 일반 어태치먼트로 리졸브하도록 지시받아야 합니다. 리졸브 타겟이 될 컬러 버퍼를 가리킬 새로운 어태치먼트 참조를 생성합니다. + +```rust +impl App { + fn create_render_pass(&mut self) -> Result<()> { + ... + let color_attachment_resolve_ref = vk::AttachmentReference::builder() + .attachment(2) + .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL); + ... + } +} +``` + +서브패스 빌더의 `.resolve_attachments()` 메서드를 사용하여 새로 생성된 어태치먼트 참조를 설정합니다. 이것만으로도 렌더 패스가 멀티샘플 리졸브 작업을 정의하게 되어, 이미지를 화면에 렌더링할 수 있게 됩니다. + +```rust +impl App { + fn create_render_pass(&mut self) -> Result<()> { + ... + let subpass = vk::SubpassDescription::builder() + .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS) + .color_attachments(std::slice::from_ref(&color_attachment_ref)) + .depth_stencil_attachment(&depth_attachment_ref) + .resolve_attachments(std::slice::from_ref(&color_attachment_resolve_ref)); // Set resolve attachment + ... + } +} +``` + +멀티샘플링된 컬러 이미지를 재사용하므로, `VkSubpassDependency`의 `src_access_mask`를 업데이트해야 합니다. 이 업데이트는 컬러 어태치먼트에 대한 쓰기 작업이 후속 작업 시작 전에 완료되도록 보장하여, 불안정한 렌더링 결과를 초래할 수 있는 쓰기 후 쓰기(write-after-write) 위험을 방지합니다. + +```rust +impl App { + fn create_render_pass(&mut self) -> Result<()> { + ... + let dependency = vk::SubpassDependency::builder() + .src_subpass(vk::SUBPASS_EXTERNAL) + .dst_subpass(0) + .src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT | vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS) + .src_access_mask(vk::AccessFlags::empty()) + .dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT | vk::PipelineStageFlags::EARLY_FRAGMENT_TESTS) + .dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE | vk::AccessFlags::DEPTH_STENCIL_ATTACHMENT_WRITE); + ... + } +} +``` + +이제 렌더 패스 생성 정보에 새로운 컬러 어태치먼트를 포함하여 업데이트합니다. + +```rust +impl App { + fn create_render_pass(&mut self) -> Result<()> { + ... + let attachments = [ + color_attachment.build(), + depth_attachment.build(), + color_attachment_resolve.build(), + ]; + let render_pass_info = vk::RenderPassCreateInfo::builder() + .attachments(&attachments) + ... + } +} +``` + +렌더 패스가 준비되었으니, `create_framebuffers`를 수정하고 새로운 이미지 뷰를 목록에 추가합니다. 어태치먼트 순서는 렌더 패스에 정의된 순서와 일치해야 합니다. + +```rust +impl App { + fn create_framebuffers(&mut self) -> Result<()> { + ... + for view in self.swapchain_image_views.iter() { + let attachments = &[self.color_image_view, self.depth_image_view, *view]; + ... + } + ... + } +} +``` + +마지막으로, `create_graphics_pipeline`을 수정하여 새로 생성된 파이프라인이 하나 이상의 샘플을 사용하도록 지시합니다. + +```rust +impl App { + fn create_graphics_pipeline(&mut self) -> Result<()> { + ... + let multisampling = vk::PipelineMultisampleStateCreateInfo::builder() + .sample_shading_enable(false) + .rasterization_samples(self.msaa_samples); + ... + } +} +``` + +이제 프로그램을 실행하면 다음과 같은 화면을 볼 수 있습니다. + +![](/images/multisampling.png) + +밉매핑과 마찬가지로, 차이가 즉시 눈에 띄지 않을 수 있습니다. 자세히 살펴보면 가장자리가 예전만큼 들쭉날쭉하지 않고 전체 이미지가 원본에 비해 약간 더 부드러워진 것을 알 수 있습니다. + +![](/images/multisampling_comparison.png) + +가장자리 중 하나를 가까이에서 보면 차이가 더 두드러집니다. + +![](/images/multisampling_comparison2.png) + +## 품질 개선 + +현재 MSAA 구현에는 몇 가지 한계가 있어 더 디테일한 장면에서 출력 이미지의 품질에 영향을 미칠 수 있습니다. 예를 들어, 현재 우리는 셰이더 앨리어싱으로 인해 발생할 수 있는 잠재적인 문제를 해결하고 있지 않습니다. 즉, MSAA는 지오메트리의 가장자리만 부드럽게 처리할 뿐 내부 채우기는 처리하지 않습니다. 이로 인해 화면에 부드러운 폴리곤이 렌더링되더라도, 적용된 텍스처에 대비가 강한 색상이 포함되어 있다면 여전히 앨리어싱이 발생한 것처럼 보일 수 있습니다. 이 문제를 해결하는 한 가지 방법은 [샘플 셰이딩(Sample Shading)](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap27.html#primsrast-sampleshading)을 활성화하는 것입니다. 이는 추가적인 성능 비용을 수반하지만 이미지 품질을 더욱 향상시킬 수 있습니다. + +```rust +// In create_logical_device +let features = vk::PhysicalDeviceFeatures::builder() + .sampler_anisotropy(true) + .sample_rate_shading(true); // 디바이스에 샘플 셰이딩 기능 활성화 + +// In create_graphics_pipeline +let multisampling = vk::PipelineMultisampleStateCreateInfo::builder() + .rasterization_samples(self.msaa_samples) + .sample_shading_enable(true) // 파이프라인에서 샘플 셰이딩 활성화 + .min_sample_shading(0.2); // 샘플 셰이딩을 위한 최소 비율; 1.0에 가까울수록 부드러워짐 +``` + +이 예제에서는 샘플 셰이딩을 비활성화 상태로 두겠지만, 특정 시나리오에서는 품질 향상이 눈에 띄게 나타날 수 있습니다. + +![](/images/sample_shading.png) + +## 결론 + +여기까지 오기까지 많은 노력이 필요했지만, 이제 여러분은 마침내 훌륭한 벌칸 프로그램의 기반을 갖추게 되었습니다. 여러분이 지금 가진 벌칸의 기본 원리에 대한 지식은 다음과 같은 더 많은 기능을 탐색하기에 충분할 것입니다. + +* 푸시 상수(Push constants) +* 인스턴스 렌더링(Instanced rendering) +* 동적 유니폼(Dynamic uniforms) +* 이미지와 샘플러 디스크립터 분리 +* 파이프라인 캐시 +* 다중 스레드 커맨드 버퍼 생성 +* 다중 서브패스 +* 컴퓨트 셰이더 + +현재 프로그램은 블린-퐁(Blinn-Phong) 조명, 후처리 효과, 그림자 매핑 등을 추가하는 등 다양한 방식으로 확장될 수 있습니다. 벌칸의 명시적인 특성에도 불구하고 많은 개념이 여전히 동일하게 작동하기 때문에, 다른 API의 튜토리얼을 통해 이러한 효과들이 어떻게 작동하는지 배울 수 있을 것입니다. + +[Rust 코드](/code/30_multisampling.rs) / +[정점 셰이더](/code/27_shader_depth.vert) / +[프래그먼트 셰이더](/code/27_shader_depth.frag) \ No newline at end of file diff --git a/ko-rust/11_Compute_Shader.md b/ko-rust/11_Compute_Shader.md new file mode 100644 index 00000000..f1df57a7 --- /dev/null +++ b/ko-rust/11_Compute_Shader.md @@ -0,0 +1,454 @@ +## 소개 + +이 보너스 챕터에서는 컴퓨트 셰이더(compute shader)에 대해 살펴보겠습니다. 지금까지의 모든 챕터는 Vulkan 파이프라인의 전통적인 그래픽스 부분을 다루었습니다. 하지만 OpenGL과 같은 오래된 API와 달리, Vulkan에서 컴퓨트 셰이더 지원은 필수입니다. 이는 고사양 데스크톱 GPU든 저전력 임베디드 장치든, 사용 가능한 모든 Vulkan 구현에서 컴퓨트 셰이더를 사용할 수 있다는 의미입니다. + +이는 여러분의 애플리케이션이 어디서 실행되든 상관없이 GPU(그래픽 처리 장치)를 이용한 범용 컴퓨팅(GPGPU, general purpose computing on graphics processor units)의 세계를 열어줍니다. GPGPU는 전통적으로 CPU의 영역이었던 일반적인 계산을 GPU에서 수행할 수 있음을 의미합니다. GPU가 점점 더 강력해지고 유연해짐에 따라, CPU의 범용적인 능력이 필요했던 많은 작업들을 이제 GPU에서 실시간으로 처리할 수 있게 되었습니다. + +GPU의 컴퓨팅 능력이 사용될 수 있는 몇 가지 예로는 이미지 처리, 가시성 테스트, 후처리(post processing), 고급 조명 계산, 애니메이션, 물리(예: 파티클 시스템) 등이 있으며, 이 외에도 훨씬 더 많습니다. 심지어 수치 연산이나 AI 관련 작업처럼 그래픽 출력이 전혀 필요 없는 비시각적 계산 전용 작업에도 컴퓨트를 사용할 수 있습니다. 이를 "헤드리스 컴퓨트(headless compute)"라고 합니다. + +## 장점 + +계산 비용이 많이 드는 작업을 GPU에서 수행하면 몇 가지 장점이 있습니다. 가장 명백한 것은 CPU의 작업을 덜어내는 것입니다. 또 다른 장점은 CPU의 주 메모리와 GPU 메모리 간에 데이터를 옮길 필요가 없다는 점입니다. 모든 데이터는 주 메모리로부터의 느린 전송을 기다릴 필요 없이 GPU에 머무를 수 있습니다. + +이 외에도 GPU는 수만 개의 작은 연산 유닛으로 고도로 병렬화되어 있습니다. 이 때문에 몇 개의 큰 연산 유닛을 가진 CPU보다 고도로 병렬화된 워크플로우에 더 적합한 경우가 많습니다. + +## Vulkan 파이프라인 + +컴퓨트는 파이프라인의 그래픽스 부분과 완전히 분리되어 있다는 점을 알아두는 것이 중요합니다. 이는 공식 명세서의 다음 Vulkan 파이프라인 블록 다이어그램에서 확인할 수 있습니다. + +![](/images/vulkan_pipeline_block_diagram.png) + +이 다이어그램의 왼쪽에는 전통적인 그래픽스 파이프라인 부분이 있고, 오른쪽에는 컴퓨트 셰이더 단계를 포함하여 이 그래픽스 파이프라인에 속하지 않는 여러 단계들이 있습니다. 컴퓨트 셰이더 단계가 그래픽스 파이프라인에서 분리되어 있으므로, 우리는 필요하다고 생각되는 어느 곳에서든 이를 사용할 수 있습니다. 이는 항상 정점 셰이더의 변환된 출력에 적용되는 프래그먼트 셰이더와는 매우 다릅니다. + +다이어그램 중앙은 디스크립터 셋(descriptor set)과 같은 요소들이 컴퓨트에서도 사용된다는 것을 보여주므로, 우리가 디스크립터 레이아웃, 디스크립터 셋, 디스크립터에 대해 배운 모든 것이 여기에도 적용됩니다. + +## 예제 + +이 챕터에서 구현할 이해하기 쉬운 예제는 GPU 기반 파티클 시스템입니다. 이러한 시스템은 많은 게임에서 사용되며, 종종 상호작용 가능한 프레임 속도로 업데이트되어야 하는 수천 개의 파티클로 구성됩니다. 이러한 시스템을 렌더링하려면 두 가지 주요 구성 요소가 필요합니다: 정점 버퍼로 전달되는 정점들과, 어떤 방정식에 기반하여 이들을 업데이트하는 방법입니다. + +GPU 기반 파티클 시스템을 사용하면 CPU를 거치는 과정이 더 이상 필요하지 않습니다. 정점 데이터는 처음에만 GPU에 업로드되며, 모든 업데이트는 컴퓨트 셰이더를 사용하여 GPU 메모리 내에서 이루어집니다. 이것이 더 빠른 주된 이유 중 하나는 GPU와 로컬 메모리 간의 훨씬 높은 대역폭 때문입니다. + +다음은 이 챕터 코드의 스크린샷입니다. 여기에 보이는 파티클은 CPU 상호작용 없이 GPU에서 직접 컴퓨트 셰이더에 의해 업데이트됩니다. + +![](/images/compute_shader_particles.png) + +## 데이터 조작 + +### 셰이더 저장 버퍼 객체 (SSBO) + +셰이더 저장 버퍼(SSBO, Shader Storage Buffer Object)는 셰이더가 버퍼에서 읽고 쓸 수 있게 해줍니다. Vulkan에서는 버퍼와 이미지에 대해 여러 사용 용도를 지정할 수 있습니다. 따라서 파티클 정점 버퍼를 정점 버퍼(그래픽스 패스)와 저장 버퍼(컴퓨트 패스)로 사용하려면, 해당 사용 플래그로 버퍼를 생성하기만 하면 됩니다. + +`ash`에서는 빌더 패턴을 사용하여 생성 정보를 구성합니다. `|` 연산자는 `bitflags` 크레이트에 의해 제공되므로 C++와 유사하게 사용할 수 있습니다. + +```rust +use ash::vk; + +let buffer_info = vk::BufferCreateInfo::builder() + .size(buffer_size) + .usage( + vk::BufferUsageFlags::VERTEX_BUFFER + | vk::BufferUsageFlags::STORAGE_BUFFER + | vk::BufferUsageFlags::TRANSFER_DST, + ) + .sharing_mode(vk::SharingMode::EXCLUSIVE); + +let shader_storage_buffer = unsafe { device.create_buffer(&buffer_info, None) } + .expect("셰이더 저장 버퍼 생성에 실패했습니다!"); +``` + +GLSL 셰이더의 SSBO 선언은 C++ 예제와 동일합니다. Rust 측에서는 이 구조체를 `#[repr(C)]`로 정의하여 C/GLSL과 메모리 레이아웃을 호환시켜야 합니다. + +```glsl +// GLSL +struct Particle { + vec2 position; + vec2 velocity; + vec4 color; +}; + +layout(std140, binding = 1) readonly buffer ParticleSSBOIn { + Particle particlesIn[ ]; +}; + +layout(std140, binding = 2) buffer ParticleSSBOOut { + Particle particlesOut[ ]; +}; +``` + +```rust +// Rust +use glam::{Vec2, Vec4}; // 또는 다른 벡터 라이브러리 + +#[repr(C)] +#[derive(Clone, Copy, Debug)] +struct Particle { + position: Vec2, + velocity: Vec2, + color: Vec4, +} +``` + +## 컴퓨트 큐 패밀리 + +컴퓨트 작업을 하려면 `VK_QUEUE_COMPUTE_BIT` 플래그를 지원하는 큐 패밀리를 찾아야 합니다. Rust에서는 이터레이터와 `find`, `enumerate`를 사용하여 이 과정을 더 깔끔하게 작성할 수 있습니다. + +```rust +let queue_families = unsafe { + instance.get_physical_device_queue_family_properties(physical_device) +}; + +let queue_family_indices = queue_families + .iter() + .enumerate() + .find(|(_index, queue_family)| { + queue_family.queue_flags.contains(vk::QueueFlags::GRAPHICS) + && queue_family.queue_flags.contains(vk::QueueFlags::COMPUTE) + }) + .map(|(index, _queue_family)| index as u32); +``` + +큐를 가져오는 작업은 `ash`의 `Device` 구조체에 대한 메서드 호출로 이루어집니다. + +```rust +let compute_queue = unsafe { + device.get_device_queue(indices.graphics_and_compute_family.unwrap(), 0) +}; +``` + +## 컴퓨트 셰이더 로드하기 + +셰이더 로드는 다른 셰이더와 동일하지만, `stage` 필드에 `vk::ShaderStageFlags::COMPUTE`를 사용해야 합니다. Rust에서는 `pName` 필드에 C 문자열을 전달해야 하므로, `CStr`을 사용합니다. + +```rust +use std::ffi::CStr; + +let compute_shader_code = read_shader_code("shaders/compute.spv"); // 셰이더 파일을 읽는 헬퍼 함수 +let compute_shader_module = create_shader_module(&device, &compute_shader_code)?; + +let shader_entry_name = CStr::from_bytes_with_nul(b"main\0").unwrap(); + +let compute_shader_stage_info = vk::PipelineShaderStageCreateInfo::builder() + .stage(vk::ShaderStageFlags::COMPUTE) + .module(compute_shader_module) + .name(shader_entry_name) + .build(); +``` + +## 셰이더 저장 버퍼 준비하기 + +`Frames in flight` 개념을 적용하여, 각 프레임마다 SSBO를 생성합니다. Rust에서는 `Vec`을 사용하여 버퍼와 메모리 핸들을 저장합니다. + +```rust +let mut shader_storage_buffers: Vec = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); +let mut shader_storage_buffers_memory: Vec = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); +``` + +호스트 측에서 파티클 데이터를 초기화하고, 스테이징 버퍼를 통해 GPU 전용 메모리로 복사합니다. + +```rust +// 파티클 초기화 (rand 크레이트 사용 가능) +let mut particles: Vec = Vec::with_capacity(PARTICLE_COUNT); +for _ in 0..PARTICLE_COUNT { + // ... 파티클 생성 로직 ... +} + +let buffer_size = (std::mem::size_of::() * PARTICLE_COUNT) as vk::DeviceSize; + +// 스테이징 버퍼 생성 및 데이터 복사 +let (staging_buffer, staging_buffer_memory) = create_buffer( + &device, + buffer_size, + vk::BufferUsageFlags::TRANSFER_SRC, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + // ... +)?; + +// `ash::util::Align`을 사용한 안전한 메모리 복사 +unsafe { + let data_ptr = device.map_memory(staging_buffer_memory, 0, buffer_size, vk::MemoryMapFlags::empty())?; + let mut align = ash::util::Align::new(data_ptr, std::mem::align_of::() as u64, buffer_size); + align.copy_from_slice(&particles); + device.unmap_memory(staging_buffer_memory); +} + +// 각 프레임에 대한 SSBO 생성 및 데이터 복사 +for i in 0..MAX_FRAMES_IN_FLIGHT { + let (buffer, memory) = create_buffer( + &device, + buffer_size, + vk::BufferUsageFlags::STORAGE_BUFFER | vk::BufferUsageFlags::VERTEX_BUFFER | vk::BufferUsageFlags::TRANSFER_DST, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + // ... + )?; + copy_buffer(staging_buffer, buffer, buffer_size, ...)?; + shader_storage_buffers.push(buffer); + shader_storage_buffers_memory.push(memory); +} + +// 스테이징 버퍼 정리 +// ... +``` + +## 디스크립터 + +컴퓨트 셰이더를 위한 디스크립터 레이아웃을 설정할 때, `stage_flags`에 `vk::ShaderStageFlags::COMPUTE`를 지정합니다. + +```rust +let layout_bindings = [ + vk::DescriptorSetLayoutBinding::builder() + .binding(0) + .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::COMPUTE) + .build(), + vk::DescriptorSetLayoutBinding::builder() + .binding(1) + .descriptor_type(vk::DescriptorType::STORAGE_BUFFER) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::COMPUTE) + .build(), + vk::DescriptorSetLayoutBinding::builder() + .binding(2) + .descriptor_type(vk::DescriptorType::STORAGE_BUFFER) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::COMPUTE) + .build(), +]; +let layout_info = vk::DescriptorSetLayoutCreateInfo::builder().bindings(&layout_bindings); +let compute_descriptor_set_layout = unsafe { device.create_descriptor_set_layout(&layout_info, None)? }; +``` + +디스크립터 셋을 업데이트할 때, 이전 프레임과 현재 프레임의 SSBO를 모두 참조하도록 설정합니다. Rust에서는 구조체와 슬라이스를 사용하여 `pBufferInfo`를 안전하게 처리해야 합니다. + +```rust +for i in 0..MAX_FRAMES_IN_FLIGHT { + let uniform_buffer_info = vk::DescriptorBufferInfo::builder() + .buffer(uniform_buffers[i]) + .offset(0) + .range(std::mem::size_of::() as u64) + .build(); + + let storage_buffer_info_last_frame = vk::DescriptorBufferInfo::builder() + .buffer(shader_storage_buffers[(i + MAX_FRAMES_IN_FLIGHT - 1) % MAX_FRAMES_IN_FLIGHT]) + .offset(0) + .range(buffer_size) + .build(); + + let storage_buffer_info_current_frame = vk::DescriptorBufferInfo::builder() + .buffer(shader_storage_buffers[i]) + .offset(0) + .range(buffer_size) + .build(); + + let descriptor_writes = [ + // UBO 쓰기 + vk::WriteDescriptorSet::builder() + .dst_set(compute_descriptor_sets[i]) + .dst_binding(0) + .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) + .buffer_info(std::slice::from_ref(&uniform_buffer_info)) + .build(), + // 이전 프레임 SSBO 쓰기 + vk::WriteDescriptorSet::builder() + .dst_set(compute_descriptor_sets[i]) + .dst_binding(1) + .descriptor_type(vk::DescriptorType::STORAGE_BUFFER) + .buffer_info(std::slice::from_ref(&storage_buffer_info_last_frame)) + .build(), + // 현재 프레임 SSBO 쓰기 + vk::WriteDescriptorSet::builder() + .dst_set(compute_descriptor_sets[i]) + .dst_binding(2) + .descriptor_type(vk::DescriptorType::STORAGE_BUFFER) + .buffer_info(std::slice::from_ref(&storage_buffer_info_current_frame)) + .build(), + ]; + + unsafe { device.update_descriptor_sets(&descriptor_writes, &[]) }; +} +``` + +디스크립터 풀을 생성할 때, 두 개의 SSBO를 사용하므로 필요한 수를 두 배로 요청해야 합니다. + +```rust +let pool_sizes = [ + // ... + vk::DescriptorPoolSize { + ty: vk::DescriptorType::STORAGE_BUFFER, + descriptor_count: (MAX_FRAMES_IN_FLIGHT * 2) as u32, + }, +]; +``` + +## 컴퓨트 파이프라인 + +컴퓨트 파이프라인은 `create_compute_pipelines` 함수로 생성합니다. 그래픽스 파이프라인보다 훨씬 간단합니다. + +```rust +let pipeline_layout_info = vk::PipelineLayoutCreateInfo::builder() + .set_layouts(std::slice::from_ref(&compute_descriptor_set_layout)); +let compute_pipeline_layout = unsafe { device.create_pipeline_layout(&pipeline_layout_info, None)? }; + +let pipeline_info = vk::ComputePipelineCreateInfo::builder() + .stage(compute_shader_stage_info) + .layout(compute_pipeline_layout) + .build(); + +let compute_pipelines = unsafe { + device.create_compute_pipelines(vk::PipelineCache::null(), &[pipeline_info], None) +}.map_err(|(_, err)| err)?; + +let compute_pipeline = compute_pipelines[0]; +// 사용 후에는 `device.destroy_pipeline`으로 파이프라인들을 정리해야 합니다. +``` + +## 컴퓨트 공간 및 컴퓨트 셰이더 + +이 개념들은 API에 독립적이므로, C++ 튜토리얼의 설명과 동일합니다. GLSL 셰이더 코드도 변경 없이 그대로 사용할 수 있습니다. + +## 컴퓨트 명령 실행하기 + +### 디스패치 + +`ash`에서 모든 `cmd_` 함수는 `unsafe` 블록 안에서 호출되어야 합니다. 이는 유효한 커맨드 버퍼 기록 상태에서만 호출되어야 함을 명시하기 위함입니다. `cmd_dispatch`로 컴퓨트 작업을 시작합니다. + +```rust +unsafe { + device.begin_command_buffer(command_buffer, &begin_info)?; + + device.cmd_bind_pipeline(command_buffer, vk::PipelineBindPoint::COMPUTE, compute_pipeline); + device.cmd_bind_descriptor_sets( + command_buffer, + vk::PipelineBindPoint::COMPUTE, + compute_pipeline_layout, + 0, + &[compute_descriptor_sets[current_frame]], + &[], + ); + + // 워크 그룹 수 계산 + let group_count = (PARTICLE_COUNT as u32 + 255) / 256; + device.cmd_dispatch(command_buffer, group_count, 1, 1); + + device.end_command_buffer(command_buffer)?; +} +``` + +### 작업 제출 및 동기화 + +컴퓨트 작업과 그래픽스 작업을 동기화하기 위해 세마포어와 펜스를 사용합니다. `ash`를 사용한 제출 로직은 다음과 같습니다. + +```rust +// 컴퓨트 작업용 동기화 객체 생성 +// compute_in_flight_fences: Vec +// compute_finished_semaphores: Vec +// ... + +// draw_frame 함수 내부 +// --- 컴퓨트 제출 --- +unsafe { + device.wait_for_fences(&[compute_in_flight_fences[current_frame]], true, u64::MAX)?; + device.reset_fences(&[compute_in_flight_fences[current_frame]])?; +} + +// ... 컴퓨트 커맨드 버퍼 기록 ... + +let compute_submit_info = vk::SubmitInfo::builder() + .command_buffers(std::slice::from_ref(&compute_command_buffers[current_frame])) + .signal_semaphores(std::slice::from_ref(&compute_finished_semaphores[current_frame])) + .build(); + +unsafe { + device.queue_submit( + compute_queue, + &[compute_submit_info], + compute_in_flight_fences[current_frame], + )?; +} + +// --- 그래픽스 제출 --- +unsafe { + device.wait_for_fences(&[in_flight_fences[current_frame]], true, u64::MAX)?; + device.reset_fences(&[in_flight_fences[current_frame]])?; +} + +// ... 그래픽스 커맨드 버퍼 기록 ... + +let wait_semaphores = [ + compute_finished_semaphores[current_frame], + image_available_semaphores[current_frame], +]; +let wait_stages = [ + vk::PipelineStageFlags::VERTEX_INPUT, + vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT, +]; + +let graphics_submit_info = vk::SubmitInfo::builder() + .wait_semaphores(&wait_semaphores) + .wait_dst_stage_mask(&wait_stages) + .command_buffers(std::slice::from_ref(&command_buffers[current_frame])) + .signal_semaphores(std::slice::from_ref(&render_finished_semaphores[current_frame])) + .build(); + +unsafe { + device.queue_submit( + graphics_queue, + &[graphics_submit_info], + in_flight_fences[current_frame], + )?; +} +``` + +## 파티클 시스템 그리기 + +SSBO는 정점 버퍼로도 사용될 수 있으므로, 그래픽스 파이프라인에서 바로 바인딩하여 그릴 수 있습니다. Rust에서는 `memoffset` 크레이트를 사용하여 구조체 멤버의 오프셋을 안전하게 계산할 수 있습니다. + +```rust +use memoffset::offset_of; +// Particle 구조체 impl 블록 내부 +impl Particle { + pub fn get_attribute_descriptions() -> [vk::VertexInputAttributeDescription; 2] { + [ + vk::VertexInputAttributeDescription { + binding: 0, + location: 0, + format: vk::Format::R32G32_SFLOAT, + offset: offset_of!(Particle, position) as u32, + }, + vk::VertexInputAttributeDescription { + binding: 0, + location: 1, + format: vk::Format::R32G32B32A32_SFLOAT, + offset: offset_of!(Particle, color) as u32, + }, + ] + } +} +``` + +드로잉 명령은 다음과 같습니다. + +```rust +unsafe { + let offsets = [0]; + device.cmd_bind_vertex_buffers( + command_buffer, + 0, + &[shader_storage_buffers[current_frame]], + &offsets, + ); + + device.cmd_draw(command_buffer, PARTICLE_COUNT as u32, 1, 0, 0); +} +``` + +## 결론 + +이 챕터에서는 Rust와 `ash` 라이브러리를 사용하여 컴퓨트 셰이더를 설정하고 실행하는 방법을 배웠습니다. C++과 개념은 동일하지만, Rust의 소유권 시스템, `unsafe` 키워드, 빌더 패턴, 이터레이터, `Result`를 통한 오류 처리 등 언어적 특성을 활용하여 코드를 작성했습니다. + +이제 여러분은 Vulkan 컴퓨트 셰이더의 기본을 익혔으며, 이를 바탕으로 공유 메모리, 비동기 컴퓨트, 원자적 연산 등 더 복잡한 GPGPU 기술을 탐구할 준비가 되었습니다. + +[Rust 코드](/examples/compute_shader/main.rs) / +[정점 셰이더](/code/31_shader_compute.vert) / +[프래그먼트 셰이더](/code/31_shader_compute.frag) / +[컴퓨트 셰이더](/code/31_shader_compute.comp) \ No newline at end of file diff --git a/ko-rust/90_FAQ.md b/ko-rust/90_FAQ.md new file mode 100644 index 00000000..b7327747 --- /dev/null +++ b/ko-rust/90_FAQ.md @@ -0,0 +1,61 @@ +이 페이지는 Rust와 `ash` 라이브러리를 사용하여 Vulkan 애플리케이션을 개발하면서 마주칠 수 있는 일반적인 문제들에 대한 해결책을 다룹니다. + +## 코어 검증 레이어에서 접근 위반(access violation) 오류가 발생합니다 + +MSI Afterburner / RivaTuner Statistics Server가 Vulkan과 몇 가지 호환성 문제가 있으므로, 해당 프로그램이 실행 중이지 않은지 확인하십시오. + +## 검증 레이어에서 아무런 메시지도 표시되지 않거나, 검증 레이어를 사용할 수 없습니다 + +먼저, 프로그램이 종료된 후에도 터미널 창을 열어 두어 검증 레이어가 오류를 출력할 시간을 주어야 합니다. 터미널에서 `cargo run`을 사용하여 프로그램을 직접 실행하면 프로그램 종료 후에도 메시지를 확인할 수 있습니다. + +그래도 메시지가 표시되지 않고 검증 레이어가 켜져 있는 것이 확실하다면, [이 페이지](https://vulkan.lunarg.com/doc/view/1.2.135.0/windows/getting_started.html)의 '설치 확인(Verify the Installation)' 안내에 따라 Vulkan SDK가 올바르게 설치되었는지 확인해야 합니다. 또한 `VK_LAYER_KHRONOS_validation` 레이어를 지원하려면 SDK 버전이 최소 **1.1.106.0** 이상인지 확인하십시오. + +## vkCreateSwapchainKHR 함수 호출 시 SteamOverlayVulkanLayer64.dll에서 오류가 발생합니다 + +이것은 스팀(Steam) 클라이언트 베타의 호환성 문제로 보입니다. 다음과 같은 몇 가지 해결 방법이 있습니다: +* 스팀 베타 프로그램 참여를 중단합니다. +* `DISABLE_VK_LAYER_VALVE_steam_overlay_1` 환경 변수를 `1`로 설정합니다. +* `HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ImplicitLayers` 경로 아래 레지스트리에서 스팀 오버레이 Vulkan 레이어 항목을 삭제합니다. + +## vkCreateInstance 호출이 VK_ERROR_INCOMPATIBLE_DRIVER 오류와 함께 실패합니다 + +최신 MoltenVK SDK와 함께 macOS를 사용하는 경우, `ash`의 `create_instance` 호출이 `VK_ERROR_INCOMPATIBLE_DRIVER` 오류를 반환할 수 있습니다. 이는 [Vulkan SDK 버전 1.3.216 이상](https://vulkan.lunarg.com/doc/sdk/1.3.216.0/mac/getting_started.html)부터 MoltenVK가 아직 완벽하게 호환되지 않기 때문에, 이를 사용하려면 `VK_KHR_PORTABILITY_subset` 확장을 활성화해야 하기 때문입니다. + +`ash::vk::InstanceCreateInfo`를 생성할 때 플래그에 `ash::vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR`를 추가하고, 활성화할 인스턴스 확장 목록에 `ash::extensions::khr::PortabilityEnumeration::name()`을 추가해야 합니다. + +코드 예시 (`winit`과 `ash-window`를 사용하는 일반적인 경우): + +```rust +use ash::extensions::khr::PortabilityEnumeration; +use ash::vk; + +// ... Entry, Window 생성 ... + +// winit 같은 윈도우 라이브러리에서 요구하는 확장을 가져옵니다. +let mut extension_names = ash_window::enumerate_required_extensions(window) + .unwrap() + .to_vec(); + +// Portability 확장을 추가합니다. +extension_names.push(PortabilityEnumeration::name().as_ptr()); + +// create_info의 flags에 ENUMERATE_PORTABILITY_KHR를 추가합니다. +let create_flags = vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR; + +let app_info = vk::ApplicationInfo::builder() + .application_name(CStr::from_bytes_with_nul(b"Vulkan App\0").unwrap()) + // ... 기타 정보 설정 + .build(); + +let create_info = vk::InstanceCreateInfo::builder() + .application_info(&app_info) + .enabled_extension_names(&extension_names) + // .enabled_layer_names(&layer_names) // 검증 레이어 등 + .flags(create_flags); // 여기에 플래그를 설정합니다. + +let instance: ash::Instance = unsafe { + entry + .create_instance(&create_info, None) + .expect("인스턴스 생성에 실패했습니다!"); +}; +``` \ No newline at end of file diff --git a/ko-rust/95_Privacy_policy.md b/ko-rust/95_Privacy_policy.md new file mode 100644 index 00000000..fea81c4b --- /dev/null +++ b/ko-rust/95_Privacy_policy.md @@ -0,0 +1,23 @@ +## Ash 라이브러리 문서 개인정보처리방침 + +### 일반 + +본 개인정보처리방침은 귀하가 Ash 라이브러리 공식 문서(예: ash-docs.rs 또는 그 하위 도메인)를 이용할 때 수집되는 정보에 적용됩니다. 본 문서는 Ash 프로젝트 팀이 귀하에 대한 정보를 수집, 이용 및 공유하는 방법을 설명합니다. + +### 분석 + +이 웹사이트는 자체 호스팅하는 Matomo([https://matomo.org/](https://matomo.org/)) 인스턴스를 사용하여 방문자에 대한 분석 데이터를 수집합니다. 이 분석 데이터에는 귀하가 조회하는 API 문서나 예제 페이지, 사용하는 기기 및 브라우저 유형, 특정 페이지를 본 시간, 유입 경로가 기록됩니다. 이 정보는 IP 주소의 앞 두 바이트(예: `123.123.xxx.xxx`)만 기록하여 익명으로 처리되며, 이렇게 익명화된 로그는 영구적으로 저장됩니다. + +수집된 분석 데이터는 어떤 API와 예제 코드가 가장 많이 조회되는지, 어떤 Rust 버전과 운영체제 환경에서 개발자들이 문서를 참고하는지 파악하는 데 사용됩니다. 이를 통해 개발자 커뮤니티의 관심사를 파악하고, 문서의 어떤 부분을 우선적으로 개선하거나 보강해야 할지(예: 특정 고급 기능에 대한 설명 추가) 결정하는 데 활용됩니다. + +이 데이터는 제3자와 공유되지 않습니다. + +### 광고 + +Ash 라이브러리 문서는 오픈 소스 프로젝트의 일환으로, 영리 목적의 광고를 게재하지 않습니다. 따라서 광고를 위한 사용자 활동 추적이나 쿠키 사용은 일절 없습니다. + +### 피드백 및 토론 + +문서의 각 페이지나 예제 코드에 대한 질문 및 토론은 GitHub Issues를 통해 이루어집니다. GitHub에 이슈나 코멘트를 작성할 경우, 귀하의 GitHub 프로필 정보가 공개적으로 표시됩니다. 이는 GitHub의 서비스 운영 방침에 따릅니다. + +GitHub의 개인정보처리방침은 다음 링크에서 확인하실 수 있습니다: [https://docs.github.com/en/site-policy/privacy-policies/github-privacy-statement](https://docs.github.com/en/site-policy/privacy-policies/github-privacy-statement) \ No newline at end of file diff --git a/ko/00_Introduction.md b/ko/00_Introduction.md new file mode 100644 index 00000000..3952db9b --- /dev/null +++ b/ko/00_Introduction.md @@ -0,0 +1,67 @@ +## 소개 + +이 튜토리얼에서는 [Vulkan](https://www.khronos.org/vulkan/) 그래픽 및 컴퓨팅 API의 기초를 배웁니다. Vulkan은 [Khronos group](https://www.khronos.org/)(OpenGL으로 유명한)에서 만든 새로운 API로, 최신 그래픽 카드를 훨씬 더 잘 추상화합니다. 이 새로운 인터페이스를 통해 애플리케이션이 무엇을 하려는지 더 잘 기술할 수 있으며, 이는 [OpenGL](https://en.wikipedia.org/wiki/OpenGL)이나 [Direct3D](https://en.wikipedia.org/wiki/Direct3D)와 같은 기존 API에 비해 더 나은 성능과 예측하기 쉬운 드라이버 동작으로 이어질 수 있습니다. Vulkan의 기본 개념은 [Direct3D 12](https://en.wikipedia.org/wiki/Direct3D#Direct3D_12)나 [Metal](https://en.wikipedia.org/wiki/Metal_(API))과 유사하지만, Vulkan은 완전한 크로스플랫폼이라는 장점이 있어 Windows, Linux, Android용 개발을 동시에 할 수 있습니다. + +하지만 이러한 이점을 얻기 위해 치러야 할 대가는 상당히 장황한 API를 다뤄야 한다는 것입니다. 초기 프레임 버퍼 생성, 버퍼 및 텍스처 이미지와 같은 객체에 대한 메모리 관리 등 그래픽 API와 관련된 모든 세부 사항을 애플리케이션에서 처음부터 설정해야 합니다. 그래픽 드라이버가 해주는 것들이 훨씬 적어지는데, 이는 정확한 동작을 보장하기 위해 애플리케이션에서 더 많은 작업을 해야 한다는 것을 의미합니다. + +핵심은 Vulkan이 모두를 위한 것은 아니라는 점입니다. Vulkan은 고성능 컴퓨터 그래픽에 열정적이고, 기꺼이 노력을 투자할 의향이 있는 프로그래머를 대상으로 합니다. 컴퓨터 그래픽보다는 게임 개발에 더 관심이 있다면, 가까운 시일 내에 Vulkan 때문에 지원이 중단되지는 않을 OpenGL이나 Direct3D를 계속 사용하는 것이 좋습니다. 또 다른 대안은 [Unreal Engine](https://en.wikipedia.org/wiki/Unreal_Engine#Unreal_Engine_4)이나 [Unity](https://en.wikipedia.org/wiki/Unity_(game_engine))와 같은 엔진을 사용하는 것입니다. 이 엔진들은 내부적으로 Vulkan을 사용하면서도 여러분에게 훨씬 더 높은 수준의 API를 제공할 수 있습니다. + +이제 이 점을 명확히 했으니, 이 튜토리얼을 따라가기 위한 몇 가지 선수 조건을 살펴보겠습니다: + +* Vulkan과 호환되는 그래픽 카드 및 드라이버 ([NVIDIA](https://developer.nvidia.com/vulkan-driver), [AMD](http://www.amd.com/en-us/innovations/software-technologies/technologies-gaming/vulkan), [Intel](https://software.intel.com/en-us/blogs/2016/03/14/new-intel-vulkan-beta-1540204404-graphics-driver-for-windows-78110-1540), [Apple Silicon (또는 Apple M1)](https://www.phoronix.com/scan.php?page=news_item&px=Apple-Silicon-Vulkan-MoltenVK)) +* C++ 경험 (RAII, 초기화 목록(initializer list)에 대한 친숙함) +* C++17 기능을 충분히 지원하는 컴파일러 (Visual Studio 2017+, GCC 7+, 또는 Clang 5+) +* 3D 컴퓨터 그래픽에 대한 약간의 경험 + +이 튜토리얼은 OpenGL이나 Direct3D 개념에 대한 지식을 가정하지는 않지만, 3D 컴퓨터 그래픽의 기초는 알고 있어야 합니다. 예를 들어, 원근 투영(perspective projection)의 기저에 있는 수학은 설명하지 않을 것입니다. 컴퓨터 그래픽 개념에 대한 훌륭한 입문서로는 [이 온라인 책](https://paroj.github.io/gltut/)을 참고하세요. 그 외 다른 훌륭한 컴퓨터 그래픽 자료는 다음과 같습니다: + +* [주말 동안 레이 트레이싱 (Ray tracing in one weekend)](https://github.com/RayTracing/raytracing.github.io) +* [물리 기반 렌더링(PBR) 책 (Physically Based Rendering book)](http://www.pbr-book.org/) +* 실제 엔진에서 Vulkan이 사용된 예시: 오픈소스 [Quake](https://github.com/Novum/vkQuake)와 [DOOM 3](https://github.com/DustinHLand/vkDOOM3) + +원한다면 C++ 대신 C를 사용할 수도 있지만, 그럴 경우 다른 선형대수 라이브러리를 사용해야 하며 코드 구조화는 스스로 해결해야 합니다. 우리는 C++의 클래스나 RAII 같은 기능을 사용해 로직과 리소스의 생명 주기를 관리할 것입니다. 또한 Rust 개발자를 위한 두 가지 대체 튜토리얼 버전도 있습니다: [Vulkano 기반](https://github.com/bwasty/vulkan-tutorial-rs), [Vulkanalia 기반](https://kylemayes.github.io/vulkanalia). + +다른 프로그래밍 언어를 사용하는 개발자들이 쉽게 따라올 수 있도록, 그리고 기본 API에 익숙해지기 위해 우리는 원본 C API를 사용하여 Vulkan을 다룰 것입니다. 하지만 C++를 사용한다면, 일부 궂은일들을 추상화하고 특정 유형의 오류를 방지하는 데 도움을 주는 최신 [Vulkan-Hpp](https://github.com/KhronosGroup/Vulkan-Hpp) 바인딩을 사용하는 것을 선호할 수도 있습니다. + +## 전자책 + +이 튜토리얼을 전자책으로 읽고 싶다면, 아래 링크에서 EPUB 또는 PDF 버전을 다운로드할 수 있습니다: + +* [EPUB](https://vulkan-tutorial.com/resources/vulkan_tutorial_en.epub) +* [PDF](https://vulkan-tutorial.com/resources/vulkan_tutorial_en.pdf) + +## 튜토리얼 구조 + +우리는 Vulkan이 어떻게 작동하는지에 대한 개요와 화면에 첫 번째 삼각형을 띄우기 위해 해야 할 작업들을 살펴보는 것으로 시작할 것입니다. 전체 그림 속에서 각 작은 단계들의 기본적인 역할을 이해하고 나면 그 목적이 더 명확해질 것입니다. 다음으로, [Vulkan SDK](https://lunarg.com/vulkan-sdk/), 선형대수 연산을 위한 [GLM 라이브러리](http://glm.g-truc.net/), 창 생성을 위한 [GLFW](http://www.glfw.org/)로 개발 환경을 설정할 것입니다. 이 튜토리얼은 Windows의 Visual Studio와 Ubuntu Linux의 GCC에서 설정하는 방법을 다룰 것입니다. + +그 후, 첫 번째 삼각형을 렌더링하는 데 필요한 Vulkan 프로그램의 모든 기본 구성 요소를 구현할 것입니다. 각 챕터는 대략 다음과 같은 구조를 따릅니다: + +* 새로운 개념과 그 목적을 소개합니다 +* 관련된 모든 API 호출을 사용하여 프로그램에 통합합니다 +* 일부를 헬퍼 함수로 추상화합니다 + +각 챕터는 이전 챕터에 이어지도록 작성되었지만, 특정 Vulkan 기능을 소개하는 독립적인 문서로도 읽을 수 있습니다. 이는 이 사이트가 참고 자료로도 유용하다는 것을 의미합니다. 모든 Vulkan 함수와 타입은 사양(specification)에 링크되어 있으므로, 클릭하여 더 자세히 알아볼 수 있습니다. Vulkan은 매우 새로운 API이므로 사양 자체에 일부 미흡한 점이 있을 수 있습니다. [이 Khronos 리포지토리](https://github.com/KhronosGroup/Vulkan-Docs)에 피드백을 제출하는 것을 권장합니다. + +앞서 언급했듯이, Vulkan API는 그래픽 하드웨어를 최대한 제어할 수 있도록 많은 파라미터를 가진 장황한 API를 가지고 있습니다. 이로 인해 텍스처 생성과 같은 기본 작업도 매번 반복해야 하는 많은 단계를 거치게 됩니다. 따라서 우리는 튜토리얼 전반에 걸쳐 우리만의 헬퍼 함수 모음을 만들어 나갈 것입니다. + +또한 각 챕터는 해당 지점까지의 전체 코드 목록 링크로 마무리됩니다. 코드 구조에 대해 의문이 있거나, 버그를 처리하며 비교하고 싶을 때 참조할 수 있습니다. 모든 코드 파일은 여러 벤더의 그래픽 카드에서 테스트하여 정확성을 검증했습니다. 각 챕터 끝에는 댓글 섹션도 있어 특정 주제와 관련된 질문을 할 수 있습니다. 저희가 여러분을 돕기 쉽도록 플랫폼, 드라이버 버전, 소스 코드, 예상 동작 및 실제 동작을 명시해 주세요. + +이 튜토리얼은 커뮤니티의 노력으로 만들어지는 것을 목표로 합니다. Vulkan은 아직 매우 새로운 API이며 모범 사례(best practice)가 완전히 정립되지 않았습니다. 튜토리얼과 사이트 자체에 대한 어떤 종류의 피드백이든 있다면, 주저하지 말고 [GitHub 리포지토리](https://github.com/Overv/VulkanTutorial)에 이슈를 제출하거나 풀 리퀘스트(pull request)를 보내주세요. 리포지토리를 'watch'하면 튜토리얼 업데이트 알림을 받을 수 있습니다. + +Vulkan으로 여러분의 첫 번째 삼각형을 화면에 그리는 의식을 치른 후에는, 선형 변환, 텍스처, 3D 모델을 포함하도록 프로그램을 확장해 나갈 것입니다. + +이전에 그래픽 API를 다뤄본 적이 있다면, 첫 도형이 화면에 나타나기까지 많은 단계가 있을 수 있다는 것을 알 것입니다. Vulkan에는 이러한 초기 단계가 많지만, 각각의 개별 단계는 이해하기 쉽고 불필요하게 느껴지지 않을 것입니다. 또한, 그 지루해 보이는 삼각형을 일단 그리고 나면, 완전한 텍스처를 입힌 3D 모델을 그리는 데는 그리 많은 추가 작업이 필요하지 않으며, 그 지점을 넘어선 각 단계는 훨씬 더 보람찰 것이라는 점을 명심하는 것이 중요합니다. + +튜토리얼을 따라가다가 문제가 발생하면, 먼저 FAQ를 확인하여 문제와 해결책이 이미 있는지 확인해 보세요. 그래도 문제가 해결되지 않으면, 가장 관련 있는 챕터의 댓글 섹션에서 자유롭게 도움을 요청하세요. + +고성능 그래픽 API의 미래로 뛰어들 준비가 되셨나요? [시작합시다!](!ko/Overview) + +## 라이선스 + +Copyright (C) 2015-2023, Alexander Overvoorde + +콘텐츠는 별도로 명시되지 않는 한 [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)에 따라 라이선스가 부여됩니다. 기여함으로써 귀하는 귀하의 기여물을 동일한 라이선스 하에 대중에게 라이선스하는 데 동의하는 것입니다. + +소스 리포지토리의 `code` 디렉토리에 있는 코드 목록은 [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/)에 따라 라이선스가 부여됩니다. 해당 디렉토리에 기여함으로써 귀하는 귀하의 기여물을 동일한 퍼블릭 도메인과 유사한 라이선스 하에 대중에게 라이선스하는 데 동의하는 것입니다. + +이 프로그램은 유용할 것이라는 희망으로 배포되지만, 어떠한 보증도 없이 배포됩니다. 상품성이나 특정 목적에의 적합성에 대한 묵시적인 보증조차 없습니다. \ No newline at end of file diff --git a/ko/01_Overview.md b/ko/01_Overview.md new file mode 100644 index 00000000..9f60e5d7 --- /dev/null +++ b/ko/01_Overview.md @@ -0,0 +1,121 @@ +이번 장에서는 먼저 벌칸(Vulkan)의 소개와 벌칸이 해결하고자 하는 문제점들에 대해 알아봅니다. 그 후, 첫 번째 삼각형을 그리는 데 필요한 요소들을 살펴볼 것입니다. 이를 통해 앞으로 이어질 각 장의 내용을 전체적인 그림 안에서 파악할 수 있게 될 것입니다. 마지막으로 벌칸 API의 구조와 일반적인 사용 패턴을 다루며 마무리하겠습니다. + +## 벌칸의 기원 + +이전의 그래픽 API들과 마찬가지로, 벌칸은 [GPU](https://en.wikipedia.org/wiki/Graphics_processing_unit)에 대한 크로스플랫폼 추상화로 설계되었습니다. 이러한 기존 API 대부분의 문제점은, 이들이 설계될 당시의 그래픽 하드웨어가 대부분 설정 가능한 고정 기능(fixed functionality)에 제한되어 있었다는 것입니다. 프로그래머들은 정점(vertex) 데이터를 표준 형식으로 제공해야 했고, 조명이나 셰이딩 옵션에 대해서는 GPU 제조사의 재량에 맡겨야 했습니다. + +그래픽 카드 아키텍처가 발전함에 따라, 프로그래밍 가능한 기능들이 점점 더 많이 제공되기 시작했습니다. 이 모든 새로운 기능들은 어떻게든 기존 API와 통합되어야 했습니다. 그 결과 이상적이지 않은 추상화가 생겨났고, 그래픽 드라이버는 프로그래머의 의도를 현대 그래픽 아키텍처에 매핑하기 위해 많은 추측을 해야 했습니다. 이것이 바로 게임 성능을 향상시키기 위한 드라이버 업데이트가, 때로는 상당한 폭으로, 빈번하게 이루어지는 이유입니다. 이러한 드라이버의 복잡성 때문에, 애플리케이션 개발자들은 [셰이더](https://en.wikipedia.org/wiki/Shader)에 허용되는 문법과 같이 제조사 간의 불일치 문제도 다루어야 합니다. 이러한 새로운 기능 외에도, 지난 10년간 강력한 그래픽 하드웨어를 갖춘 모바일 기기들이 대거 등장했습니다. 이 모바일 GPU들은 에너지 및 공간 요구사항에 따라 다른 아키텍처를 가집니다. 한 예로 [타일 기반 렌더링(tiled rendering)](https://en.wikipedia.org/wiki/Tiled_rendering)이 있는데, 이는 프로그래머에게 해당 기능에 대한 더 많은 제어권을 제공함으로써 성능을 향상시킬 수 있습니다. 이러한 API들의 시대에서 비롯된 또 다른 한계는 제한적인 멀티스레딩 지원으로, 이는 CPU 측의 병목 현상을 유발할 수 있습니다. + +벌칸은 현대 그래픽 아키텍처를 위해 처음부터 새롭게 설계됨으로써 이러한 문제들을 해결합니다. 벌칸은 프로그래머가 더 상세한(verbose) API를 사용하여 자신의 의도를 명확하게 지정할 수 있게 함으로써 드라이버 오버헤드를 줄이고, 여러 스레드가 병렬로 커맨드를 생성하고 제출할 수 있도록 합니다. 또한 단일 컴파일러를 사용하는 표준화된 바이트코드 형식으로 전환하여 셰이더 컴파일의 불일치를 줄입니다. 마지막으로, 현대 그래픽 카드의 범용 처리 능력을 인정하여 그래픽과 컴퓨팅 기능을 단일 API로 통합합니다. + +## 삼각형 하나를 그리기까지 + +이제 잘 만들어진 벌칸 프로그램에서 삼각형 하나를 렌더링하는 데 필요한 모든 단계를 개괄적으로 살펴보겠습니다. 여기서 소개되는 모든 개념은 다음 장들에서 자세히 설명될 것입니다. 이 부분은 개별 구성 요소들을 전체적인 그림 속에서 파악할 수 있도록 돕기 위함입니다. + +### 1단계 - 인스턴스와 물리 장치 선택 + +벌칸 애플리케이션은 `VkInstance`를 통해 벌칸 API를 설정하는 것으로 시작합니다. 인스턴스는 애플리케이션과 사용하려는 API 확장에 대한 정보를 기술하여 생성합니다. 인스턴스를 생성한 후, 벌칸을 지원하는 하드웨어를 쿼리하고 연산에 사용할 하나 이상의 `VkPhysicalDevice`를 선택할 수 있습니다. VRAM 크기나 장치 기능과 같은 속성을 쿼리하여 원하는 장치를 선택할 수 있으며, 예를 들어 전용 그래픽 카드를 선호하도록 설정할 수 있습니다. + +### 2단계 - 논리 장치와 큐 패밀리 + +사용할 하드웨어 장치를 선택한 후에는 `VkDevice`(논리 장치)를 생성해야 합니다. 여기서는 멀티 뷰포트 렌더링이나 64비트 부동소수점 같은, 사용할 `VkPhysicalDeviceFeatures`를 더 구체적으로 기술합니다. 또한 사용하고자 하는 큐 패밀리도 지정해야 합니다. 그리기 커맨드나 메모리 연산과 같이 벌칸으로 수행되는 대부분의 작업은 `VkQueue`에 제출되어 비동기적으로 실행됩니다. 큐는 큐 패밀리로부터 할당되며, 각 큐 패밀리는 자신의 큐에서 특정 유형의 작업 집합을 지원합니다. 예를 들어, 그래픽, 컴퓨트, 메모리 전송 작업을 위한 별도의 큐 패밀리가 있을 수 있습니다. 큐 패밀리의 가용성은 물리 장치를 선택하는 구별 요소로 사용될 수도 있습니다. 벌칸을 지원하는 장치가 그래픽 기능을 전혀 제공하지 않을 수도 있지만, 오늘날 벌칸을 지원하는 모든 그래픽 카드는 일반적으로 우리가 관심 있는 모든 큐 작업을 지원합니다. + +### 3단계 - 윈도우 서피스와 스왑 체인 + +오프스크린 렌더링에만 관심 있는 것이 아니라면, 렌더링된 이미지를 표시할 윈도우를 생성해야 합니다. 윈도우는 네이티브 플랫폼 API나 [GLFW](http://www.glfw.org/), [SDL](https://www.libsdl.org/)과 같은 라이브러리를 사용하여 생성할 수 있습니다. 이 튜토리얼에서는 GLFW를 사용할 것이며, 이에 대한 자세한 내용은 다음 장에서 다룹니다. + +윈도우에 실제로 렌더링하려면 두 가지 구성 요소가 더 필요합니다: 윈도우 서피스(`VkSurfaceKHR`)와 스왑 체인(`VkSwapchainKHR`)입니다. `KHR` 접미사에 주목하세요. 이는 이 객체들이 벌칸 확장의 일부임을 의미합니다. 벌칸 API 자체는 완전히 플랫폼에 독립적이므로, 윈도우 관리자와 상호작용하기 위해 표준화된 WSI(Window System Interface) 확장을 사용해야 합니다. 서피스는 렌더링 대상이 되는 윈도우에 대한 크로스플랫폼 추상화이며, 일반적으로 Windows의 `HWND`와 같은 네이티브 윈도우 핸들에 대한 참조를 제공하여 인스턴스화됩니다. 다행히도 GLFW 라이브러리에는 이러한 플랫폼별 세부 사항을 처리하는 내장 함수가 있습니다. + +스왑 체인은 렌더 타겟의 모음입니다. 스왑 체인의 기본 목적은 현재 렌더링 중인 이미지와 화면에 표시 중인 이미지가 서로 다르도록 보장하는 것입니다. 이는 완전한 이미지만 표시되도록 하는 데 중요합니다. 프레임을 그릴 때마다 스왑 체인에 렌더링할 이미지를 요청해야 합니다. 프레임 그리기가 끝나면, 이미지는 언젠가 화면에 표시될 수 있도록 스왑 체인에 반환됩니다. 렌더 타겟의 수와 완성된 이미지를 화면에 표시하는 조건은 제시 모드(present mode)에 따라 달라집니다. 일반적인 제시 모드로는 이중 버퍼링(vsync)과 삼중 버퍼링이 있습니다. 이는 스왑 체인 생성 장에서 살펴보겠습니다. + +일부 플랫폼에서는 `VK_KHR_display` 및 `VK_KHR_display_swapchain` 확장을 통해 윈도우 관리자와 상호작용 없이 디스플레이에 직접 렌더링할 수 있습니다. 이를 통해 전체 화면을 나타내는 서피스를 생성할 수 있으며, 예를 들어 자신만의 윈도우 관리자를 구현하는 데 사용될 수 있습니다. + +### 4단계 - 이미지 뷰와 프레임버퍼 + +스왑 체인에서 얻은 이미지에 그리려면, 이를 `VkImageView`와 `VkFramebuffer`로 감싸야 합니다. 이미지 뷰는 사용할 이미지의 특정 부분을 참조하고, 프레임버퍼는 색상, 깊이, 스텐실 타겟으로 사용될 이미지 뷰들을 참조합니다. 스왑 체인에는 여러 개의 다른 이미지가 있을 수 있으므로, 각 이미지에 대해 이미지 뷰와 프레임버퍼를 미리 생성해두고 그리기 시점에 올바른 것을 선택할 것입니다. + +### 5단계 - 렌더 패스 + +벌칸의 렌더 패스는 렌더링 작업 중에 사용되는 이미지의 유형, 사용 방식, 그리고 내용 처리 방법을 기술합니다. 우리의 초기 삼각형 렌더링 애플리케이션에서는, 단일 이미지를 색상 타겟으로 사용할 것이며, 그리기 작업 직전에 단색으로 지워지기를 원한다고 벌칸에 알릴 것입니다. 렌더 패스가 이미지의 유형만 기술하는 반면, `VkFramebuffer`는 실제로 특정 이미지를 이 슬롯에 바인딩합니다. + +### 6단계 - 그래픽 파이프라인 + +벌칸의 그래픽 파이프라인은 `VkPipeline` 객체를 생성하여 설정합니다. 이는 뷰포트 크기 및 깊이 버퍼 연산과 같은 그래픽 카드의 설정 가능한 상태와 `VkShaderModule` 객체를 사용한 프로그래밍 가능한 상태를 기술합니다. `VkShaderModule` 객체는 셰이더 바이트코드로부터 생성됩니다. 드라이버는 또한 파이프라인에서 어떤 렌더 타겟이 사용될지 알아야 하며, 이는 렌더 패스를 참조하여 지정합니다. + +기존 API와 비교하여 벌칸의 가장 독특한 특징 중 하나는, 그래픽 파이프라인의 거의 모든 구성이 사전에 설정되어야 한다는 것입니다. 즉, 다른 셰이더로 전환하거나 정점 레이아웃을 약간 변경하려면 그래픽 파이프라인 전체를 다시 생성해야 합니다. 이는 렌더링 작업에 필요한 모든 다양한 조합에 대해 미리 많은 `VkPipeline` 객체를 생성해야 함을 의미합니다. 뷰포트 크기나 클리어 색상과 같은 일부 기본 구성만 동적으로 변경할 수 있습니다. 또한 모든 상태는 명시적으로 기술되어야 합니다. 예를 들어, 기본 색상 혼합 상태 같은 것은 없습니다. + +좋은 소식은, 적시 컴파일(just-in-time compilation) 방식보다는 사전 컴파일(ahead-of-time compilation) 방식에 가깝게 작업하기 때문에, 드라이버에 더 많은 최적화 기회가 있고 런타임 성능이 더 예측 가능하다는 것입니다. 왜냐하면 다른 그래픽 파이프라인으로 전환하는 것과 같은 큰 상태 변경이 매우 명시적으로 이루어지기 때문입니다. + +### 7단계 - 커맨드 풀과 커맨드 버퍼 + +앞서 언급했듯이, 우리가 실행하고자 하는 벌칸의 많은 작업(예: 그리기 작업)은 큐에 제출되어야 합니다. 이러한 작업들은 제출되기 전에 먼저 `VkCommandBuffer`에 기록되어야 합니다. 이 커맨드 버퍼들은 특정 큐 패밀리와 연관된 `VkCommandPool`로부터 할당됩니다. 간단한 삼각형을 그리려면 다음과 같은 작업들을 포함하는 커맨드 버퍼를 기록해야 합니다: + +* 렌더 패스 시작 +* 그래픽 파이프라인 바인딩 +* 정점 3개 그리기 +* 렌더 패스 종료 + +프레임버퍼의 이미지는 스왑 체인이 어떤 특정 이미지를 제공하느냐에 따라 달라지기 때문에, 우리는 가능한 각 이미지에 대해 커맨드 버퍼를 기록하고 그리기 시점에 올바른 것을 선택해야 합니다. 대안은 매 프레임마다 커맨드 버퍼를 다시 기록하는 것인데, 이는 그다지 효율적이지 않습니다. + +### 8단계 - 메인 루프 + +이제 그리기 커맨드들이 커맨드 버퍼에 감싸졌으므로, 메인 루프는 매우 간단합니다. 먼저 `vkAcquireNextImageKHR`로 스왑 체인에서 이미지를 얻습니다. 그런 다음 해당 이미지에 적합한 커맨드 버퍼를 선택하고 `vkQueueSubmit`으로 실행합니다. 마지막으로, `vkQueuePresentKHR`을 사용하여 화면에 표시하기 위해 이미지를 스왑 체인으로 반환합니다. + +큐에 제출된 작업들은 비동기적으로 실행됩니다. 따라서 올바른 실행 순서를 보장하기 위해 세마포어(semaphore)와 같은 동기화 객체를 사용해야 합니다. 그리기 커맨드 버퍼의 실행은 이미지 획득이 완료되기를 기다리도록 설정되어야 합니다. 그렇지 않으면 화면에 표시되기 위해 아직 읽고 있는 이미지에 렌더링을 시작할 수 있습니다. `vkQueuePresentKHR` 호출은 다시 렌더링이 완료되기를 기다려야 하며, 이를 위해 렌더링이 완료된 후 신호를 보내는 두 번째 세마포어를 사용할 것입니다. + +### 요약 + +이 간략한 여정은 첫 번째 삼각형을 그리기 위해 앞으로 해야 할 일에 대한 기본적인 이해를 제공했을 것입니다. 실제 프로그램에는 정점 버퍼 할당, 유니폼 버퍼 생성, 텍스처 이미지 업로드와 같은 더 많은 단계가 포함되며, 이는 후속 장에서 다룰 것입니다. 하지만 벌칸은 그 자체로 학습 곡선이 가파르기 때문에 간단하게 시작하겠습니다. 참고로, 처음에는 정점 버퍼를 사용하는 대신 정점 좌표를 정점 셰이더에 내장하여 약간의 편법을 사용할 것입니다. 이는 정점 버퍼 관리가 먼저 커맨드 버퍼에 대한 어느 정도의 익숙함을 요구하기 때문입니다. + +요약하자면, 첫 번째 삼각형을 그리기 위해 우리는 다음을 수행해야 합니다: + +* `VkInstance` 생성 +* 지원되는 그래픽 카드(`VkPhysicalDevice`) 선택 +* 그리기와 화면 표시를 위한 `VkDevice`와 `VkQueue` 생성 +* 윈도우, 윈도우 서피스, 스왑 체인 생성 +* 스왑 체인 이미지들을 `VkImageView`로 감싸기 +* 렌더 타겟과 사용법을 명시하는 렌더 패스 생성 +* 렌더 패스를 위한 프레임버퍼 생성 +* 그래픽 파이프라인 설정 +* 가능한 모든 스왑 체인 이미지에 대해 그리기 커맨드가 담긴 커맨드 버퍼를 할당하고 기록 +* 이미지를 획득하고, 올바른 그리기 커맨드 버퍼를 제출하고, 이미지를 다시 스왑 체인으로 반환하여 프레임 그리기 + +단계가 많지만, 각 개별 단계의 목적은 앞으로의 장들에서 매우 간단하고 명확하게 설명될 것입니다. 만약 단일 단계가 전체 프로그램과 어떻게 관련되는지 혼란스럽다면, 이 장을 다시 참조해야 합니다. + +## API 개념 + +이 장은 벌칸 API가 더 낮은 수준에서 어떻게 구조화되어 있는지에 대한 간략한 개요로 마무리하겠습니다. + +### 코딩 규칙 + +모든 벌칸 함수, 열거형, 구조체는 LunarG에서 개발한 [벌칸 SDK](https://lunarg.com/vulkan-sdk/)에 포함된 `vulkan.h` 헤더에 정의되어 있습니다. 이 SDK를 설치하는 방법은 다음 장에서 살펴보겠습니다. + +함수는 소문자 `vk` 접두사를, 열거형과 구조체 같은 타입은 `Vk` 접두사를, 열거형 값은 `VK_` 접두사를 가집니다. API는 함수에 매개변수를 제공하기 위해 구조체를 많이 사용합니다. 예를 들어, 객체 생성은 일반적으로 다음 패턴을 따릅니다: + +```c++ +VkXXXCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_XXX_CREATE_INFO; +createInfo.pNext = nullptr; +createInfo.foo = ...; +createInfo.bar = ...; + +VkXXX object; +if (vkCreateXXX(&createInfo, nullptr, &object) != VK_SUCCESS) { + std::cerr << "객체 생성 실패" << std::endl; + return false; +} +``` + +벌칸의 많은 구조체는 `sType` 멤버에 구조체의 타입을 명시적으로 지정하도록 요구합니다. `pNext` 멤버는 확장 구조체를 가리킬 수 있으며, 이 튜토리얼에서는 항상 `nullptr`일 것입니다. 객체를 생성하거나 파괴하는 함수는 `VkAllocationCallbacks` 매개변수를 가지며, 이를 통해 드라이버 메모리를 위한 커스텀 할당자를 사용할 수 있습니다. 이 또한 이 튜토리얼에서는 `nullptr`로 남겨둘 것입니다. + +거의 모든 함수는 `VK_SUCCESS` 또는 오류 코드를 나타내는 `VkResult`를 반환합니다. 명세서는 각 함수가 어떤 오류 코드를 반환할 수 있는지와 그 의미를 설명합니다. + +### 유효성 검사 레이어 + +앞서 언급했듯이, 벌칸은 고성능과 낮은 드라이버 오버헤드를 위해 설계되었습니다. 따라서 기본적으로 매우 제한적인 오류 검사 및 디버깅 기능만 포함합니다. 잘못된 작업을 수행하면 드라이버가 오류 코드를 반환하는 대신 충돌하는 경우가 많으며, 더 나쁜 경우, 여러분의 그래픽 카드에서는 작동하는 것처럼 보이다가 다른 그래픽 카드에서는 완전히 실패할 수 있습니다. + +벌칸은 *유효성 검사 레이어(validation layers)*라는 기능을 통해 광범위한 검사를 활성화할 수 있습니다. 유효성 검사 레이어는 API와 그래픽 드라이버 사이에 삽입될 수 있는 코드 조각으로, 함수 매개변수에 대한 추가 검사 실행이나 메모리 관리 문제 추적과 같은 작업을 수행합니다. 좋은 점은 개발 중에는 이를 활성화했다가 애플리케이션을 출시할 때는 완전히 비활성화하여 오버헤드가 전혀 없게 할 수 있다는 것입니다. 누구나 자신만의 유효성 검사 레이어를 작성할 수 있지만, 이 튜토리얼에서는 LunarG의 벌칸 SDK가 제공하는 표준 유효성 검사 레이어 세트를 사용할 것입니다. 또한 레이어로부터 디버그 메시지를 받기 위해 콜백 함수를 등록해야 합니다. + +벌칸은 모든 작업에 대해 매우 명시적이고 유효성 검사 레이어는 매우 광범위하기 때문에, 화면이 왜 검은색으로 나오는지 알아내는 것이 OpenGL이나 Direct3D에 비해 훨씬 쉬울 수 있습니다. + +코드를 작성하기 전까지 이제 단 한 단계만 남았습니다. 바로 [개발 환경 설정하기](!en/Development_environment)입니다. \ No newline at end of file diff --git a/ko/02_Development_environment.md b/ko/02_Development_environment.md new file mode 100644 index 00000000..ccfd3ba2 --- /dev/null +++ b/ko/02_Development_environment.md @@ -0,0 +1,462 @@ +이 챕터에서는 Vulkan 애플리케이션 개발을 위한 환경을 설정하고 몇 가지 유용한 라이브러리를 설치합니다. 컴파일러를 제외한 우리가 사용할 모든 도구는 Windows, Linux, MacOS와 호환되지만, 설치 단계가 조금씩 다르기 때문에 여기서는 각각 따로 설명합니다. + +## Windows + +Windows에서 개발하신다면, 코드를 컴파일하기 위해 Visual Studio를 사용한다고 가정하겠습니다. C++17을 완벽하게 지원하려면 Visual Studio 2017 또는 2019를 사용해야 합니다. 아래 설명된 단계는 VS 2017을 기준으로 작성되었습니다. + +### Vulkan SDK + +Vulkan 애플리케이션 개발에 필요한 가장 중요한 구성 요소는 SDK입니다. SDK에는 헤더, 표준 유효성 검사 레이어, 디버깅 도구, 그리고 Vulkan 함수를 위한 로더가 포함되어 있습니다. 로더는 런타임에 드라이버에서 함수를 찾아주는 역할을 하며, OpenGL의 GLEW와 유사하다고 생각하시면 됩니다. + +SDK는 [LunarG 웹사이트](https://vulkan.lunarg.com/) 페이지 하단의 버튼을 통해 다운로드할 수 있습니다. 계정을 만들 필요는 없지만, 계정을 만들면 몇 가지 추가적인 문서에 접근할 수 있어 유용할 수 있습니다. + +![](/images/vulkan_sdk_download_buttons.png) + +설치를 진행하고 SDK가 설치된 위치를 잘 기억해두세요. 가장 먼저 할 일은 그래픽 카드와 드라이버가 Vulkan을 제대로 지원하는지 확인하는 것입니다. SDK를 설치한 디렉터리로 이동하여 `Bin` 디렉터리를 열고 `vkcube.exe` 데모를 실행하세요. 다음과 같은 화면이 나타나야 합니다: + +![](/images/cube_demo.png) + +만약 오류 메시지가 표시된다면 드라이버가 최신 버전인지, Vulkan 런타임을 포함하고 있는지, 그리고 그래픽 카드가 지원되는 모델인지 확인하세요. 주요 제조사별 드라이버 링크는 [소개 챕터](!en/Introduction)에서 확인할 수 있습니다. + +이 디렉터리에는 개발에 유용한 또 다른 프로그램이 있습니다. `glslangValidator.exe`와 `glslc.exe` 프로그램은 사람이 읽을 수 있는 [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language) 셰이더를 바이트코드로 컴파일하는 데 사용됩니다. 이 부분은 [셰이더 모듈](!en/Drawing_a_triangle/Graphics_pipeline_basics/Shader_modules) 챕터에서 자세히 다룰 것입니다. `Bin` 디렉터리에는 Vulkan 로더와 유효성 검사 레이어의 바이너리가 포함되어 있으며, `Lib` 디렉터리에는 라이브러리가 들어있습니다. + +마지막으로 `Include` 디렉터리에는 Vulkan 헤더가 있습니다. 다른 파일들도 자유롭게 둘러보셔도 좋지만, 이 튜토리얼에서는 필요하지 않습니다. + +### GLFW + +앞서 언급했듯이 Vulkan 자체는 플랫폼에 독립적인 API이며 렌더링 결과를 표시할 창을 만드는 도구는 포함하지 않습니다. Vulkan의 크로스플랫폼 이점을 활용하고 Win32의 끔찍함을 피하기 위해, 우리는 [GLFW 라이브러리](http://www.glfw.org/)를 사용하여 창을 만들 것입니다. GLFW는 Windows, Linux, MacOS를 지원합니다. [SDL](https://www.libsdl.org/)과 같은 다른 라이브러리도 있지만, GLFW의 장점은 단순히 창 생성뿐만 아니라 다른 플랫폼별 Vulkan 관련 사항들도 추상화해준다는 점입니다. + +[공식 웹사이트](http://www.glfw.org/download.html)에서 최신 버전의 GLFW를 찾을 수 있습니다. 이 튜토리얼에서는 64비트 바이너리를 사용하지만, 물론 32비트 모드로 빌드할 수도 있습니다. 그럴 경우 Vulkan SDK의 `Lib` 대신 `Lib32` 디렉터리에 있는 바이너리와 링크해야 합니다. 다운로드 후, 압축 파일을 적절한 위치에 푸세요. 저는 문서 폴더 아래의 Visual Studio 디렉터리에 `Libraries`라는 디렉터리를 만들었습니다. + +![](/images/glfw_directory.png) + +### GLM + +DirectX 12와 달리 Vulkan은 선형대수 연산을 위한 라이브러리를 포함하지 않으므로, 직접 다운로드해야 합니다. [GLM](http://glm.g-truc.net/)은 그래픽 API와 함께 사용하도록 설계된 멋진 라이브러리로, OpenGL에서도 흔히 사용됩니다. + +GLM은 헤더 전용 라이브러리이므로, [최신 버전](https://github.com/g-truc/glm/releases)을 다운로드하여 적절한 위치에 저장하기만 하면 됩니다. 이제 다음과 비슷한 디렉터리 구조를 갖게 될 것입니다: + +![](/images/library_directory.png) + +### Visual Studio 설정하기 + +이제 모든 종속 요소를 설치했으니, Vulkan을 위한 기본 Visual Studio 프로젝트를 설정하고 모든 것이 제대로 작동하는지 확인하기 위해 간단한 코드를 작성해 보겠습니다. + +Visual Studio를 시작하고 `Windows 데스크톱 마법사` 프로젝트를 새로 만드세요. 이름을 입력하고 `확인`을 누릅니다. + +![](/images/vs_new_cpp_project.png) + +디버그 메시지를 출력할 공간이 있도록 애플리케이션 종류로 `콘솔 애플리케이션(.exe)`을 선택하고, Visual Studio가 상용구 코드를 추가하지 않도록 `빈 프로젝트`를 체크하세요. + +![](/images/vs_application_settings.png) + +`확인`을 눌러 프로젝트를 만들고 C++ 소스 파일을 추가합니다. 이미 이 과정은 알고 계시겠지만, 완전성을 위해 단계를 포함했습니다. + +![](/images/vs_new_item.png) + +![](/images/vs_new_source_file.png) + +이제 파일에 다음 코드를 추가하세요. 지금 당장 이 코드를 이해하려고 애쓰지 마세요. 우리는 단지 Vulkan 애플리케이션을 컴파일하고 실행할 수 있는지 확인하는 중입니다. 다음 챕터부터 처음부터 다시 시작할 것입니다. + +```c++ +#define GLFW_INCLUDE_VULKAN +#include + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include +#include + +#include + +int main() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan window", nullptr, nullptr); + + uint32_t extensionCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); + + std::cout << extensionCount << " extensions supported\n"; + + glm::mat4 matrix; + glm::vec4 vec; + auto test = matrix * vec; + + while(!glfwWindowShouldClose(window)) { + glfwPollEvents(); + } + + glfwDestroyWindow(window); + + glfwTerminate(); + + return 0; +} +``` + +이제 오류를 없애기 위해 프로젝트를 구성해 보겠습니다. 프로젝트 속성 대화상자를 열고, 대부분의 설정이 `Debug`와 `Release` 모드 모두에 적용되므로 `모든 구성`이 선택되었는지 확인하세요. + +![](/images/vs_open_project_properties.png) + +![](/images/vs_all_configs.png) + +`C/C++ -> 일반 -> 추가 포함 디렉터리`로 이동하여 드롭다운 상자에서 `<편집...>`을 누르세요. + +![](/images/vs_cpp_general.png) + +Vulkan, GLFW, GLM의 헤더 디렉터리를 추가합니다: + +![](/images/vs_include_dirs.png) + +다음으로, `링커 -> 일반` 아래의 라이브러리 디렉터리 편집기를 엽니다: + +![](/images/vs_link_settings.png) + +그리고 Vulkan과 GLFW의 오브젝트 파일 위치를 추가합니다: + +![](/images/vs_link_dirs.png) + +`링커 -> 입력`으로 이동하여 `추가 종속성` 드롭다운 상자에서 `<편집...>`을 누르세요. + +![](/images/vs_link_input.png) + +Vulkan과 GLFW 오브젝트 파일의 이름을 입력합니다: + +![](/images/vs_dependencies.png) + +그리고 마지막으로 컴파일러가 C++17 기능을 지원하도록 변경합니다: + +![](/images/vs_cpp17.png) + +이제 프로젝트 속성 대화상자를 닫아도 됩니다. 모든 것을 올바르게 설정했다면 코드에서 더 이상 오류가 강조 표시되지 않을 것입니다. + +마지막으로, 실제로 64비트 모드로 컴파일하고 있는지 확인하세요: + +![](/images/vs_build_mode.png) + +`F5`를 눌러 프로젝트를 컴파일하고 실행하면 다음과 같이 명령 프롬프트와 창이 나타날 것입니다: + +![](/images/vs_test_window.png) + +확장 기능(extension)의 수가 0이 아니어야 합니다. 축하합니다, 이제 [Vulkan과 함께할](!en/Drawing_a_triangle/Setup/Base_code) 모든 준비가 끝났습니다! + +## Linux + +이 설명은 Ubuntu, Fedora, Arch Linux 사용자를 대상으로 하지만, 자신의 배포판에 맞는 패키지 매니저 명령어로 변경하여 따라 할 수 있습니다. C++17을 지원하는 컴파일러(GCC 7+ 또는 Clang 5+)가 필요하며, `make`도 필요합니다. + +### Vulkan 패키지 + +Linux에서 Vulkan 애플리케이션을 개발하는 데 필요한 가장 중요한 구성 요소는 Vulkan 로더, 유효성 검사 레이어, 그리고 여러분의 컴퓨터가 Vulkan을 지원하는지 테스트할 몇 가지 커맨드 라인 유틸리티입니다: + +* `sudo apt install vulkan-tools` 또는 `sudo dnf install vulkan-tools`: 커맨드 라인 유틸리티, 특히 `vulkaninfo`와 `vkcube`를 설치합니다. 이들을 실행하여 컴퓨터가 Vulkan을 지원하는지 확인하세요. +* `sudo apt install libvulkan-dev` 또는 `sudo dnf install vulkan-loader-devel`: Vulkan 로더를 설치합니다. 로더는 런타임에 드라이버에서 함수를 찾아주는 역할을 하며, OpenGL의 GLEW와 유사하다고 생각하시면 됩니다. +* `sudo apt install vulkan-validationlayers spirv-tools` 또는 `sudo dnf install mesa-vulkan-drivers vulkan-validation-layers-devel`: 표준 유효성 검사 레이어와 필요한 SPIR-V 도구를 설치합니다. 이는 Vulkan 애플리케이션을 디버깅할 때 매우 중요하며, 다음 챕터에서 다룰 것입니다. + +Arch Linux에서는 `sudo pacman -S vulkan-devel`을 실행하여 위의 모든 도구를 설치할 수 있습니다. + +설치가 성공적으로 완료되었다면 Vulkan 관련 부분은 모두 준비된 것입니다. `vkcube`를 실행하여 다음과 같은 창이 나타나는지 확인하는 것을 잊지 마세요: + +![](/images/cube_demo_nowindow.png) + +만약 오류 메시지가 표시된다면 드라이버가 최신 버전인지, Vulkan 런타임을 포함하고 있는지, 그리고 그래픽 카드가 지원되는 모델인지 확인하세요. 주요 제조사별 드라이버 링크는 [소개 챕터](!en/Introduction)에서 확인할 수 있습니다. + +### X Window System and XFree86-VidModeExtension +이 라이브러리들이 시스템에 없을 수 있습니다. 없다면 다음 명령어를 사용해 설치할 수 있습니다: +* `sudo apt install libxxf86vm-dev` 또는 `dnf install libXxf86vm-devel`: XFree86-VidModeExtension에 대한 인터페이스를 제공합니다. +* `sudo apt install libxi-dev` 또는 `dnf install libXi-devel`: XINPUT 확장에 대한 X Window System 클라이언트 인터페이스를 제공합니다. + +### GLFW + +앞서 언급했듯이 Vulkan 자체는 플랫폼에 독립적인 API이며 렌더링 결과를 표시할 창을 만드는 도구는 포함하지 않습니다. Vulkan의 크로스플랫폼 이점을 활용하고 X11의 끔찍함을 피하기 위해, 우리는 [GLFW 라이브러리](http://www.glfw.org/)를 사용하여 창을 만들 것입니다. GLFW는 Windows, Linux, MacOS를 지원합니다. [SDL](https://www.libsdl.org/)과 같은 다른 라이브러리도 있지만, GLFW의 장점은 단순히 창 생성뿐만 아니라 다른 플랫폼별 Vulkan 관련 사항들도 추상화해준다는 점입니다. + +다음 명령어를 통해 GLFW를 설치할 것입니다: + +```bash +sudo apt install libglfw3-dev +``` +또는 +```bash +sudo dnf install glfw-devel +``` +또는 +```bash +sudo pacman -S glfw +``` + +### GLM + +DirectX 12와 달리 Vulkan은 선형대수 연산을 위한 라이브러리를 포함하지 않으므로, 직접 다운로드해야 합니다. [GLM](http://glm.g-truc.net/)은 그래픽 API와 함께 사용하도록 설계된 멋진 라이브러리로, OpenGL에서도 흔히 사용됩니다. + +이것은 `libglm-dev` 또는 `glm-devel` 패키지로부터 설치할 수 있는 헤더 전용 라이브러리입니다: + +```bash +sudo apt install libglm-dev +``` +또는 +```bash +sudo dnf install glm-devel +``` +또는 +```bash +sudo pacman -S glm +``` + +### 셰이더 컴파일러 + +이제 거의 모든 것이 준비되었지만, 사람이 읽을 수 있는 [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language)을 바이트코드로 컴파일할 프로그램이 필요합니다. + +널리 사용되는 두 셰이더 컴파일러는 Khronos Group의 `glslangValidator`와 Google의 `glslc`입니다. 후자는 GCC 및 Clang과 유사한 사용법을 가지고 있으므로, 이것을 사용하겠습니다: Ubuntu에서는 Google의 [비공식 바이너리](https://github.com/google/shaderc/blob/main/downloads.md)를 다운로드하고 `glslc`를 `/usr/local/bin`에 복사하세요. 권한에 따라 `sudo`가 필요할 수 있습니다. Fedora에서는 `sudo dnf install glslc`를, Arch Linux에서는 `sudo pacman -S shaderc`를 실행하세요. 테스트하려면 `glslc`를 실행해보세요. 컴파일할 셰이더를 전달하지 않았다고 올바르게 불평할 것입니다: + +`glslc: error: no input files` + +`glslc`에 대해서는 [셰이더 모듈](!en/Drawing_a_triangle/Graphics_pipeline_basics/Shader_modules) 챕터에서 자세히 다룰 것입니다. + +### Makefile 프로젝트 설정하기 + +이제 모든 종속 요소를 설치했으니, Vulkan을 위한 기본 Makefile 프로젝트를 설정하고 모든 것이 제대로 작동하는지 확인하기 위해 간단한 코드를 작성해 보겠습니다. + +`VulkanTest`와 같은 이름으로 적절한 위치에 새 디렉터리를 만드세요. `main.cpp`라는 소스 파일을 만들고 다음 코드를 삽입하세요. 지금 당장 이 코드를 이해하려고 애쓰지 마세요. 우리는 단지 Vulkan 애플리케이션을 컴파일하고 실행할 수 있는지 확인하는 중입니다. 다음 챕터부터 처음부터 다시 시작할 것입니다. + +```c++ +#define GLFW_INCLUDE_VULKAN +#include + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include +#include + +#include + +int main() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan window", nullptr, nullptr); + + uint32_t extensionCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); + + std::cout << extensionCount << " extensions supported\n"; + + glm::mat4 matrix; + glm::vec4 vec; + auto test = matrix * vec; + + while(!glfwWindowShouldClose(window)) { + glfwPollEvents(); + } + + glfwDestroyWindow(window); + + glfwTerminate(); + + return 0; +} +``` + +다음으로, 이 기본 Vulkan 코드를 컴파일하고 실행하기 위한 Makefile을 작성하겠습니다. `Makefile`이라는 이름의 빈 파일을 만드세요. 변수나 규칙과 같은 Makefile의 기본 개념에 대해서는 이미 어느 정도 경험이 있다고 가정하겠습니다. 만약 아니라면, [이 튜토리얼](https://makefiletutorial.com/)을 통해 빠르게 익힐 수 있습니다. + +먼저 파일의 나머지 부분을 단순화하기 위해 몇 가지 변수를 정의하겠습니다. 기본 컴파일러 플래그를 지정할 `CFLAGS` 변수를 정의합니다: + +```make +CFLAGS = -std=c++17 -O2 +``` + +최신 C++(`-std=c++17`)을 사용할 것이고, 최적화 수준은 O2로 설정할 것입니다. `-O2`를 제거하면 프로그램을 더 빨리 컴파일할 수 있지만, 릴리스 빌드에서는 다시 추가하는 것을 기억해야 합니다. + +비슷하게, `LDFLAGS` 변수에 링커 플래그를 정의합니다: + +```make +LDFLAGS = -lglfw -lvulkan -ldl -lpthread -lX11 -lXxf86vm -lXrandr -lXi +``` + +`-lglfw` 플래그는 GLFW를 위한 것이고, `-lvulkan`은 Vulkan 함수 로더와 링크하며, 나머지 플래그는 GLFW가 필요로 하는 저수준 시스템 라이브러리들입니다. 나머지 플래그는 GLFW 자체의 종속성인 스레딩 및 창 관리 라이브러리입니다. + +`Xxf86vm`과 `Xi` 라이브러리가 아직 시스템에 설치되어 있지 않을 수 있습니다. 다음 패키지에서 찾을 수 있습니다: + +```bash +sudo apt install libxxf86vm-dev libxi-dev +``` +또는 +```bash +sudo dnf install libXi-devel libXxf86vm-devel +``` +또는 +```bash +sudo pacman -S libxi libxxf86vm +``` + +이제 `VulkanTest`를 컴파일하는 규칙을 지정하는 것은 간단합니다. 들여쓰기는 공백 대신 탭을 사용해야 합니다. + +```make +VulkanTest: main.cpp + g++ $(CFLAGS) -o VulkanTest main.cpp $(LDFLAGS) +``` + +Makefile을 저장하고 `main.cpp`와 `Makefile`이 있는 디렉터리에서 `make`를 실행하여 이 규칙이 작동하는지 확인하세요. `VulkanTest` 실행 파일이 생성되어야 합니다. + +이제 `test`와 `clean`이라는 두 개의 규칙을 더 정의할 것입니다. 전자는 실행 파일을 실행하고, 후자는 빌드된 실행 파일을 제거합니다: + +```make +.PHONY: test clean + +test: VulkanTest + ./VulkanTest + +clean: + rm -f VulkanTest +``` + +`make test`를 실행하면 프로그램이 성공적으로 실행되고 Vulkan 확장 기능의 수가 표시될 것입니다. 빈 창을 닫으면 애플리케이션이 성공 반환 코드(`0`)로 종료되어야 합니다. 이제 다음과 같은 완전한 Makefile을 갖게 되었을 것입니다: + +```make +CFLAGS = -std=c++17 -O2 +LDFLAGS = -lglfw -lvulkan -ldl -lpthread -lX11 -lXxf86vm -lXrandr -lXi + +VulkanTest: main.cpp + g++ $(CFLAGS) -o VulkanTest main.cpp $(LDFLAGS) + +.PHONY: test clean + +test: VulkanTest + ./VulkanTest + +clean: + rm -f VulkanTest +``` + +이제 이 디렉터리를 Vulkan 프로젝트의 템플릿으로 사용할 수 있습니다. 복사해서 `HelloTriangle` 같은 이름으로 바꾸고 `main.cpp`의 모든 코드를 지우세요. + +이제 [진정한 모험](!en/Drawing_a_triangle/Setup/Base_code)을 떠날 준비가 모두 끝났습니다. + +## MacOS + +이 설명은 Xcode와 [Homebrew 패키지 매니저](https://brew.sh/)를 사용한다고 가정합니다. 또한, 최소 MacOS 버전 10.11이 필요하며, 사용하시는 기기가 [Metal API](https://en.wikipedia.org/wiki/Metal_(API)#Supported_GPUs)를 지원해야 한다는 점을 명심하세요. + +### Vulkan SDK + +Vulkan 애플리케이션 개발에 필요한 가장 중요한 구성 요소는 SDK입니다. SDK에는 헤더, 표준 유효성 검사 레이어, 디버깅 도구, 그리고 Vulkan 함수를 위한 로더가 포함되어 있습니다. 로더는 런타임에 드라이버에서 함수를 찾아주는 역할을 하며, OpenGL의 GLEW와 유사하다고 생각하시면 됩니다. + +SDK는 [LunarG 웹사이트](https://vulkan.lunarg.com/) 페이지 하단의 버튼을 통해 다운로드할 수 있습니다. 계정을 만들 필요는 없지만, 계정을 만들면 몇 가지 추가적인 문서에 접근할 수 있어 유용할 수 있습니다. + +![](/images/vulkan_sdk_download_buttons.png) + +MacOS용 SDK 버전은 내부적으로 [MoltenVK](https://moltengl.com/)를 사용합니다. MacOS는 Vulkan을 네이티브로 지원하지 않으므로, MoltenVK는 Vulkan API 호출을 Apple의 Metal 그래픽 프레임워크로 변환하는 레이어 역할을 합니다. 이를 통해 Apple의 Metal 프레임워크가 제공하는 디버깅 및 성능 이점을 활용할 수 있습니다. + +다운로드 후, 내용물을 원하는 폴더에 압축 해제하세요 (Xcode에서 프로젝트를 만들 때 이 경로를 참조해야 하므로 잘 기억해두세요). 압축 해제한 폴더 안의 `Applications` 폴더에 SDK를 사용하는 몇 가지 데모를 실행할 수 있는 실행 파일들이 있습니다. `vkcube` 실행 파일을 실행하면 다음과 같은 화면이 나타날 것입니다: + +![](/images/cube_demo_mac.png) + +### GLFW + +앞서 언급했듯이 Vulkan 자체는 플랫폼에 독립적인 API이며 렌더링 결과를 표시할 창을 만드는 도구는 포함하지 않습니다. 우리는 [GLFW 라이브러리](http://www.glfw.org/)를 사용하여 창을 만들 것입니다. GLFW는 Windows, Linux, MacOS를 지원합니다. [SDL](https://www.libsdl.org/)과 같은 다른 라이브러리도 있지만, GLFW의 장점은 단순히 창 생성뿐만 아니라 다른 플랫폼별 Vulkan 관련 사항들도 추상화해준다는 점입니다. + +MacOS에 GLFW를 설치하기 위해 Homebrew 패키지 매니저를 사용하여 `glfw` 패키지를 설치합니다: + +```bash +brew install glfw +``` + +### GLM + +Vulkan은 선형대수 연산을 위한 라이브러리를 포함하지 않으므로, 직접 다운로드해야 합니다. [GLM](http://glm.g-truc.net/)은 그래픽 API와 함께 사용하도록 설계된 멋진 라이브러리로, OpenGL에서도 흔히 사용됩니다. + +이것은 `glm` 패키지로부터 설치할 수 있는 헤더 전용 라이브러리입니다: + +```bash +brew install glm +``` + +### Xcode 설정하기 + +이제 모든 종속 요소를 설치했으니, Vulkan을 위한 기본 Xcode 프로젝트를 설정해 보겠습니다. 여기 설명의 대부분은 모든 종속성을 프로젝트에 연결하기 위한 '배관' 작업과 같습니다. 또한, 다음 설명에서 `vulkansdk` 폴더를 언급할 때는 Vulkan SDK를 압축 해제한 폴더를 가리킨다는 점을 기억하세요. + +Xcode를 시작하고 새 Xcode 프로젝트를 만듭니다. 열리는 창에서 Application > Command Line Tool을 선택하세요. + +![](/images/xcode_new_project.png) + +`Next`를 선택하고, 프로젝트 이름을 작성한 후 `Language`로 `C++`를 선택하세요. + +![](/images/xcode_new_project_2.png) + +`Next`를 누르면 프로젝트가 생성됩니다. 이제 생성된 `main.cpp` 파일의 코드를 다음 코드로 변경합시다: + +```c++ +#define GLFW_INCLUDE_VULKAN +#include + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include +#include + +#include + +int main() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan window", nullptr, nullptr); + + uint32_t extensionCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); + + std::cout << extensionCount << " extensions supported\n"; + + glm::mat4 matrix; + glm::vec4 vec; + auto test = matrix * vec; + + while(!glfwWindowShouldClose(window)) { + glfwPollEvents(); + } + + glfwDestroyWindow(window); + + glfwTerminate(); + + return 0; +} +``` + +아직 이 코드가 무엇을 하는지 전부 이해할 필요는 없습니다. 우리는 단지 모든 것이 작동하는지 확인하기 위해 몇 가지 API 호출을 설정하는 중입니다. + +Xcode는 이미 찾을 수 없는 라이브러리 같은 오류들을 보여주고 있을 것입니다. 이제 그 오류들을 없애기 위해 프로젝트 설정을 시작하겠습니다. *프로젝트 탐색기(Project Navigator)* 패널에서 프로젝트를 선택하세요. *Build Settings* 탭을 연 다음: + +* **Header Search Paths** 필드를 찾아 `/usr/local/include` (Homebrew가 헤더를 설치하는 위치이므로 glm과 glfw3 헤더 파일이 있어야 합니다)와 Vulkan 헤더를 위한 `vulkansdk/macOS/include` 링크를 추가하세요. +* **Library Search Paths** 필드를 찾아 `/usr/local/lib` (마찬가지로 Homebrew가 라이브러리를 설치하는 위치이므로 glm과 glfw3 라이브러리 파일이 있어야 합니다)와 `vulkansdk/macOS/lib` 링크를 추가하세요. + +다음과 같이 보일 것입니다 (물론, 파일을 어디에 두었느냐에 따라 경로는 달라집니다): + +![](/images/xcode_paths.png) + +이제 *Build Phases* 탭의 **Link Binary With Libraries**에 `glfw3`와 `vulkan` 프레임워크를 모두 추가할 것입니다. 작업을 쉽게 하기 위해 동적 라이브러리를 프로젝트에 추가할 것입니다 (정적 프레임워크를 사용하고 싶다면 해당 라이브러리의 문서를 확인하세요). + +* glfw의 경우 `/usr/local/lib` 폴더를 열면 `libglfw.3.x.dylib`과 같은 이름의 파일이 있을 것입니다 ("x"는 라이브러리의 버전 번호이며, Homebrew에서 패키지를 다운로드한 시점에 따라 다를 수 있습니다). 이 파일을 Xcode의 Linked Frameworks and Libraries 탭으로 드래그 앤 드롭하세요. +* vulkan의 경우 `vulkansdk/macOS/lib`로 이동하세요. `libvulkan.1.dylib`와 `libvulkan.1.x.xx.dylib` 두 파일에 대해 동일한 작업을 수행하세요 ("x"는 다운로드한 SDK의 버전 번호입니다). + +이 라이브러리들을 추가한 후, 같은 탭의 **Copy Files**에서 `Destination`을 "Frameworks"로 변경하고, 하위 경로는 비우고 "Copy only when installing"을 선택 해제하세요. "+" 기호를 클릭하고 이 세 프레임워크를 여기에 모두 추가하세요. + +Xcode 설정은 다음과 같을 것입니다: + +![](/images/xcode_frameworks.png) + +마지막으로 설정해야 할 것은 몇 가지 환경 변수입니다. Xcode 툴바에서 `Product` > `Scheme` > `Edit Scheme...`으로 이동하여, `Arguments` 탭에 다음 두 환경 변수를 추가하세요: + +* VK_ICD_FILENAMES = `vulkansdk/macOS/share/vulkan/icd.d/MoltenVK_icd.json` +* VK_LAYER_PATH = `vulkansdk/macOS/share/vulkan/explicit_layer.d` + +다음과 같이 보일 것입니다: + +![](/images/xcode_variables.png) + +드디어 모든 준비가 끝났습니다! 이제 프로젝트를 실행하면 (선택한 구성에 따라 빌드 구성을 Debug 또는 Release로 설정하는 것을 잊지 마세요) 다음과 같은 화면이 나타날 것입니다: + +![](/images/xcode_output.png) + +확장 기능(extension)의 수가 0이 아니어야 합니다. 다른 로그들은 라이브러리에서 나온 것이며, 설정에 따라 다른 메시지를 받을 수도 있습니다. + +이제 [진짜배기](!en/Drawing_a_triangle/Setup/Base_code)를 위한 모든 준비가 끝났습니다. \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/00_Setup/00_Base_code.md b/ko/03_Drawing_a_triangle/00_Setup/00_Base_code.md new file mode 100644 index 00000000..df26c6ac --- /dev/null +++ b/ko/03_Drawing_a_triangle/00_Setup/00_Base_code.md @@ -0,0 +1,217 @@ +## General structure + +In the previous chapter you've created a Vulkan project with all of the proper +configuration and tested it with the sample code. In this chapter we're starting +from scratch with the following code: + +```c++ +#include + +#include +#include +#include + +class HelloTriangleApplication { +public: + void run() { + initVulkan(); + mainLoop(); + cleanup(); + } + +private: + void initVulkan() { + + } + + void mainLoop() { + + } + + void cleanup() { + + } +}; + +int main() { + HelloTriangleApplication app; + + try { + app.run(); + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} +``` + +We first include the Vulkan header from the LunarG SDK, which provides the +functions, structures and enumerations. The `stdexcept` and `iostream` headers +are included for reporting and propagating errors. The `cstdlib` +header provides the `EXIT_SUCCESS` and `EXIT_FAILURE` macros. + +The program itself is wrapped into a class where we'll store the Vulkan objects +as private class members and add functions to initiate each of them, which will +be called from the `initVulkan` function. Once everything has been prepared, we +enter the main loop to start rendering frames. We'll fill in the `mainLoop` +function to include a loop that iterates until the window is closed in a moment. +Once the window is closed and `mainLoop` returns, we'll make sure to deallocate +the resources we've used in the `cleanup` function. + +If any kind of fatal error occurs during execution then we'll throw a +`std::runtime_error` exception with a descriptive message, which will propagate +back to the `main` function and be printed to the command prompt. To handle +a variety of standard exception types as well, we catch the more general `std::exception`. One example of an error that we will deal with soon is finding +out that a certain required extension is not supported. + +Roughly every chapter that follows after this one will add one new function that +will be called from `initVulkan` and one or more new Vulkan objects to the +private class members that need to be freed at the end in `cleanup`. + +## Resource management + +Just like each chunk of memory allocated with `malloc` requires a call to +`free`, every Vulkan object that we create needs to be explicitly destroyed when +we no longer need it. In C++ it is possible to perform automatic resource +management using [RAII](https://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization) +or smart pointers provided in the `` header. However, I've chosen to be +explicit about allocation and deallocation of Vulkan objects in this tutorial. +After all, Vulkan's niche is to be explicit about every operation to avoid +mistakes, so it's good to be explicit about the lifetime of objects to learn how +the API works. + +After following this tutorial, you could implement automatic resource management +by writing C++ classes that acquire Vulkan objects in their constructor and +release them in their destructor, or by providing a custom deleter to either +`std::unique_ptr` or `std::shared_ptr`, depending on your ownership requirements. +RAII is the recommended model for larger Vulkan programs, but +for learning purposes it's always good to know what's going on behind the +scenes. + +Vulkan objects are either created directly with functions like `vkCreateXXX`, or +allocated through another object with functions like `vkAllocateXXX`. After +making sure that an object is no longer used anywhere, you need to destroy it +with the counterparts `vkDestroyXXX` and `vkFreeXXX`. The parameters for these +functions generally vary for different types of objects, but there is one +parameter that they all share: `pAllocator`. This is an optional parameter that +allows you to specify callbacks for a custom memory allocator. We will ignore +this parameter in the tutorial and always pass `nullptr` as argument. + +## Integrating GLFW + +Vulkan works perfectly fine without creating a window if you want to use it for +off-screen rendering, but it's a lot more exciting to actually show something! +First replace the `#include ` line with + +```c++ +#define GLFW_INCLUDE_VULKAN +#include +``` + +That way GLFW will include its own definitions and automatically load the Vulkan +header with it. Add a `initWindow` function and add a call to it from the `run` +function before the other calls. We'll use that function to initialize GLFW and +create a window. + +```c++ +void run() { + initWindow(); + initVulkan(); + mainLoop(); + cleanup(); +} + +private: + void initWindow() { + + } +``` + +The very first call in `initWindow` should be `glfwInit()`, which initializes +the GLFW library. Because GLFW was originally designed to create an OpenGL +context, we need to tell it to not create an OpenGL context with a subsequent +call: + +```c++ +glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); +``` + +Because handling resized windows takes special care that we'll look into later, +disable it for now with another window hint call: + +```c++ +glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); +``` + +All that's left now is creating the actual window. Add a `GLFWwindow* window;` +private class member to store a reference to it and initialize the window with: + +```c++ +window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr); +``` + +The first three parameters specify the width, height and title of the window. +The fourth parameter allows you to optionally specify a monitor to open the +window on and the last parameter is only relevant to OpenGL. + +It's a good idea to use constants instead of hardcoded width and height numbers +because we'll be referring to these values a couple of times in the future. I've +added the following lines above the `HelloTriangleApplication` class definition: + +```c++ +const uint32_t WIDTH = 800; +const uint32_t HEIGHT = 600; +``` + +and replaced the window creation call with + +```c++ +window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); +``` + +You should now have a `initWindow` function that looks like this: + +```c++ +void initWindow() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + + window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); +} +``` + +To keep the application running until either an error occurs or the window is +closed, we need to add an event loop to the `mainLoop` function as follows: + +```c++ +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + } +} +``` + +This code should be fairly self-explanatory. It loops and checks for events like +pressing the X button until the window has been closed by the user. This is also +the loop where we'll later call a function to render a single frame. + +Once the window is closed, we need to clean up resources by destroying it and +terminating GLFW itself. This will be our first `cleanup` code: + +```c++ +void cleanup() { + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +When you run the program now you should see a window titled `Vulkan` show up +until the application is terminated by closing the window. Now that we have the +skeleton for the Vulkan application, let's [create the first Vulkan object](!en/Drawing_a_triangle/Setup/Instance)! + +[C++ code](/code/00_base_code.cpp) diff --git a/ko/03_Drawing_a_triangle/00_Setup/01_Instance.md b/ko/03_Drawing_a_triangle/00_Setup/01_Instance.md new file mode 100644 index 00000000..d9744a1c --- /dev/null +++ b/ko/03_Drawing_a_triangle/00_Setup/01_Instance.md @@ -0,0 +1,221 @@ +## Creating an instance + +The very first thing you need to do is initialize the Vulkan library by creating +an *instance*. The instance is the connection between your application and the +Vulkan library and creating it involves specifying some details about your +application to the driver. + +Start by adding a `createInstance` function and invoking it in the +`initVulkan` function. + +```c++ +void initVulkan() { + createInstance(); +} +``` + +Additionally add a data member to hold the handle to the instance: + +```c++ +private: +VkInstance instance; +``` + +Now, to create an instance we'll first have to fill in a struct with some +information about our application. This data is technically optional, but it may +provide some useful information to the driver in order to optimize our specific +application (e.g. because it uses a well-known graphics engine with +certain special behavior). This struct is called `VkApplicationInfo`: + +```c++ +void createInstance() { + VkApplicationInfo appInfo{}; + appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + appInfo.pApplicationName = "Hello Triangle"; + appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.pEngineName = "No Engine"; + appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.apiVersion = VK_API_VERSION_1_0; +} +``` + +As mentioned before, many structs in Vulkan require you to explicitly specify +the type in the `sType` member. This is also one of the many structs with a +`pNext` member that can point to extension information in the future. We're +using value initialization here to leave it as `nullptr`. + +A lot of information in Vulkan is passed through structs instead of function +parameters and we'll have to fill in one more struct to provide sufficient +information for creating an instance. This next struct is not optional and tells +the Vulkan driver which global extensions and validation layers we want to use. +Global here means that they apply to the entire program and not a specific +device, which will become clear in the next few chapters. + +```c++ +VkInstanceCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; +createInfo.pApplicationInfo = &appInfo; +``` + +The first two parameters are straightforward. The next two layers specify the +desired global extensions. As mentioned in the overview chapter, Vulkan is a +platform agnostic API, which means that you need an extension to interface with +the window system. GLFW has a handy built-in function that returns the +extension(s) it needs to do that which we can pass to the struct: + +```c++ +uint32_t glfwExtensionCount = 0; +const char** glfwExtensions; + +glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + +createInfo.enabledExtensionCount = glfwExtensionCount; +createInfo.ppEnabledExtensionNames = glfwExtensions; +``` + +The last two members of the struct determine the global validation layers to +enable. We'll talk about these more in-depth in the next chapter, so just leave +these empty for now. + +```c++ +createInfo.enabledLayerCount = 0; +``` + +We've now specified everything Vulkan needs to create an instance and we can +finally issue the `vkCreateInstance` call: + +```c++ +VkResult result = vkCreateInstance(&createInfo, nullptr, &instance); +``` + +As you'll see, the general pattern that object creation function parameters in +Vulkan follow is: + +* Pointer to struct with creation info +* Pointer to custom allocator callbacks, always `nullptr` in this tutorial +* Pointer to the variable that stores the handle to the new object + +If everything went well then the handle to the instance was stored in the +`VkInstance` class member. Nearly all Vulkan functions return a value of type +`VkResult` that is either `VK_SUCCESS` or an error code. To check if the +instance was created successfully, we don't need to store the result and can +just use a check for the success value instead: + +```c++ +if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); +} +``` + +Now run the program to make sure that the instance is created successfully. + +## Encountered VK_ERROR_INCOMPATIBLE_DRIVER: +If using MacOS with the latest MoltenVK sdk, you may get `VK_ERROR_INCOMPATIBLE_DRIVER` +returned from `vkCreateInstance`. According to the [Getting Start Notes](https://vulkan.lunarg.com/doc/sdk/1.3.216.0/mac/getting_started.html). Beginning with the 1.3.216 Vulkan SDK, the `VK_KHR_PORTABILITY_subset` +extension is mandatory. + +To get over this error, first add the `VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR` bit +to `VkInstanceCreateInfo` struct's flags, then add `VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME` +to instance enabled extension list. + +Typically the code could be like this: +```c++ +... + +std::vector requiredExtensions; + +for(uint32_t i = 0; i < glfwExtensionCount; i++) { + requiredExtensions.emplace_back(glfwExtensions[i]); +} + +requiredExtensions.emplace_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + +createInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + +createInfo.enabledExtensionCount = (uint32_t) requiredExtensions.size(); +createInfo.ppEnabledExtensionNames = requiredExtensions.data(); + +if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); +} +``` + +## Checking for extension support + +If you look at the `vkCreateInstance` documentation then you'll see that one of +the possible error codes is `VK_ERROR_EXTENSION_NOT_PRESENT`. We could simply +specify the extensions we require and terminate if that error code comes back. +That makes sense for essential extensions like the window system interface, but +what if we want to check for optional functionality? + +To retrieve a list of supported extensions before creating an instance, there's +the `vkEnumerateInstanceExtensionProperties` function. It takes a pointer to a +variable that stores the number of extensions and an array of +`VkExtensionProperties` to store details of the extensions. It also takes an +optional first parameter that allows us to filter extensions by a specific +validation layer, which we'll ignore for now. + +To allocate an array to hold the extension details we first need to know how +many there are. You can request just the number of extensions by leaving the +latter parameter empty: + +```c++ +uint32_t extensionCount = 0; +vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); +``` + +Now allocate an array to hold the extension details (`include `): + +```c++ +std::vector extensions(extensionCount); +``` + +Finally we can query the extension details: + +```c++ +vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data()); +``` + +Each `VkExtensionProperties` struct contains the name and version of an +extension. We can list them with a simple for loop (`\t` is a tab for +indentation): + +```c++ +std::cout << "available extensions:\n"; + +for (const auto& extension : extensions) { + std::cout << '\t' << extension.extensionName << '\n'; +} +``` + +You can add this code to the `createInstance` function if you'd like to provide +some details about the Vulkan support. As a challenge, try to create a function +that checks if all of the extensions returned by +`glfwGetRequiredInstanceExtensions` are included in the supported extensions +list. + +## Cleaning up + +The `VkInstance` should be only destroyed right before the program exits. It can +be destroyed in `cleanup` with the `vkDestroyInstance` function: + +```c++ +void cleanup() { + vkDestroyInstance(instance, nullptr); + + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +The parameters for the `vkDestroyInstance` function are straightforward. As +mentioned in the previous chapter, the allocation and deallocation functions +in Vulkan have an optional allocator callback that we'll ignore by passing +`nullptr` to it. All of the other Vulkan resources that we'll create in the +following chapters should be cleaned up before the instance is destroyed. + +Before continuing with the more complex steps after instance creation, it's time +to evaluate our debugging options by checking out [validation layers](!en/Drawing_a_triangle/Setup/Validation_layers). + +[C++ code](/code/01_instance_creation.cpp) diff --git a/ko/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md b/ko/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md new file mode 100644 index 00000000..569a0178 --- /dev/null +++ b/ko/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md @@ -0,0 +1,458 @@ +## What are validation layers? + +The Vulkan API is designed around the idea of minimal driver overhead and one of +the manifestations of that goal is that there is very limited error checking in +the API by default. Even mistakes as simple as setting enumerations to incorrect +values or passing null pointers to required parameters are generally not +explicitly handled and will simply result in crashes or undefined behavior. +Because Vulkan requires you to be very explicit about everything you're doing, +it's easy to make many small mistakes like using a new GPU feature and +forgetting to request it at logical device creation time. + +However, that doesn't mean that these checks can't be added to the API. Vulkan +introduces an elegant system for this known as *validation layers*. Validation +layers are optional components that hook into Vulkan function calls to apply +additional operations. Common operations in validation layers are: + +* Checking the values of parameters against the specification to detect misuse +* Tracking creation and destruction of objects to find resource leaks +* Checking thread safety by tracking the threads that calls originate from +* Logging every call and its parameters to the standard output +* Tracing Vulkan calls for profiling and replaying + +Here's an example of what the implementation of a function in a diagnostics +validation layer could look like: + +```c++ +VkResult vkCreateInstance( + const VkInstanceCreateInfo* pCreateInfo, + const VkAllocationCallbacks* pAllocator, + VkInstance* instance) { + + if (pCreateInfo == nullptr || instance == nullptr) { + log("Null pointer passed to required parameter!"); + return VK_ERROR_INITIALIZATION_FAILED; + } + + return real_vkCreateInstance(pCreateInfo, pAllocator, instance); +} +``` + +These validation layers can be freely stacked to include all the debugging +functionality that you're interested in. You can simply enable validation layers +for debug builds and completely disable them for release builds, which gives you +the best of both worlds! + +Vulkan does not come with any validation layers built-in, but the LunarG Vulkan +SDK provides a nice set of layers that check for common errors. They're also +completely [open source](https://github.com/KhronosGroup/Vulkan-ValidationLayers), +so you can check which kind of mistakes they check for and contribute. Using the +validation layers is the best way to avoid your application breaking on +different drivers by accidentally relying on undefined behavior. + +Validation layers can only be used if they have been installed onto the system. +For example, the LunarG validation layers are only available on PCs with the +Vulkan SDK installed. + +There were formerly two different types of validation layers in Vulkan: instance +and device specific. The idea was that instance layers would only check +calls related to global Vulkan objects like instances, and device specific layers +would only check calls related to a specific GPU. Device specific layers have now been +deprecated, which means that instance validation layers apply to all Vulkan +calls. The specification document still recommends that you enable validation +layers at device level as well for compatibility, which is required by some +implementations. We'll simply specify the same layers as the instance at logical +device level, which we'll see [later on](!en/Drawing_a_triangle/Setup/Logical_device_and_queues). + +## Using validation layers + +In this section we'll see how to enable the standard diagnostics layers provided +by the Vulkan SDK. Just like extensions, validation layers need to be enabled by +specifying their name. All of the useful standard validation is bundled into a layer included in the SDK that is known as `VK_LAYER_KHRONOS_validation`. + +Let's first add two configuration variables to the program to specify the layers +to enable and whether to enable them or not. I've chosen to base that value on +whether the program is being compiled in debug mode or not. The `NDEBUG` macro +is part of the C++ standard and means "not debug". + +```c++ +const uint32_t WIDTH = 800; +const uint32_t HEIGHT = 600; + +const std::vector validationLayers = { + "VK_LAYER_KHRONOS_validation" +}; + +#ifdef NDEBUG + const bool enableValidationLayers = false; +#else + const bool enableValidationLayers = true; +#endif +``` + +We'll add a new function `checkValidationLayerSupport` that checks if all of +the requested layers are available. First list all of the available layers +using the `vkEnumerateInstanceLayerProperties` function. Its usage is identical +to that of `vkEnumerateInstanceExtensionProperties` which was discussed in the +instance creation chapter. + +```c++ +bool checkValidationLayerSupport() { + uint32_t layerCount; + vkEnumerateInstanceLayerProperties(&layerCount, nullptr); + + std::vector availableLayers(layerCount); + vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data()); + + return false; +} +``` + +Next, check if all of the layers in `validationLayers` exist in the +`availableLayers` list. You may need to include `` for `strcmp`. + +```c++ +for (const char* layerName : validationLayers) { + bool layerFound = false; + + for (const auto& layerProperties : availableLayers) { + if (strcmp(layerName, layerProperties.layerName) == 0) { + layerFound = true; + break; + } + } + + if (!layerFound) { + return false; + } +} + +return true; +``` + +We can now use this function in `createInstance`: + +```c++ +void createInstance() { + if (enableValidationLayers && !checkValidationLayerSupport()) { + throw std::runtime_error("validation layers requested, but not available!"); + } + + ... +} +``` + +Now run the program in debug mode and ensure that the error does not occur. If +it does, then have a look at the FAQ. + +Finally, modify the `VkInstanceCreateInfo` struct instantiation to include the +validation layer names if they are enabled: + +```c++ +if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); +} else { + createInfo.enabledLayerCount = 0; +} +``` + +If the check was successful then `vkCreateInstance` should not ever return a +`VK_ERROR_LAYER_NOT_PRESENT` error, but you should run the program to make sure. + +## Message callback + +The validation layers will print debug messages to the standard output by default, but we can also handle them ourselves by providing an explicit callback in our program. This will also allow you to decide which kind of messages you would like to see, because not all are necessarily (fatal) errors. If you don't want to do that right now then you may skip to the last section in this chapter. + +To set up a callback in the program to handle messages and the associated details, we have to set up a debug messenger with a callback using the `VK_EXT_debug_utils` extension. + +We'll first create a `getRequiredExtensions` function that will return the +required list of extensions based on whether validation layers are enabled or +not: + +```c++ +std::vector getRequiredExtensions() { + uint32_t glfwExtensionCount = 0; + const char** glfwExtensions; + glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + + std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); + + if (enableValidationLayers) { + extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + } + + return extensions; +} +``` + +The extensions specified by GLFW are always required, but the debug messenger +extension is conditionally added. Note that I've used the +`VK_EXT_DEBUG_UTILS_EXTENSION_NAME` macro here which is equal to the literal +string "VK_EXT_debug_utils". Using this macro lets you avoid typos. + +We can now use this function in `createInstance`: + +```c++ +auto extensions = getRequiredExtensions(); +createInfo.enabledExtensionCount = static_cast(extensions.size()); +createInfo.ppEnabledExtensionNames = extensions.data(); +``` + +Run the program to make sure you don't receive a +`VK_ERROR_EXTENSION_NOT_PRESENT` error. We don't really need to check for the +existence of this extension, because it should be implied by the availability of +the validation layers. + +Now let's see what a debug callback function looks like. Add a new static member +function called `debugCallback` with the `PFN_vkDebugUtilsMessengerCallbackEXT` +prototype. The `VKAPI_ATTR` and `VKAPI_CALL` ensure that the function has the +right signature for Vulkan to call it. + +```c++ +static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( + VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + VkDebugUtilsMessageTypeFlagsEXT messageType, + const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, + void* pUserData) { + + std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl; + + return VK_FALSE; +} +``` + +The first parameter specifies the severity of the message, which is one of the following flags: + +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT`: Diagnostic message +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT`: Informational message like the creation of a resource +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT`: Message about behavior that is not necessarily an error, but very likely a bug in your application +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT`: Message about behavior that is invalid and may cause crashes + +The values of this enumeration are set up in such a way that you can use a comparison operation to check if a message is equal or worse compared to some level of severity, for example: + +```c++ +if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + // Message is important enough to show +} +``` + +The `messageType` parameter can have the following values: + +* `VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT`: Some event has happened that is unrelated to the specification or performance +* `VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT`: Something has happened that violates the specification or indicates a possible mistake +* `VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT`: Potential non-optimal use of Vulkan + +The `pCallbackData` parameter refers to a `VkDebugUtilsMessengerCallbackDataEXT` struct containing the details of the message itself, with the most important members being: + +* `pMessage`: The debug message as a null-terminated string +* `pObjects`: Array of Vulkan object handles related to the message +* `objectCount`: Number of objects in array + +Finally, the `pUserData` parameter contains a pointer that was specified during the setup of the callback and allows you to pass your own data to it. + +The callback returns a boolean that indicates if the Vulkan call that triggered +the validation layer message should be aborted. If the callback returns true, +then the call is aborted with the `VK_ERROR_VALIDATION_FAILED_EXT` error. This +is normally only used to test the validation layers themselves, so you should +always return `VK_FALSE`. + +All that remains now is telling Vulkan about the callback function. Perhaps +somewhat surprisingly, even the debug callback in Vulkan is managed with a +handle that needs to be explicitly created and destroyed. Such a callback is part of a *debug messenger* and you can have as many of them as you want. Add a class member for +this handle right under `instance`: + +```c++ +VkDebugUtilsMessengerEXT debugMessenger; +``` + +Now add a function `setupDebugMessenger` to be called from `initVulkan` right +after `createInstance`: + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); +} + +void setupDebugMessenger() { + if (!enableValidationLayers) return; + +} +``` + +We'll need to fill in a structure with details about the messenger and its callback: + +```c++ +VkDebugUtilsMessengerCreateInfoEXT createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; +createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; +createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; +createInfo.pfnUserCallback = debugCallback; +createInfo.pUserData = nullptr; // Optional +``` + +The `messageSeverity` field allows you to specify all the types of severities you would like your callback to be called for. I've specified all types except for `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT` here to receive notifications about possible problems while leaving out verbose general debug info. + +Similarly the `messageType` field lets you filter which types of messages your callback is notified about. I've simply enabled all types here. You can always disable some if they're not useful to you. + +Finally, the `pfnUserCallback` field specifies the pointer to the callback function. You can optionally pass a pointer to the `pUserData` field which will be passed along to the callback function via the `pUserData` parameter. You could use this to pass a pointer to the `HelloTriangleApplication` class, for example. + +Note that there are many more ways to configure validation layer messages and debug callbacks, but this is a good setup to get started with for this tutorial. See the [extension specification](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap50.html#VK_EXT_debug_utils) for more info about the possibilities. + +This struct should be passed to the `vkCreateDebugUtilsMessengerEXT` function to +create the `VkDebugUtilsMessengerEXT` object. Unfortunately, because this +function is an extension function, it is not automatically loaded. We have to +look up its address ourselves using `vkGetInstanceProcAddr`. We're going to +create our own proxy function that handles this in the background. I've added it +right above the `HelloTriangleApplication` class definition. + +```c++ +VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) { + auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT"); + if (func != nullptr) { + return func(instance, pCreateInfo, pAllocator, pDebugMessenger); + } else { + return VK_ERROR_EXTENSION_NOT_PRESENT; + } +} +``` + +The `vkGetInstanceProcAddr` function will return `nullptr` if the function +couldn't be loaded. We can now call this function to create the extension +object if it's available: + +```c++ +if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { + throw std::runtime_error("failed to set up debug messenger!"); +} +``` + +The second to last parameter is again the optional allocator callback that we +set to `nullptr`, other than that the parameters are fairly straightforward. +Since the debug messenger is specific to our Vulkan instance and its layers, it +needs to be explicitly specified as first argument. You will also see this +pattern with other *child* objects later on. + +The `VkDebugUtilsMessengerEXT` object also needs to be cleaned up with a call to +`vkDestroyDebugUtilsMessengerEXT`. Similarly to `vkCreateDebugUtilsMessengerEXT` +the function needs to be explicitly loaded. + +Create another proxy function right below `CreateDebugUtilsMessengerEXT`: + +```c++ +void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) { + auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT"); + if (func != nullptr) { + func(instance, debugMessenger, pAllocator); + } +} +``` + +Make sure that this function is either a static class function or a function +outside the class. We can then call it in the `cleanup` function: + +```c++ +void cleanup() { + if (enableValidationLayers) { + DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr); + } + + vkDestroyInstance(instance, nullptr); + + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +## Debugging instance creation and destruction + +Although we've now added debugging with validation layers to the program we're not covering everything quite yet. The `vkCreateDebugUtilsMessengerEXT` call requires a valid instance to have been created and `vkDestroyDebugUtilsMessengerEXT` must be called before the instance is destroyed. This currently leaves us unable to debug any issues in the `vkCreateInstance` and `vkDestroyInstance` calls. + +However, if you closely read the [extension documentation](https://github.com/KhronosGroup/Vulkan-Docs/blob/main/appendices/VK_EXT_debug_utils.adoc#examples), you'll see that there is a way to create a separate debug utils messenger specifically for those two function calls. It requires you to simply pass a pointer to a `VkDebugUtilsMessengerCreateInfoEXT` struct in the `pNext` extension field of `VkInstanceCreateInfo`. First extract population of the messenger create info into a separate function: + +```c++ +void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) { + createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; + createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; + createInfo.pfnUserCallback = debugCallback; +} + +... + +void setupDebugMessenger() { + if (!enableValidationLayers) return; + + VkDebugUtilsMessengerCreateInfoEXT createInfo; + populateDebugMessengerCreateInfo(createInfo); + + if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { + throw std::runtime_error("failed to set up debug messenger!"); + } +} +``` + +We can now re-use this in the `createInstance` function: + +```c++ +void createInstance() { + ... + + VkInstanceCreateInfo createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + createInfo.pApplicationInfo = &appInfo; + + ... + + VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; + if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); + + populateDebugMessengerCreateInfo(debugCreateInfo); + createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*) &debugCreateInfo; + } else { + createInfo.enabledLayerCount = 0; + + createInfo.pNext = nullptr; + } + + if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); + } +} +``` + +The `debugCreateInfo` variable is placed outside the if statement to ensure that it is not destroyed before the `vkCreateInstance` call. By creating an additional debug messenger this way it will automatically be used during `vkCreateInstance` and `vkDestroyInstance` and cleaned up after that. + +## Testing + +Now let's intentionally make a mistake to see the validation layers in action. Temporarily remove the call to `DestroyDebugUtilsMessengerEXT` in the `cleanup` function and run your program. Once it exits you should see something like this: + +![](/images/validation_layer_test.png) + +>If you don't see any messages then [check your installation](https://vulkan.lunarg.com/doc/view/1.2.131.1/windows/getting_started.html#user-content-verify-the-installation). + +If you want to see which call triggered a message, you can add a breakpoint to the message callback and look at the stack trace. + +## Configuration + +There are a lot more settings for the behavior of validation layers than just +the flags specified in the `VkDebugUtilsMessengerCreateInfoEXT` struct. Browse +to the Vulkan SDK and go to the `Config` directory. There you will find a +`vk_layer_settings.txt` file that explains how to configure the layers. + +To configure the layer settings for your own application, copy the file to the +`Debug` and `Release` directories of your project and follow the instructions to +set the desired behavior. However, for the remainder of this tutorial I'll +assume that you're using the default settings. + +Throughout this tutorial I'll be making a couple of intentional mistakes to show +you how helpful the validation layers are with catching them and to teach you +how important it is to know exactly what you're doing with Vulkan. Now it's time +to look at [Vulkan devices in the system](!en/Drawing_a_triangle/Setup/Physical_devices_and_queue_families). + +[C++ code](/code/02_validation_layers.cpp) diff --git a/ko/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md b/ko/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md new file mode 100644 index 00000000..5761b9bc --- /dev/null +++ b/ko/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md @@ -0,0 +1,364 @@ +## Selecting a physical device + +After initializing the Vulkan library through a VkInstance we need to look for +and select a graphics card in the system that supports the features we need. In +fact we can select any number of graphics cards and use them simultaneously, but +in this tutorial we'll stick to the first graphics card that suits our needs. + +We'll add a function `pickPhysicalDevice` and add a call to it in the +`initVulkan` function. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + pickPhysicalDevice(); +} + +void pickPhysicalDevice() { + +} +``` + +The graphics card that we'll end up selecting will be stored in a +VkPhysicalDevice handle that is added as a new class member. This object will be +implicitly destroyed when the VkInstance is destroyed, so we won't need to do +anything new in the `cleanup` function. + +```c++ +VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; +``` + +Listing the graphics cards is very similar to listing extensions and starts with +querying just the number. + +```c++ +uint32_t deviceCount = 0; +vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr); +``` + +If there are 0 devices with Vulkan support then there is no point going further. + +```c++ +if (deviceCount == 0) { + throw std::runtime_error("failed to find GPUs with Vulkan support!"); +} +``` + +Otherwise we can now allocate an array to hold all of the VkPhysicalDevice +handles. + +```c++ +std::vector devices(deviceCount); +vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()); +``` + +Now we need to evaluate each of them and check if they are suitable for the +operations we want to perform, because not all graphics cards are created equal. +For that we'll introduce a new function: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + return true; +} +``` + +And we'll check if any of the physical devices meet the requirements that we'll +add to that function. + +```c++ +for (const auto& device : devices) { + if (isDeviceSuitable(device)) { + physicalDevice = device; + break; + } +} + +if (physicalDevice == VK_NULL_HANDLE) { + throw std::runtime_error("failed to find a suitable GPU!"); +} +``` + +The next section will introduce the first requirements that we'll check for in +the `isDeviceSuitable` function. As we'll start using more Vulkan features in +the later chapters we will also extend this function to include more checks. + +## Base device suitability checks + +To evaluate the suitability of a device we can start by querying for some +details. Basic device properties like the name, type and supported Vulkan +version can be queried using vkGetPhysicalDeviceProperties. + +```c++ +VkPhysicalDeviceProperties deviceProperties; +vkGetPhysicalDeviceProperties(device, &deviceProperties); +``` + +The support for optional features like texture compression, 64 bit floats and +multi viewport rendering (useful for VR) can be queried using +vkGetPhysicalDeviceFeatures: + +```c++ +VkPhysicalDeviceFeatures deviceFeatures; +vkGetPhysicalDeviceFeatures(device, &deviceFeatures); +``` + +There are more details that can be queried from devices that we'll discuss later +concerning device memory and queue families (see the next section). + +As an example, let's say we consider our application only usable for dedicated +graphics cards that support geometry shaders. Then the `isDeviceSuitable` +function would look like this: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + VkPhysicalDeviceProperties deviceProperties; + VkPhysicalDeviceFeatures deviceFeatures; + vkGetPhysicalDeviceProperties(device, &deviceProperties); + vkGetPhysicalDeviceFeatures(device, &deviceFeatures); + + return deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU && + deviceFeatures.geometryShader; +} +``` + +Instead of just checking if a device is suitable or not and going with the first +one, you could also give each device a score and pick the highest one. That way +you could favor a dedicated graphics card by giving it a higher score, but fall +back to an integrated GPU if that's the only available one. You could implement +something like that as follows: + +```c++ +#include + +... + +void pickPhysicalDevice() { + ... + + // Use an ordered map to automatically sort candidates by increasing score + std::multimap candidates; + + for (const auto& device : devices) { + int score = rateDeviceSuitability(device); + candidates.insert(std::make_pair(score, device)); + } + + // Check if the best candidate is suitable at all + if (candidates.rbegin()->first > 0) { + physicalDevice = candidates.rbegin()->second; + } else { + throw std::runtime_error("failed to find a suitable GPU!"); + } +} + +int rateDeviceSuitability(VkPhysicalDevice device) { + ... + + int score = 0; + + // Discrete GPUs have a significant performance advantage + if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { + score += 1000; + } + + // Maximum possible size of textures affects graphics quality + score += deviceProperties.limits.maxImageDimension2D; + + // Application can't function without geometry shaders + if (!deviceFeatures.geometryShader) { + return 0; + } + + return score; +} +``` + +You don't need to implement all that for this tutorial, but it's to give you an +idea of how you could design your device selection process. Of course you can +also just display the names of the choices and allow the user to select. + +Because we're just starting out, Vulkan support is the only thing we need and +therefore we'll settle for just any GPU: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + return true; +} +``` + +In the next section we'll discuss the first real required feature to check for. + +## Queue families + +It has been briefly touched upon before that almost every operation in Vulkan, +anything from drawing to uploading textures, requires commands to be submitted +to a queue. There are different types of queues that originate from different +*queue families* and each family of queues allows only a subset of commands. For +example, there could be a queue family that only allows processing of compute +commands or one that only allows memory transfer related commands. + +We need to check which queue families are supported by the device and which one +of these supports the commands that we want to use. For that purpose we'll add a +new function `findQueueFamilies` that looks for all the queue families we need. + +Right now we are only going to look for a queue that supports graphics commands, +so the function could look like this: + +```c++ +uint32_t findQueueFamilies(VkPhysicalDevice device) { + // Logic to find graphics queue family +} +``` + +However, in one of the next chapters we're already going to look for yet another +queue, so it's better to prepare for that and bundle the indices into a struct: + +```c++ +struct QueueFamilyIndices { + uint32_t graphicsFamily; +}; + +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + // Logic to find queue family indices to populate struct with + return indices; +} +``` + +But what if a queue family is not available? We could throw an exception in +`findQueueFamilies`, but this function is not really the right place to make +decisions about device suitability. For example, we may *prefer* devices with a +dedicated transfer queue family, but not require it. Therefore we need some way +of indicating whether a particular queue family was found. + +It's not really possible to use a magic value to indicate the nonexistence of a +queue family, since any value of `uint32_t` could in theory be a valid queue +family index including `0`. Luckily C++17 introduced a data structure to +distinguish between the case of a value existing or not: + +```c++ +#include + +... + +std::optional graphicsFamily; + +std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // false + +graphicsFamily = 0; + +std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // true +``` + +`std::optional` is a wrapper that contains no value until you assign something +to it. At any point you can query if it contains a value or not by calling its +`has_value()` member function. That means that we can change the logic to: + +```c++ +#include + +... + +struct QueueFamilyIndices { + std::optional graphicsFamily; +}; + +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + // Assign index to queue families that could be found + return indices; +} +``` + +We can now begin to actually implement `findQueueFamilies`: + +```c++ +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + + ... + + return indices; +} +``` + +The process of retrieving the list of queue families is exactly what you expect +and uses `vkGetPhysicalDeviceQueueFamilyProperties`: + +```c++ +uint32_t queueFamilyCount = 0; +vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr); + +std::vector queueFamilies(queueFamilyCount); +vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data()); +``` + +The VkQueueFamilyProperties struct contains some details about the queue family, +including the type of operations that are supported and the number of queues +that can be created based on that family. We need to find at least one queue +family that supports `VK_QUEUE_GRAPHICS_BIT`. + +```c++ +int i = 0; +for (const auto& queueFamily : queueFamilies) { + if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) { + indices.graphicsFamily = i; + } + + i++; +} +``` + +Now that we have this fancy queue family lookup function, we can use it as a +check in the `isDeviceSuitable` function to ensure that the device can process +the commands we want to use: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + QueueFamilyIndices indices = findQueueFamilies(device); + + return indices.graphicsFamily.has_value(); +} +``` + +To make this a little bit more convenient, we'll also add a generic check to the +struct itself: + +```c++ +struct QueueFamilyIndices { + std::optional graphicsFamily; + + bool isComplete() { + return graphicsFamily.has_value(); + } +}; + +... + +bool isDeviceSuitable(VkPhysicalDevice device) { + QueueFamilyIndices indices = findQueueFamilies(device); + + return indices.isComplete(); +} +``` + +We can now also use this for an early exit from `findQueueFamilies`: + +```c++ +for (const auto& queueFamily : queueFamilies) { + ... + + if (indices.isComplete()) { + break; + } + + i++; +} +``` + +Great, that's all we need for now to find the right physical device! The next +step is to [create a logical device](!en/Drawing_a_triangle/Setup/Logical_device_and_queues) +to interface with it. + +[C++ code](/code/03_physical_device_selection.cpp) diff --git a/ko/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md b/ko/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md new file mode 100644 index 00000000..f2677d08 --- /dev/null +++ b/ko/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md @@ -0,0 +1,171 @@ +## Introduction + +After selecting a physical device to use we need to set up a *logical device* to +interface with it. The logical device creation process is similar to the +instance creation process and describes the features we want to use. We also +need to specify which queues to create now that we've queried which queue +families are available. You can even create multiple logical devices from the +same physical device if you have varying requirements. + +Start by adding a new class member to store the logical device handle in. + +```c++ +VkDevice device; +``` + +Next, add a `createLogicalDevice` function that is called from `initVulkan`. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + pickPhysicalDevice(); + createLogicalDevice(); +} + +void createLogicalDevice() { + +} +``` + +## Specifying the queues to be created + +The creation of a logical device involves specifying a bunch of details in +structs again, of which the first one will be `VkDeviceQueueCreateInfo`. This +structure describes the number of queues we want for a single queue family. +Right now we're only interested in a queue with graphics capabilities. + +```c++ +QueueFamilyIndices indices = findQueueFamilies(physicalDevice); + +VkDeviceQueueCreateInfo queueCreateInfo{}; +queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; +queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value(); +queueCreateInfo.queueCount = 1; +``` + +The currently available drivers will only allow you to create a small number of +queues for each queue family and you don't really need more than one. That's +because you can create all of the command buffers on multiple threads and then +submit them all at once on the main thread with a single low-overhead call. + +Vulkan lets you assign priorities to queues to influence the scheduling of +command buffer execution using floating point numbers between `0.0` and `1.0`. +This is required even if there is only a single queue: + +```c++ +float queuePriority = 1.0f; +queueCreateInfo.pQueuePriorities = &queuePriority; +``` + +## Specifying used device features + +The next information to specify is the set of device features that we'll be +using. These are the features that we queried support for with +`vkGetPhysicalDeviceFeatures` in the previous chapter, like geometry shaders. +Right now we don't need anything special, so we can simply define it and leave +everything to `VK_FALSE`. We'll come back to this structure once we're about to +start doing more interesting things with Vulkan. + +```c++ +VkPhysicalDeviceFeatures deviceFeatures{}; +``` + +## Creating the logical device + +With the previous two structures in place, we can start filling in the main +`VkDeviceCreateInfo` structure. + +```c++ +VkDeviceCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; +``` + +First add pointers to the queue creation info and device features structs: + +```c++ +createInfo.pQueueCreateInfos = &queueCreateInfo; +createInfo.queueCreateInfoCount = 1; + +createInfo.pEnabledFeatures = &deviceFeatures; +``` + +The remainder of the information bears a resemblance to the +`VkInstanceCreateInfo` struct and requires you to specify extensions and +validation layers. The difference is that these are device specific this time. + +An example of a device specific extension is `VK_KHR_swapchain`, which allows +you to present rendered images from that device to windows. It is possible that +there are Vulkan devices in the system that lack this ability, for example +because they only support compute operations. We will come back to this +extension in the swap chain chapter. + +Previous implementations of Vulkan made a distinction between instance and device specific validation layers, but this is [no longer the case](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap40.html#extendingvulkan-layers-devicelayerdeprecation). That means that the `enabledLayerCount` and `ppEnabledLayerNames` fields of `VkDeviceCreateInfo` are ignored by up-to-date implementations. However, it is still a good idea to set them anyway to be compatible with older implementations: + +```c++ +createInfo.enabledExtensionCount = 0; + +if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); +} else { + createInfo.enabledLayerCount = 0; +} +``` + +We won't need any device specific extensions for now. + +That's it, we're now ready to instantiate the logical device with a call to the +appropriately named `vkCreateDevice` function. + +```c++ +if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) { + throw std::runtime_error("failed to create logical device!"); +} +``` + +The parameters are the physical device to interface with, the queue and usage +info we just specified, the optional allocation callbacks pointer and a pointer +to a variable to store the logical device handle in. Similarly to the instance +creation function, this call can return errors based on enabling non-existent +extensions or specifying the desired usage of unsupported features. + +The device should be destroyed in `cleanup` with the `vkDestroyDevice` function: + +```c++ +void cleanup() { + vkDestroyDevice(device, nullptr); + ... +} +``` + +Logical devices don't interact directly with instances, which is why it's not +included as a parameter. + +## Retrieving queue handles + +The queues are automatically created along with the logical device, but we don't +have a handle to interface with them yet. First add a class member to store a +handle to the graphics queue: + +```c++ +VkQueue graphicsQueue; +``` + +Device queues are implicitly cleaned up when the device is destroyed, so we +don't need to do anything in `cleanup`. + +We can use the `vkGetDeviceQueue` function to retrieve queue handles for each +queue family. The parameters are the logical device, queue family, queue index +and a pointer to the variable to store the queue handle in. Because we're only +creating a single queue from this family, we'll simply use index `0`. + +```c++ +vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue); +``` + +With the logical device and queue handles we can now actually start using the +graphics card to do things! In the next few chapters we'll set up the resources +to present results to the window system. + +[C++ code](/code/04_logical_device.cpp) diff --git a/ko/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md b/ko/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md new file mode 100644 index 00000000..966a8946 --- /dev/null +++ b/ko/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md @@ -0,0 +1,233 @@ +Since Vulkan is a platform agnostic API, it can not interface directly with the +window system on its own. To establish the connection between Vulkan and the +window system to present results to the screen, we need to use the WSI (Window +System Integration) extensions. In this chapter we'll discuss the first one, +which is `VK_KHR_surface`. It exposes a `VkSurfaceKHR` object that represents an +abstract type of surface to present rendered images to. The surface in our +program will be backed by the window that we've already opened with GLFW. + +The `VK_KHR_surface` extension is an instance level extension and we've actually +already enabled it, because it's included in the list returned by +`glfwGetRequiredInstanceExtensions`. The list also includes some other WSI +extensions that we'll use in the next couple of chapters. + +The window surface needs to be created right after the instance creation, +because it can actually influence the physical device selection. The reason we +postponed this is because window surfaces are part of the larger topic of +render targets and presentation for which the explanation would have cluttered +the basic setup. It should also be noted that window surfaces are an entirely +optional component in Vulkan, if you just need off-screen rendering. Vulkan +allows you to do that without hacks like creating an invisible window +(necessary for OpenGL). + +## Window surface creation + +Start by adding a `surface` class member right below the debug callback. + +```c++ +VkSurfaceKHR surface; +``` + +Although the `VkSurfaceKHR` object and its usage is platform agnostic, its +creation isn't because it depends on window system details. For example, it +needs the `HWND` and `HMODULE` handles on Windows. Therefore there is a +platform-specific addition to the extension, which on Windows is called +`VK_KHR_win32_surface` and is also automatically included in the list from +`glfwGetRequiredInstanceExtensions`. + +I will demonstrate how this platform specific extension can be used to create a +surface on Windows, but we won't actually use it in this tutorial. It doesn't +make any sense to use a library like GLFW and then proceed to use +platform-specific code anyway. GLFW actually has `glfwCreateWindowSurface` that +handles the platform differences for us. Still, it's good to see what it does +behind the scenes before we start relying on it. + +To access native platform functions, you need to update the includes at the top: + +```c++ +#define VK_USE_PLATFORM_WIN32_KHR +#define GLFW_INCLUDE_VULKAN +#include +#define GLFW_EXPOSE_NATIVE_WIN32 +#include +``` + +Because a window surface is a Vulkan object, it comes with a +`VkWin32SurfaceCreateInfoKHR` struct that needs to be filled in. It has two +important parameters: `hwnd` and `hinstance`. These are the handles to the +window and the process. + +```c++ +VkWin32SurfaceCreateInfoKHR createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; +createInfo.hwnd = glfwGetWin32Window(window); +createInfo.hinstance = GetModuleHandle(nullptr); +``` + +The `glfwGetWin32Window` function is used to get the raw `HWND` from the GLFW +window object. The `GetModuleHandle` call returns the `HINSTANCE` handle of the +current process. + +After that the surface can be created with `vkCreateWin32SurfaceKHR`, which includes a parameter for the instance, surface creation details, custom allocators and the variable for the surface handle to be stored in. Technically this is a WSI extension function, but it is so commonly used that the standard Vulkan loader includes it, so unlike other extensions you don't need to explicitly load it. + +```c++ +if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) { + throw std::runtime_error("failed to create window surface!"); +} +``` + +The process is similar for other platforms like Linux, where +`vkCreateXcbSurfaceKHR` takes an XCB connection and window as creation details +with X11. + +The `glfwCreateWindowSurface` function performs exactly this operation with a +different implementation for each platform. We'll now integrate it into our +program. Add a function `createSurface` to be called from `initVulkan` right +after instance creation and `setupDebugMessenger`. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); +} + +void createSurface() { + +} +``` + +The GLFW call takes simple parameters instead of a struct which makes the +implementation of the function very straightforward: + +```c++ +void createSurface() { + if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) { + throw std::runtime_error("failed to create window surface!"); + } +} +``` + +The parameters are the `VkInstance`, GLFW window pointer, custom allocators and +pointer to `VkSurfaceKHR` variable. It simply passes through the `VkResult` from +the relevant platform call. GLFW doesn't offer a special function for destroying +a surface, but that can easily be done through the original API: + +```c++ +void cleanup() { + ... + vkDestroySurfaceKHR(instance, surface, nullptr); + vkDestroyInstance(instance, nullptr); + ... + } +``` + +Make sure that the surface is destroyed before the instance. + +## Querying for presentation support + +Although the Vulkan implementation may support window system integration, that +does not mean that every device in the system supports it. Therefore we need to +extend `isDeviceSuitable` to ensure that a device can present images to the +surface we created. Since the presentation is a queue-specific feature, the +problem is actually about finding a queue family that supports presenting to the +surface we created. + +It's actually possible that the queue families supporting drawing commands and +the ones supporting presentation do not overlap. Therefore we have to take into +account that there could be a distinct presentation queue by modifying the +`QueueFamilyIndices` structure: + +```c++ +struct QueueFamilyIndices { + std::optional graphicsFamily; + std::optional presentFamily; + + bool isComplete() { + return graphicsFamily.has_value() && presentFamily.has_value(); + } +}; +``` + +Next, we'll modify the `findQueueFamilies` function to look for a queue family +that has the capability of presenting to our window surface. The function to +check for that is `vkGetPhysicalDeviceSurfaceSupportKHR`, which takes the +physical device, queue family index and surface as parameters. Add a call to it +in the same loop as the `VK_QUEUE_GRAPHICS_BIT`: + +```c++ +VkBool32 presentSupport = false; +vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport); +``` + +Then simply check the value of the boolean and store the presentation family +queue index: + +```c++ +if (presentSupport) { + indices.presentFamily = i; +} +``` + +Note that it's very likely that these end up being the same queue family after +all, but throughout the program we will treat them as if they were separate +queues for a uniform approach. Nevertheless, you could add logic to explicitly +prefer a physical device that supports drawing and presentation in the same +queue for improved performance. + +## Creating the presentation queue + +The one thing that remains is modifying the logical device creation procedure to +create the presentation queue and retrieve the `VkQueue` handle. Add a member +variable for the handle: + +```c++ +VkQueue presentQueue; +``` + +Next, we need to have multiple `VkDeviceQueueCreateInfo` structs to create a +queue from both families. An elegant way to do that is to create a set of all +unique queue families that are necessary for the required queues: + +```c++ +#include + +... + +QueueFamilyIndices indices = findQueueFamilies(physicalDevice); + +std::vector queueCreateInfos; +std::set uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()}; + +float queuePriority = 1.0f; +for (uint32_t queueFamily : uniqueQueueFamilies) { + VkDeviceQueueCreateInfo queueCreateInfo{}; + queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queueCreateInfo.queueFamilyIndex = queueFamily; + queueCreateInfo.queueCount = 1; + queueCreateInfo.pQueuePriorities = &queuePriority; + queueCreateInfos.push_back(queueCreateInfo); +} +``` + +And modify `VkDeviceCreateInfo` to point to the vector: + +```c++ +createInfo.queueCreateInfoCount = static_cast(queueCreateInfos.size()); +createInfo.pQueueCreateInfos = queueCreateInfos.data(); +``` + +If the queue families are the same, then we only need to pass its index once. +Finally, add a call to retrieve the queue handle: + +```c++ +vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue); +``` + +In case the queue families are the same, the two handles will most likely have +the same value now. In the next chapter we're going to look at swap chains and +how they give us the ability to present images to the surface. + +[C++ code](/code/05_window_surface.cpp) diff --git a/ko/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md b/ko/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md new file mode 100644 index 00000000..f593b5a6 --- /dev/null +++ b/ko/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md @@ -0,0 +1,603 @@ +Vulkan does not have the concept of a "default framebuffer", hence it requires an infrastructure that will own the buffers we will render to before we visualize them on the screen. This infrastructure is +known as the *swap chain* and must be created explicitly in Vulkan. The swap +chain is essentially a queue of images that are waiting to be presented to the +screen. Our application will acquire such an image to draw to it, and then +return it to the queue. How exactly the queue works and the conditions for +presenting an image from the queue depend on how the swap chain is set up, but +the general purpose of the swap chain is to synchronize the presentation of +images with the refresh rate of the screen. + +## Checking for swap chain support + +Not all graphics cards are capable of presenting images directly to a screen for +various reasons, for example because they are designed for servers and don't +have any display outputs. Secondly, since image presentation is heavily tied +into the window system and the surfaces associated with windows, it is not +actually part of the Vulkan core. You have to enable the `VK_KHR_swapchain` +device extension after querying for its support. + +For that purpose we'll first extend the `isDeviceSuitable` function to check if +this extension is supported. We've previously seen how to list the extensions +that are supported by a `VkPhysicalDevice`, so doing that should be fairly +straightforward. Note that the Vulkan header file provides a nice macro +`VK_KHR_SWAPCHAIN_EXTENSION_NAME` that is defined as `VK_KHR_swapchain`. The +advantage of using this macro is that the compiler will catch misspellings. + +First declare a list of required device extensions, similar to the list of +validation layers to enable. + +```c++ +const std::vector deviceExtensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME +}; +``` + +Next, create a new function `checkDeviceExtensionSupport` that is called from +`isDeviceSuitable` as an additional check: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + QueueFamilyIndices indices = findQueueFamilies(device); + + bool extensionsSupported = checkDeviceExtensionSupport(device); + + return indices.isComplete() && extensionsSupported; +} + +bool checkDeviceExtensionSupport(VkPhysicalDevice device) { + return true; +} +``` + +Modify the body of the function to enumerate the extensions and check if all of +the required extensions are amongst them. + +```c++ +bool checkDeviceExtensionSupport(VkPhysicalDevice device) { + uint32_t extensionCount; + vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr); + + std::vector availableExtensions(extensionCount); + vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data()); + + std::set requiredExtensions(deviceExtensions.begin(), deviceExtensions.end()); + + for (const auto& extension : availableExtensions) { + requiredExtensions.erase(extension.extensionName); + } + + return requiredExtensions.empty(); +} +``` + +I've chosen to use a set of strings here to represent the unconfirmed required +extensions. That way we can easily tick them off while enumerating the sequence +of available extensions. Of course you can also use a nested loop like in +`checkValidationLayerSupport`. The performance difference is irrelevant. Now run +the code and verify that your graphics card is indeed capable of creating a +swap chain. It should be noted that the availability of a presentation queue, +as we checked in the previous chapter, implies that the swap chain extension +must be supported. However, it's still good to be explicit about things, and +the extension does have to be explicitly enabled. + +## Enabling device extensions + +Using a swapchain requires enabling the `VK_KHR_swapchain` extension first. +Enabling the extension just requires a small change to the logical device +creation structure: + +```c++ +createInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); +createInfo.ppEnabledExtensionNames = deviceExtensions.data(); +``` + +Make sure to replace the existing line `createInfo.enabledExtensionCount = 0;` when you do so. + +## Querying details of swap chain support + +Just checking if a swap chain is available is not sufficient, because it may not +actually be compatible with our window surface. Creating a swap chain also +involves a lot more settings than instance and device creation, so we need to +query for some more details before we're able to proceed. + +There are basically three kinds of properties we need to check: + +* Basic surface capabilities (min/max number of images in swap chain, min/max +width and height of images) +* Surface formats (pixel format, color space) +* Available presentation modes + +Similar to `findQueueFamilies`, we'll use a struct to pass these details around +once they've been queried. The three aforementioned types of properties come in +the form of the following structs and lists of structs: + +```c++ +struct SwapChainSupportDetails { + VkSurfaceCapabilitiesKHR capabilities; + std::vector formats; + std::vector presentModes; +}; +``` + +We'll now create a new function `querySwapChainSupport` that will populate this +struct. + +```c++ +SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) { + SwapChainSupportDetails details; + + return details; +} +``` + +This section covers how to query the structs that include this information. The +meaning of these structs and exactly which data they contain is discussed in the +next section. + +Let's start with the basic surface capabilities. These properties are simple to +query and are returned into a single `VkSurfaceCapabilitiesKHR` struct. + +```c++ +vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities); +``` + +This function takes the specified `VkPhysicalDevice` and `VkSurfaceKHR` window +surface into account when determining the supported capabilities. All of the +support querying functions have these two as first parameters because they are +the core components of the swap chain. + +The next step is about querying the supported surface formats. Because this is a +list of structs, it follows the familiar ritual of 2 function calls: + +```c++ +uint32_t formatCount; +vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr); + +if (formatCount != 0) { + details.formats.resize(formatCount); + vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data()); +} +``` + +Make sure that the vector is resized to hold all the available formats. And +finally, querying the supported presentation modes works exactly the same way +with `vkGetPhysicalDeviceSurfacePresentModesKHR`: + +```c++ +uint32_t presentModeCount; +vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr); + +if (presentModeCount != 0) { + details.presentModes.resize(presentModeCount); + vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data()); +} +``` + +All of the details are in the struct now, so let's extend `isDeviceSuitable` +once more to utilize this function to verify that swap chain support is +adequate. Swap chain support is sufficient for this tutorial if there is at +least one supported image format and one supported presentation mode given the +window surface we have. + +```c++ +bool swapChainAdequate = false; +if (extensionsSupported) { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); + swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); +} +``` + +It is important that we only try to query for swap chain support after verifying +that the extension is available. The last line of the function changes to: + +```c++ +return indices.isComplete() && extensionsSupported && swapChainAdequate; +``` + +## Choosing the right settings for the swap chain + +If the `swapChainAdequate` conditions were met then the support is definitely +sufficient, but there may still be many different modes of varying optimality. +We'll now write a couple of functions to find the right settings for the best +possible swap chain. There are three types of settings to determine: + +* Surface format (color depth) +* Presentation mode (conditions for "swapping" images to the screen) +* Swap extent (resolution of images in swap chain) + +For each of these settings we'll have an ideal value in mind that we'll go with +if it's available and otherwise we'll create some logic to find the next best +thing. + +### Surface format + +The function for this setting starts out like this. We'll later pass the +`formats` member of the `SwapChainSupportDetails` struct as argument. + +```c++ +VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { + +} +``` + +Each `VkSurfaceFormatKHR` entry contains a `format` and a `colorSpace` member. The +`format` member specifies the color channels and types. For example, +`VK_FORMAT_B8G8R8A8_SRGB` means that we store the B, G, R and alpha channels in +that order with an 8 bit unsigned integer for a total of 32 bits per pixel. The +`colorSpace` member indicates if the SRGB color space is supported or not using +the `VK_COLOR_SPACE_SRGB_NONLINEAR_KHR` flag. Note that this flag used to be +called `VK_COLORSPACE_SRGB_NONLINEAR_KHR` in old versions of the specification. + +For the color space we'll use SRGB if it is available, because it [results in more accurate perceived colors](http://stackoverflow.com/questions/12524623/). It is also pretty much the standard color space for images, like the textures we'll use later on. +Because of that we should also use an SRGB color format, of which one of the most common ones is `VK_FORMAT_B8G8R8A8_SRGB`. + +Let's go through the list and see if the preferred combination is available: + +```c++ +for (const auto& availableFormat : availableFormats) { + if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + return availableFormat; + } +} +``` + +If that also fails then we could start ranking the available formats based on +how "good" they are, but in most cases it's okay to just settle with the first +format that is specified. + +```c++ +VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { + for (const auto& availableFormat : availableFormats) { + if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + return availableFormat; + } + } + + return availableFormats[0]; +} +``` + +### Presentation mode + +The presentation mode is arguably the most important setting for the swap chain, +because it represents the actual conditions for showing images to the screen. +There are four possible modes available in Vulkan: + +* `VK_PRESENT_MODE_IMMEDIATE_KHR`: Images submitted by your application are +transferred to the screen right away, which may result in tearing. +* `VK_PRESENT_MODE_FIFO_KHR`: The swap chain is a queue where the display takes +an image from the front of the queue when the display is refreshed and the +program inserts rendered images at the back of the queue. If the queue is full +then the program has to wait. This is most similar to vertical sync as found in +modern games. The moment that the display is refreshed is known as "vertical +blank". +* `VK_PRESENT_MODE_FIFO_RELAXED_KHR`: This mode only differs from the previous +one if the application is late and the queue was empty at the last vertical +blank. Instead of waiting for the next vertical blank, the image is transferred +right away when it finally arrives. This may result in visible tearing. +* `VK_PRESENT_MODE_MAILBOX_KHR`: This is another variation of the second mode. +Instead of blocking the application when the queue is full, the images that are +already queued are simply replaced with the newer ones. This mode can be used to +render frames as fast as possible while still avoiding tearing, resulting in fewer latency issues than standard vertical sync. This is commonly known as "triple buffering", although the existence of three buffers alone does not necessarily mean that the framerate is unlocked. + +Only the `VK_PRESENT_MODE_FIFO_KHR` mode is guaranteed to be available, so we'll +again have to write a function that looks for the best mode that is available: + +```c++ +VkPresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { + return VK_PRESENT_MODE_FIFO_KHR; +} +``` + +I personally think that `VK_PRESENT_MODE_MAILBOX_KHR` is a very nice trade-off if energy usage is not a concern. It allows us to avoid tearing while still maintaining a fairly low latency by rendering new images that are as up-to-date as possible right until the vertical blank. On mobile devices, where energy usage is more important, you will probably want to use `VK_PRESENT_MODE_FIFO_KHR` instead. Now, let's look through the list to see if `VK_PRESENT_MODE_MAILBOX_KHR` is available: + +```c++ +VkPresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { + for (const auto& availablePresentMode : availablePresentModes) { + if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) { + return availablePresentMode; + } + } + + return VK_PRESENT_MODE_FIFO_KHR; +} +``` + +### Swap extent + +That leaves only one major property, for which we'll add one last function: + +```c++ +VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) { + +} +``` + +The swap extent is the resolution of the swap chain images and it's almost +always exactly equal to the resolution of the window that we're drawing to _in +pixels_ (more on that in a moment). The range of the possible resolutions is +defined in the `VkSurfaceCapabilitiesKHR` structure. Vulkan tells us to match +the resolution of the window by setting the width and height in the +`currentExtent` member. However, some window managers do allow us to differ here +and this is indicated by setting the width and height in `currentExtent` to a +special value: the maximum value of `uint32_t`. In that case we'll pick the +resolution that best matches the window within the `minImageExtent` and +`maxImageExtent` bounds. But we must specify the resolution in the correct unit. + +GLFW uses two units when measuring sizes: pixels and +[screen coordinates](https://www.glfw.org/docs/latest/intro_guide.html#coordinate_systems). +For example, the resolution `{WIDTH, HEIGHT}` that we specified earlier when +creating the window is measured in screen coordinates. But Vulkan works with +pixels, so the swap chain extent must be specified in pixels as well. +Unfortunately, if you are using a high DPI display (like Apple's Retina +display), screen coordinates don't correspond to pixels. Instead, due to the +higher pixel density, the resolution of the window in pixel will be larger than +the resolution in screen coordinates. So if Vulkan doesn't fix the swap extent +for us, we can't just use the original `{WIDTH, HEIGHT}`. Instead, we must use +`glfwGetFramebufferSize` to query the resolution of the window in pixel before +matching it against the minimum and maximum image extent. + +```c++ +#include // Necessary for uint32_t +#include // Necessary for std::numeric_limits +#include // Necessary for std::clamp + +... + +VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) { + if (capabilities.currentExtent.width != std::numeric_limits::max()) { + return capabilities.currentExtent; + } else { + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + VkExtent2D actualExtent = { + static_cast(width), + static_cast(height) + }; + + actualExtent.width = std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); + actualExtent.height = std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); + + return actualExtent; + } +} +``` + +The `clamp` function is used here to bound the values of `width` and `height` between the allowed minimum and maximum extents that are supported by the implementation. + +## Creating the swap chain + +Now that we have all of these helper functions assisting us with the choices we +have to make at runtime, we finally have all the information that is needed to +create a working swap chain. + +Create a `createSwapChain` function that starts out with the results of these +calls and make sure to call it from `initVulkan` after logical device creation. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); +} + +void createSwapChain() { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice); + + VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats); + VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes); + VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities); +} +``` + +Aside from these properties we also have to decide how many images we would like to have in the swap chain. The implementation specifies the minimum number that it requires to function: + +```c++ +uint32_t imageCount = swapChainSupport.capabilities.minImageCount; +``` + +However, simply sticking to this minimum means that we may sometimes have to wait on the driver to complete internal operations before we can acquire another image to render to. Therefore it is recommended to request at least one more image than the minimum: + +```c++ +uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1; +``` + +We should also make sure to not exceed the maximum number of images while doing this, where `0` is a special value that means that there is no maximum: + +```c++ +if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) { + imageCount = swapChainSupport.capabilities.maxImageCount; +} +``` + +As is tradition with Vulkan objects, creating the swap chain object requires +filling in a large structure. It starts out very familiarly: + +```c++ +VkSwapchainCreateInfoKHR createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; +createInfo.surface = surface; +``` + +After specifying which surface the swap chain should be tied to, the details of +the swap chain images are specified: + +```c++ +createInfo.minImageCount = imageCount; +createInfo.imageFormat = surfaceFormat.format; +createInfo.imageColorSpace = surfaceFormat.colorSpace; +createInfo.imageExtent = extent; +createInfo.imageArrayLayers = 1; +createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; +``` + +The `imageArrayLayers` specifies the amount of layers each image consists of. +This is always `1` unless you are developing a stereoscopic 3D application. The +`imageUsage` bit field specifies what kind of operations we'll use the images in +the swap chain for. In this tutorial we're going to render directly to them, +which means that they're used as color attachment. It is also possible that +you'll render images to a separate image first to perform operations like +post-processing. In that case you may use a value like +`VK_IMAGE_USAGE_TRANSFER_DST_BIT` instead and use a memory operation to transfer +the rendered image to a swap chain image. + +```c++ +QueueFamilyIndices indices = findQueueFamilies(physicalDevice); +uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()}; + +if (indices.graphicsFamily != indices.presentFamily) { + createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT; + createInfo.queueFamilyIndexCount = 2; + createInfo.pQueueFamilyIndices = queueFamilyIndices; +} else { + createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + createInfo.queueFamilyIndexCount = 0; // Optional + createInfo.pQueueFamilyIndices = nullptr; // Optional +} +``` + +Next, we need to specify how to handle swap chain images that will be used +across multiple queue families. That will be the case in our application if the +graphics queue family is different from the presentation queue. We'll be drawing +on the images in the swap chain from the graphics queue and then submitting them +on the presentation queue. There are two ways to handle images that are +accessed from multiple queues: + +* `VK_SHARING_MODE_EXCLUSIVE`: An image is owned by one queue family at a time +and ownership must be explicitly transferred before using it in another queue +family. This option offers the best performance. +* `VK_SHARING_MODE_CONCURRENT`: Images can be used across multiple queue +families without explicit ownership transfers. + +If the queue families differ, then we'll be using the concurrent mode in this +tutorial to avoid having to do the ownership chapters, because these involve +some concepts that are better explained at a later time. Concurrent mode +requires you to specify in advance between which queue families ownership will +be shared using the `queueFamilyIndexCount` and `pQueueFamilyIndices` +parameters. If the graphics queue family and presentation queue family are the +same, which will be the case on most hardware, then we should stick to exclusive +mode, because concurrent mode requires you to specify at least two distinct +queue families. + +```c++ +createInfo.preTransform = swapChainSupport.capabilities.currentTransform; +``` + +We can specify that a certain transform should be applied to images in the swap +chain if it is supported (`supportedTransforms` in `capabilities`), like a 90 +degree clockwise rotation or horizontal flip. To specify that you do not want +any transformation, simply specify the current transformation. + +```c++ +createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; +``` + +The `compositeAlpha` field specifies if the alpha channel should be used for +blending with other windows in the window system. You'll almost always want to +simply ignore the alpha channel, hence `VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR`. + +```c++ +createInfo.presentMode = presentMode; +createInfo.clipped = VK_TRUE; +``` + +The `presentMode` member speaks for itself. If the `clipped` member is set to +`VK_TRUE` then that means that we don't care about the color of pixels that are +obscured, for example because another window is in front of them. Unless you +really need to be able to read these pixels back and get predictable results, +you'll get the best performance by enabling clipping. + +```c++ +createInfo.oldSwapchain = VK_NULL_HANDLE; +``` + +That leaves one last field, `oldSwapchain`. With Vulkan it's possible that your swap chain becomes invalid or unoptimized while your application is +running, for example because the window was resized. In that case the swap chain +actually needs to be recreated from scratch and a reference to the old one must +be specified in this field. This is a complex topic that we'll learn more about +in [a future chapter](!en/Drawing_a_triangle/Swap_chain_recreation). For now we'll +assume that we'll only ever create one swap chain. + +Now add a class member to store the `VkSwapchainKHR` object: + +```c++ +VkSwapchainKHR swapChain; +``` + +Creating the swap chain is now as simple as calling `vkCreateSwapchainKHR`: + +```c++ +if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) { + throw std::runtime_error("failed to create swap chain!"); +} +``` + +The parameters are the logical device, swap chain creation info, optional custom +allocators and a pointer to the variable to store the handle in. No surprises +there. It should be cleaned up using `vkDestroySwapchainKHR` before the device: + +```c++ +void cleanup() { + vkDestroySwapchainKHR(device, swapChain, nullptr); + ... +} +``` + +Now run the application to ensure that the swap chain is created successfully! If at this point you get an access violation error in `vkCreateSwapchainKHR` or see a message like `Failed to find 'vkGetInstanceProcAddress' in layer SteamOverlayVulkanLayer.dll`, then see the [FAQ entry](!en/FAQ) about the Steam overlay layer. + +Try removing the `createInfo.imageExtent = extent;` line with validation layers +enabled. You'll see that one of the validation layers immediately catches the +mistake and a helpful message is printed: + +![](/images/swap_chain_validation_layer.png) + +## Retrieving the swap chain images + +The swap chain has been created now, so all that remains is retrieving the +handles of the `VkImage`s in it. We'll reference these during rendering +operations in later chapters. Add a class member to store the handles: + +```c++ +std::vector swapChainImages; +``` + +The images were created by the implementation for the swap chain and they will +be automatically cleaned up once the swap chain has been destroyed, therefore we +don't need to add any cleanup code. + +I'm adding the code to retrieve the handles to the end of the `createSwapChain` +function, right after the `vkCreateSwapchainKHR` call. Retrieving them is very +similar to the other times where we retrieved an array of objects from Vulkan. Remember that we only specified a minimum number of images in the swap chain, so the implementation is allowed to create a swap chain with more. That's why we'll first query the final number of images with `vkGetSwapchainImagesKHR`, then resize the container and finally call it again +to retrieve the handles. + +```c++ +vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr); +swapChainImages.resize(imageCount); +vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data()); +``` + +One last thing, store the format and extent we've chosen for the swap chain +images in member variables. We'll need them in future chapters. + +```c++ +VkSwapchainKHR swapChain; +std::vector swapChainImages; +VkFormat swapChainImageFormat; +VkExtent2D swapChainExtent; + +... + +swapChainImageFormat = surfaceFormat.format; +swapChainExtent = extent; +``` + +We now have a set of images that can be drawn onto and can be presented to the +window. The next chapter will begin to cover how we can set up the images as +render targets and then we start looking into the actual graphics pipeline and +drawing commands! + +[C++ code](/code/06_swap_chain_creation.cpp) diff --git a/ko/03_Drawing_a_triangle/01_Presentation/02_Image_views.md b/ko/03_Drawing_a_triangle/01_Presentation/02_Image_views.md new file mode 100644 index 00000000..5988468a --- /dev/null +++ b/ko/03_Drawing_a_triangle/01_Presentation/02_Image_views.md @@ -0,0 +1,127 @@ +To use any `VkImage`, including those in the swap chain, in the render pipeline +we have to create a `VkImageView` object. An image view is quite literally a +view into an image. It describes how to access the image and which part of the +image to access, for example if it should be treated as a 2D texture depth +texture without any mipmapping levels. + +In this chapter we'll write a `createImageViews` function that creates a basic +image view for every image in the swap chain so that we can use them as color +targets later on. + +First add a class member to store the image views in: + +```c++ +std::vector swapChainImageViews; +``` + +Create the `createImageViews` function and call it right after swap chain +creation. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); +} + +void createImageViews() { + +} +``` + +The first thing we need to do is resize the list to fit all of the image views +we'll be creating: + +```c++ +void createImageViews() { + swapChainImageViews.resize(swapChainImages.size()); + +} +``` + +Next, set up the loop that iterates over all of the swap chain images. + +```c++ +for (size_t i = 0; i < swapChainImages.size(); i++) { + +} +``` + +The parameters for image view creation are specified in a +`VkImageViewCreateInfo` structure. The first few parameters are straightforward. + +```c++ +VkImageViewCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; +createInfo.image = swapChainImages[i]; +``` + +The `viewType` and `format` fields specify how the image data should be +interpreted. The `viewType` parameter allows you to treat images as 1D textures, +2D textures, 3D textures and cube maps. + +```c++ +createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; +createInfo.format = swapChainImageFormat; +``` + +The `components` field allows you to swizzle the color channels around. For +example, you can map all of the channels to the red channel for a monochrome +texture. You can also map constant values of `0` and `1` to a channel. In our +case we'll stick to the default mapping. + +```c++ +createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; +createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; +createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; +createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; +``` + +The `subresourceRange` field describes what the image's purpose is and which +part of the image should be accessed. Our images will be used as color targets +without any mipmapping levels or multiple layers. + +```c++ +createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +createInfo.subresourceRange.baseMipLevel = 0; +createInfo.subresourceRange.levelCount = 1; +createInfo.subresourceRange.baseArrayLayer = 0; +createInfo.subresourceRange.layerCount = 1; +``` + +If you were working on a stereographic 3D application, then you would create a +swap chain with multiple layers. You could then create multiple image views for +each image representing the views for the left and right eyes by accessing +different layers. + +Creating the image view is now a matter of calling `vkCreateImageView`: + +```c++ +if (vkCreateImageView(device, &createInfo, nullptr, &swapChainImageViews[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create image views!"); +} +``` + +Unlike images, the image views were explicitly created by us, so we need to add +a similar loop to destroy them again at the end of the program: + +```c++ +void cleanup() { + for (auto imageView : swapChainImageViews) { + vkDestroyImageView(device, imageView, nullptr); + } + + ... +} +``` + +An image view is sufficient to start using an image as a texture, but it's not +quite ready to be used as a render target just yet. That requires one more step +of indirection, known as a framebuffer. But first we'll have to set up the +graphics pipeline. + +[C++ code](/code/07_image_views.cpp) diff --git a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md new file mode 100644 index 00000000..9ee7f739 --- /dev/null +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md @@ -0,0 +1,99 @@ +Over the course of the next few chapters we'll be setting up a graphics pipeline +that is configured to draw our first triangle. The graphics pipeline is the +sequence of operations that take the vertices and textures of your meshes all +the way to the pixels in the render targets. A simplified overview is displayed +below: + +![](/images/vulkan_simplified_pipeline.svg) + +The *input assembler* collects the raw vertex data from the buffers you specify +and may also use an index buffer to repeat certain elements without having to +duplicate the vertex data itself. + +The *vertex shader* is run for every vertex and generally applies +transformations to turn vertex positions from model space to screen space. It +also passes per-vertex data down the pipeline. + +The *tessellation shaders* allow you to subdivide geometry based on certain +rules to increase the mesh quality. This is often used to make surfaces like +brick walls and staircases look less flat when they are nearby. + +The *geometry shader* is run on every primitive (triangle, line, point) and can +discard it or output more primitives than came in. This is similar to the +tessellation shader, but much more flexible. However, it is not used much in +today's applications because the performance is not that good on most graphics +cards except for Intel's integrated GPUs. + +The *rasterization* stage discretizes the primitives into *fragments*. These are +the pixel elements that they fill on the framebuffer. Any fragments that fall +outside the screen are discarded and the attributes outputted by the vertex +shader are interpolated across the fragments, as shown in the figure. Usually +the fragments that are behind other primitive fragments are also discarded here +because of depth testing. + +The *fragment shader* is invoked for every fragment that survives and determines +which framebuffer(s) the fragments are written to and with which color and depth +values. It can do this using the interpolated data from the vertex shader, which +can include things like texture coordinates and normals for lighting. + +The *color blending* stage applies operations to mix different fragments that +map to the same pixel in the framebuffer. Fragments can simply overwrite each +other, add up or be mixed based upon transparency. + +Stages with a green color are known as *fixed-function* stages. These stages +allow you to tweak their operations using parameters, but the way they work is +predefined. + +Stages with an orange color on the other hand are `programmable`, which means +that you can upload your own code to the graphics card to apply exactly the +operations you want. This allows you to use fragment shaders, for example, to +implement anything from texturing and lighting to ray tracers. These programs +run on many GPU cores simultaneously to process many objects, like vertices and +fragments in parallel. + +If you've used older APIs like OpenGL and Direct3D before, then you'll be used +to being able to change any pipeline settings at will with calls like +`glBlendFunc` and `OMSetBlendState`. The graphics pipeline in Vulkan is almost +completely immutable, so you must recreate the pipeline from scratch if you want +to change shaders, bind different framebuffers or change the blend function. The +disadvantage is that you'll have to create a number of pipelines that represent +all of the different combinations of states you want to use in your rendering +operations. However, because all of the operations you'll be doing in the +pipeline are known in advance, the driver can optimize for it much better. + +Some of the programmable stages are optional based on what you intend to do. For +example, the tessellation and geometry stages can be disabled if you are just +drawing simple geometry. If you are only interested in depth values then you can +disable the fragment shader stage, which is useful for [shadow map](https://en.wikipedia.org/wiki/Shadow_mapping) +generation. + +In the next chapter we'll first create the two programmable stages required to +put a triangle onto the screen: the vertex shader and fragment shader. The +fixed-function configuration like blending mode, viewport, rasterization will be +set up in the chapter after that. The final part of setting up the graphics +pipeline in Vulkan involves the specification of input and output framebuffers. + +Create a `createGraphicsPipeline` function that is called right after +`createImageViews` in `initVulkan`. We'll work on this function throughout the +following chapters. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createGraphicsPipeline(); +} + +... + +void createGraphicsPipeline() { + +} +``` + +[C++ code](/code/08_graphics_pipeline.cpp) diff --git a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md new file mode 100644 index 00000000..ef12e836 --- /dev/null +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md @@ -0,0 +1,467 @@ +Unlike earlier APIs, shader code in Vulkan has to be specified in a bytecode +format as opposed to human-readable syntax like [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language) +and [HLSL](https://en.wikipedia.org/wiki/High-Level_Shading_Language). This +bytecode format is called [SPIR-V](https://www.khronos.org/spir) and is designed +to be used with both Vulkan and OpenCL (both Khronos APIs). It is a format that +can be used to write graphics and compute shaders, but we will focus on shaders +used in Vulkan's graphics pipelines in this tutorial. + +The advantage of using a bytecode format is that the compilers written by GPU +vendors to turn shader code into native code are significantly less complex. The +past has shown that with human-readable syntax like GLSL, some GPU vendors were +rather flexible with their interpretation of the standard. If you happen to +write non-trivial shaders with a GPU from one of these vendors, then you'd risk +other vendor's drivers rejecting your code due to syntax errors, or worse, your +shader running differently because of compiler bugs. With a straightforward +bytecode format like SPIR-V that will hopefully be avoided. + +However, that does not mean that we need to write this bytecode by hand. Khronos +has released their own vendor-independent compiler that compiles GLSL to SPIR-V. +This compiler is designed to verify that your shader code is fully standards +compliant and produces one SPIR-V binary that you can ship with your program. +You can also include this compiler as a library to produce SPIR-V at runtime, +but we won't be doing that in this tutorial. Although we can use this compiler directly via `glslangValidator.exe`, we will be using `glslc.exe` by Google instead. The advantage of `glslc` is that it uses the same parameter format as well-known compilers like GCC and Clang and includes some extra functionality like *includes*. Both of them are already included in the Vulkan SDK, so you don't need to download anything extra. + +GLSL is a shading language with a C-style syntax. Programs written in it have a +`main` function that is invoked for every object. Instead of using parameters +for input and a return value as output, GLSL uses global variables to handle +input and output. The language includes many features to aid in graphics +programming, like built-in vector and matrix primitives. Functions for +operations like cross products, matrix-vector products and reflections around a +vector are included. The vector type is called `vec` with a number indicating +the amount of elements. For example, a 3D position would be stored in a `vec3`. +It is possible to access single components through members like `.x`, but it's +also possible to create a new vector from multiple components at the same time. +For example, the expression `vec3(1.0, 2.0, 3.0).xy` would result in `vec2`. The +constructors of vectors can also take combinations of vector objects and scalar +values. For example, a `vec3` can be constructed with +`vec3(vec2(1.0, 2.0), 3.0)`. + +As the previous chapter mentioned, we need to write a vertex shader and a +fragment shader to get a triangle on the screen. The next two sections will +cover the GLSL code of each of those and after that I'll show you how to produce +two SPIR-V binaries and load them into the program. + +## Vertex shader + +The vertex shader processes each incoming vertex. It takes its attributes, like +model space position, color, normal and texture coordinates as input. The output is +the final position in clip coordinates and the attributes that need to be passed +on to the fragment shader, like color and texture coordinates. These values will +then be interpolated over the fragments by the rasterizer to produce a smooth +gradient. + +A *clip coordinate* is a four dimensional vector from the vertex shader that is +subsequently turned into a *normalized device coordinate* by dividing the whole +vector by its last component. These normalized device coordinates are +[homogeneous coordinates](https://en.wikipedia.org/wiki/Homogeneous_coordinates) +that map the framebuffer to a [-1, 1] by [-1, 1] coordinate system that looks +like the following: + +![](/images/normalized_device_coordinates.svg) + +You should already be familiar with these if you have dabbled in computer +graphics before. If you have used OpenGL before, then you'll notice that the +sign of the Y coordinates is now flipped. The Z coordinate now uses the same +range as it does in Direct3D, from 0 to 1. + +For our first triangle we won't be applying any transformations, we'll just +specify the positions of the three vertices directly as normalized device +coordinates to create the following shape: + +![](/images/triangle_coordinates.svg) + +We can directly output normalized device coordinates by outputting them as clip +coordinates from the vertex shader with the last component set to `1`. That way +the division to transform clip coordinates to normalized device coordinates will +not change anything. + +Normally these coordinates would be stored in a vertex buffer, but creating a +vertex buffer in Vulkan and filling it with data is not trivial. Therefore I've +decided to postpone that until after we've had the satisfaction of seeing a +triangle pop up on the screen. We're going to do something a little unorthodox +in the meanwhile: include the coordinates directly inside the vertex shader. The +code looks like this: + +```glsl +#version 450 + +vec2 positions[3] = vec2[]( + vec2(0.0, -0.5), + vec2(0.5, 0.5), + vec2(-0.5, 0.5) +); + +void main() { + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); +} +``` + +The `main` function is invoked for every vertex. The built-in `gl_VertexIndex` +variable contains the index of the current vertex. This is usually an index into +the vertex buffer, but in our case it will be an index into a hardcoded array +of vertex data. The position of each vertex is accessed from the constant array +in the shader and combined with dummy `z` and `w` components to produce a +position in clip coordinates. The built-in variable `gl_Position` functions as +the output. + +## Fragment shader + +The triangle that is formed by the positions from the vertex shader fills an +area on the screen with fragments. The fragment shader is invoked on these +fragments to produce a color and depth for the framebuffer (or framebuffers). A +simple fragment shader that outputs the color red for the entire triangle looks +like this: + +```glsl +#version 450 + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(1.0, 0.0, 0.0, 1.0); +} +``` + +The `main` function is called for every fragment just like the vertex shader +`main` function is called for every vertex. Colors in GLSL are 4-component +vectors with the R, G, B and alpha channels within the [0, 1] range. Unlike +`gl_Position` in the vertex shader, there is no built-in variable to output a +color for the current fragment. You have to specify your own output variable for +each framebuffer where the `layout(location = 0)` modifier specifies the index +of the framebuffer. The color red is written to this `outColor` variable that is +linked to the first (and only) framebuffer at index `0`. + +## Per-vertex colors + +Making the entire triangle red is not very interesting, wouldn't something like +the following look a lot nicer? + +![](/images/triangle_coordinates_colors.png) + +We have to make a couple of changes to both shaders to accomplish this. First +off, we need to specify a distinct color for each of the three vertices. The +vertex shader should now include an array with colors just like it does for +positions: + +```glsl +vec3 colors[3] = vec3[]( + vec3(1.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(0.0, 0.0, 1.0) +); +``` + +Now we just need to pass these per-vertex colors to the fragment shader so it +can output their interpolated values to the framebuffer. Add an output for color +to the vertex shader and write to it in the `main` function: + +```glsl +layout(location = 0) out vec3 fragColor; + +void main() { + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); + fragColor = colors[gl_VertexIndex]; +} +``` + +Next, we need to add a matching input in the fragment shader: + +```glsl +layout(location = 0) in vec3 fragColor; + +void main() { + outColor = vec4(fragColor, 1.0); +} +``` + +The input variable does not necessarily have to use the same name, they will be +linked together using the indexes specified by the `location` directives. The +`main` function has been modified to output the color along with an alpha value. +As shown in the image above, the values for `fragColor` will be automatically +interpolated for the fragments between the three vertices, resulting in a smooth +gradient. + +## Compiling the shaders + +Create a directory called `shaders` in the root directory of your project and +store the vertex shader in a file called `shader.vert` and the fragment shader +in a file called `shader.frag` in that directory. GLSL shaders don't have an +official extension, but these two are commonly used to distinguish them. + +The contents of `shader.vert` should be: + +```glsl +#version 450 + +layout(location = 0) out vec3 fragColor; + +vec2 positions[3] = vec2[]( + vec2(0.0, -0.5), + vec2(0.5, 0.5), + vec2(-0.5, 0.5) +); + +vec3 colors[3] = vec3[]( + vec3(1.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(0.0, 0.0, 1.0) +); + +void main() { + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); + fragColor = colors[gl_VertexIndex]; +} +``` + +And the contents of `shader.frag` should be: + +```glsl +#version 450 + +layout(location = 0) in vec3 fragColor; + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(fragColor, 1.0); +} +``` + +We're now going to compile these into SPIR-V bytecode using the +`glslc` program. + +**Windows** + +Create a `compile.bat` file with the following contents: + +```bash +C:/VulkanSDK/x.x.x.x/Bin/glslc.exe shader.vert -o vert.spv +C:/VulkanSDK/x.x.x.x/Bin/glslc.exe shader.frag -o frag.spv +pause +``` + +Replace the path to `glslc.exe` with the path to where you installed +the Vulkan SDK. Double click the file to run it. + +**Linux** + +Create a `compile.sh` file with the following contents: + +```bash +/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.vert -o vert.spv +/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.frag -o frag.spv +``` + +Replace the path to `glslc` with the path to where you installed the +Vulkan SDK. Make the script executable with `chmod +x compile.sh` and run it. + +**End of platform-specific instructions** + +These two commands tell the compiler to read the GLSL source file and output a SPIR-V bytecode file using the `-o` (output) flag. + +If your shader contains a syntax error then the compiler will tell you the line +number and problem, as you would expect. Try leaving out a semicolon for example +and run the compile script again. Also try running the compiler without any +arguments to see what kinds of flags it supports. It can, for example, also +output the bytecode into a human-readable format so you can see exactly what +your shader is doing and any optimizations that have been applied at this stage. + +Compiling shaders on the commandline is one of the most straightforward options and it's the one that we'll use in this tutorial, but it's also possible to compile shaders directly from your own code. The Vulkan SDK includes [libshaderc](https://github.com/google/shaderc), which is a library to compile GLSL code to SPIR-V from within your program. + +## Loading a shader + +Now that we have a way of producing SPIR-V shaders, it's time to load them into +our program to plug them into the graphics pipeline at some point. We'll first +write a simple helper function to load the binary data from the files. + +```c++ +#include + +... + +static std::vector readFile(const std::string& filename) { + std::ifstream file(filename, std::ios::ate | std::ios::binary); + + if (!file.is_open()) { + throw std::runtime_error("failed to open file!"); + } +} +``` + +The `readFile` function will read all of the bytes from the specified file and +return them in a byte array managed by `std::vector`. We start by opening the +file with two flags: + +* `ate`: Start reading at the end of the file +* `binary`: Read the file as binary file (avoid text transformations) + +The advantage of starting to read at the end of the file is that we can use the +read position to determine the size of the file and allocate a buffer: + +```c++ +size_t fileSize = (size_t) file.tellg(); +std::vector buffer(fileSize); +``` + +After that, we can seek back to the beginning of the file and read all of the +bytes at once: + +```c++ +file.seekg(0); +file.read(buffer.data(), fileSize); +``` + +And finally close the file and return the bytes: + +```c++ +file.close(); + +return buffer; +``` + +We'll now call this function from `createGraphicsPipeline` to load the bytecode +of the two shaders: + +```c++ +void createGraphicsPipeline() { + auto vertShaderCode = readFile("shaders/vert.spv"); + auto fragShaderCode = readFile("shaders/frag.spv"); +} +``` + +Make sure that the shaders are loaded correctly by printing the size of the +buffers and checking if they match the actual file size in bytes. Note that the code doesn't need to be null terminated since it's binary code and we will later be explicit about its size. + +## Creating shader modules + +Before we can pass the code to the pipeline, we have to wrap it in a +`VkShaderModule` object. Let's create a helper function `createShaderModule` to +do that. + +```c++ +VkShaderModule createShaderModule(const std::vector& code) { + +} +``` + +The function will take a buffer with the bytecode as parameter and create a +`VkShaderModule` from it. + +Creating a shader module is simple, we only need to specify a pointer to the +buffer with the bytecode and the length of it. This information is specified in +a `VkShaderModuleCreateInfo` structure. The one catch is that the size of the +bytecode is specified in bytes, but the bytecode pointer is a `uint32_t` pointer +rather than a `char` pointer. Therefore we will need to cast the pointer with +`reinterpret_cast` as shown below. When you perform a cast like this, you also +need to ensure that the data satisfies the alignment requirements of `uint32_t`. +Lucky for us, the data is stored in an `std::vector` where the default allocator +already ensures that the data satisfies the worst case alignment requirements. + +```c++ +VkShaderModuleCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; +createInfo.codeSize = code.size(); +createInfo.pCode = reinterpret_cast(code.data()); +``` + +The `VkShaderModule` can then be created with a call to `vkCreateShaderModule`: + +```c++ +VkShaderModule shaderModule; +if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) { + throw std::runtime_error("failed to create shader module!"); +} +``` + +The parameters are the same as those in previous object creation functions: the +logical device, pointer to create info structure, optional pointer to custom +allocators and handle output variable. The buffer with the code can be freed +immediately after creating the shader module. Don't forget to return the created +shader module: + +```c++ +return shaderModule; +``` + +Shader modules are just a thin wrapper around the shader bytecode that we've previously loaded from a file and the functions defined in it. The compilation and linking of the SPIR-V bytecode to machine code for execution by the GPU doesn't happen until the graphics pipeline is created. That means that we're allowed to destroy the shader modules again as soon as pipeline creation is finished, which is why we'll make them local variables in the `createGraphicsPipeline` function instead of class members: + +```c++ +void createGraphicsPipeline() { + auto vertShaderCode = readFile("shaders/vert.spv"); + auto fragShaderCode = readFile("shaders/frag.spv"); + + VkShaderModule vertShaderModule = createShaderModule(vertShaderCode); + VkShaderModule fragShaderModule = createShaderModule(fragShaderCode); +``` + +The cleanup should then happen at the end of the function by adding two calls to `vkDestroyShaderModule`. All of the remaining code in this chapter will be inserted before these lines. + +```c++ + ... + vkDestroyShaderModule(device, fragShaderModule, nullptr); + vkDestroyShaderModule(device, vertShaderModule, nullptr); +} +``` + +## Shader stage creation + +To actually use the shaders we'll need to assign them to a specific pipeline stage through `VkPipelineShaderStageCreateInfo` structures as part of the actual pipeline creation process. + +We'll start by filling in the structure for the vertex shader, again in the +`createGraphicsPipeline` function. + +```c++ +VkPipelineShaderStageCreateInfo vertShaderStageInfo{}; +vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; +vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; +``` + +The first step, besides the obligatory `sType` member, is telling Vulkan in +which pipeline stage the shader is going to be used. There is an enum value for +each of the programmable stages described in the previous chapter. + +```c++ +vertShaderStageInfo.module = vertShaderModule; +vertShaderStageInfo.pName = "main"; +``` + +The next two members specify the shader module containing the code, and the +function to invoke, known as the *entrypoint*. That means that it's possible to combine multiple fragment +shaders into a single shader module and use different entry points to +differentiate between their behaviors. In this case we'll stick to the standard +`main`, however. + +There is one more (optional) member, `pSpecializationInfo`, which we won't be +using here, but is worth discussing. It allows you to specify values for shader +constants. You can use a single shader module where its behavior can be +configured at pipeline creation by specifying different values for the constants +used in it. This is more efficient than configuring the shader using variables +at render time, because the compiler can do optimizations like eliminating `if` +statements that depend on these values. If you don't have any constants like +that, then you can set the member to `nullptr`, which our struct initialization +does automatically. + +Modifying the structure to suit the fragment shader is easy: + +```c++ +VkPipelineShaderStageCreateInfo fragShaderStageInfo{}; +fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; +fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT; +fragShaderStageInfo.module = fragShaderModule; +fragShaderStageInfo.pName = "main"; +``` + +Finish by defining an array that contains these two structs, which we'll later +use to reference them in the actual pipeline creation step. + +```c++ +VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; +``` + +That's all there is to describing the programmable stages of the pipeline. In +the next chapter we'll look at the fixed-function stages. + +[C++ code](/code/09_shader_modules.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md new file mode 100644 index 00000000..5b4bfdec --- /dev/null +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md @@ -0,0 +1,439 @@ + +The older graphics APIs provided default state for most of the stages of the +graphics pipeline. In Vulkan you have to be explicit about most pipeline states as +it'll be baked into an immutable pipeline state object. In this chapter we'll fill +in all of the structures to configure these fixed-function operations. + +## Dynamic state + +While *most* of the pipeline state needs to be baked into the pipeline state, +a limited amount of the state *can* actually be changed without recreating the +pipeline at draw time. Examples are the size of the viewport, line width +and blend constants. If you want to use dynamic state and keep these properties out, +then you'll have to fill in a `VkPipelineDynamicStateCreateInfo` structure like this: + +```c++ +std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR +}; + +VkPipelineDynamicStateCreateInfo dynamicState{}; +dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; +dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); +dynamicState.pDynamicStates = dynamicStates.data(); +``` + +This will cause the configuration of these values to be ignored and you will be +able (and required) to specify the data at drawing time. This results in a more flexible +setup and is very common for things like viewport and scissor state, which would +result in a more complex setup when being baked into the pipeline state. + +## Vertex input + +The `VkPipelineVertexInputStateCreateInfo` structure describes the format of the +vertex data that will be passed to the vertex shader. It describes this in +roughly two ways: + +* Bindings: spacing between data and whether the data is per-vertex or +per-instance (see [instancing](https://en.wikipedia.org/wiki/Geometry_instancing)) +* Attribute descriptions: type of the attributes passed to the vertex shader, +which binding to load them from and at which offset + +Because we're hard coding the vertex data directly in the vertex shader, we'll +fill in this structure to specify that there is no vertex data to load for now. +We'll get back to it in the vertex buffer chapter. + +```c++ +VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; +vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; +vertexInputInfo.vertexBindingDescriptionCount = 0; +vertexInputInfo.pVertexBindingDescriptions = nullptr; // Optional +vertexInputInfo.vertexAttributeDescriptionCount = 0; +vertexInputInfo.pVertexAttributeDescriptions = nullptr; // Optional +``` + +The `pVertexBindingDescriptions` and `pVertexAttributeDescriptions` members +point to an array of structs that describe the aforementioned details for +loading vertex data. Add this structure to the `createGraphicsPipeline` function +right after the `shaderStages` array. + +## Input assembly + +The `VkPipelineInputAssemblyStateCreateInfo` struct describes two things: what +kind of geometry will be drawn from the vertices and if primitive restart should +be enabled. The former is specified in the `topology` member and can have values +like: + +* `VK_PRIMITIVE_TOPOLOGY_POINT_LIST`: points from vertices +* `VK_PRIMITIVE_TOPOLOGY_LINE_LIST`: line from every 2 vertices without reuse +* `VK_PRIMITIVE_TOPOLOGY_LINE_STRIP`: the end vertex of every line is used as +start vertex for the next line +* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST`: triangle from every 3 vertices without +reuse +* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP `: the second and third vertex of every +triangle are used as first two vertices of the next triangle + +Normally, the vertices are loaded from the vertex buffer by index in sequential +order, but with an *element buffer* you can specify the indices to use yourself. +This allows you to perform optimizations like reusing vertices. If you set the +`primitiveRestartEnable` member to `VK_TRUE`, then it's possible to break up +lines and triangles in the `_STRIP` topology modes by using a special index of +`0xFFFF` or `0xFFFFFFFF`. + +We intend to draw triangles throughout this tutorial, so we'll stick to the +following data for the structure: + +```c++ +VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; +inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; +inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; +inputAssembly.primitiveRestartEnable = VK_FALSE; +``` + +## Viewports and scissors + +A viewport basically describes the region of the framebuffer that the output +will be rendered to. This will almost always be `(0, 0)` to `(width, height)` +and in this tutorial that will also be the case. + +```c++ +VkViewport viewport{}; +viewport.x = 0.0f; +viewport.y = 0.0f; +viewport.width = (float) swapChainExtent.width; +viewport.height = (float) swapChainExtent.height; +viewport.minDepth = 0.0f; +viewport.maxDepth = 1.0f; +``` + +Remember that the size of the swap chain and its images may differ from the +`WIDTH` and `HEIGHT` of the window. The swap chain images will be used as +framebuffers later on, so we should stick to their size. + +The `minDepth` and `maxDepth` values specify the range of depth values to use +for the framebuffer. These values must be within the `[0.0f, 1.0f]` range, but +`minDepth` may be higher than `maxDepth`. If you aren't doing anything special, +then you should stick to the standard values of `0.0f` and `1.0f`. + +While viewports define the transformation from the image to the framebuffer, +scissor rectangles define in which regions pixels will actually be stored. Any +pixels outside the scissor rectangles will be discarded by the rasterizer. They +function like a filter rather than a transformation. The difference is +illustrated below. Note that the left scissor rectangle is just one of the many +possibilities that would result in that image, as long as it's larger than the +viewport. + +![](/images/viewports_scissors.png) + +So if we wanted to draw to the entire framebuffer, we would specify a scissor rectangle that covers it entirely: + +```c++ +VkRect2D scissor{}; +scissor.offset = {0, 0}; +scissor.extent = swapChainExtent; +``` + +Viewport(s) and scissor rectangle(s) can either be specified as a static part of the pipeline or as a [dynamic state](#dynamic-state) set in the command buffer. While the former is more in line with the other states it's often convenient to make viewport and scissor state dynamic as it gives you a lot more flexibility. This is very common and all implementations can handle this dynamic state without a performance penalty. + +When opting for dynamic viewport(s) and scissor rectangle(s) you need to enable the respective dynamic states for the pipeline: + +```c++ +std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR +}; + +VkPipelineDynamicStateCreateInfo dynamicState{}; +dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; +dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); +dynamicState.pDynamicStates = dynamicStates.data(); +``` + +And then you only need to specify their count at pipeline creation time: + +```c++ +VkPipelineViewportStateCreateInfo viewportState{}; +viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; +viewportState.viewportCount = 1; +viewportState.scissorCount = 1; +``` + +The actual viewport(s) and scissor rectangle(s) will then later be set up at drawing time. + +With dynamic state it's even possible to specify different viewports and or scissor rectangles within a single command buffer. + +Without dynamic state, the viewport and scissor rectangle need to be set in the pipeline using the `VkPipelineViewportStateCreateInfo` struct. This makes the viewport and scissor rectangle for this pipeline immutable. +Any changes required to these values would require a new pipeline to be created with the new values. + +```c++ +VkPipelineViewportStateCreateInfo viewportState{}; +viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; +viewportState.viewportCount = 1; +viewportState.pViewports = &viewport; +viewportState.scissorCount = 1; +viewportState.pScissors = &scissor; +``` + +Independent of how you set them, it's possible to use multiple viewports and scissor rectangles on some graphics cards, so the structure members reference an array of them. Using multiple requires enabling a GPU feature (see logical device creation). + +## Rasterizer + +The rasterizer takes the geometry that is shaped by the vertices from the vertex +shader and turns it into fragments to be colored by the fragment shader. It also +performs [depth testing](https://en.wikipedia.org/wiki/Z-buffering), +[face culling](https://en.wikipedia.org/wiki/Back-face_culling) and the scissor +test, and it can be configured to output fragments that fill entire polygons or +just the edges (wireframe rendering). All this is configured using the +`VkPipelineRasterizationStateCreateInfo` structure. + +```c++ +VkPipelineRasterizationStateCreateInfo rasterizer{}; +rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; +rasterizer.depthClampEnable = VK_FALSE; +``` + +If `depthClampEnable` is set to `VK_TRUE`, then fragments that are beyond the +near and far planes are clamped to them as opposed to discarding them. This is +useful in some special cases like shadow maps. Using this requires enabling a +GPU feature. + +```c++ +rasterizer.rasterizerDiscardEnable = VK_FALSE; +``` + +If `rasterizerDiscardEnable` is set to `VK_TRUE`, then geometry never passes +through the rasterizer stage. This basically disables any output to the +framebuffer. + +```c++ +rasterizer.polygonMode = VK_POLYGON_MODE_FILL; +``` + +The `polygonMode` determines how fragments are generated for geometry. The +following modes are available: + +* `VK_POLYGON_MODE_FILL`: fill the area of the polygon with fragments +* `VK_POLYGON_MODE_LINE`: polygon edges are drawn as lines +* `VK_POLYGON_MODE_POINT`: polygon vertices are drawn as points + +Using any mode other than fill requires enabling a GPU feature. + +```c++ +rasterizer.lineWidth = 1.0f; +``` + +The `lineWidth` member is straightforward, it describes the thickness of lines +in terms of number of fragments. The maximum line width that is supported +depends on the hardware and any line thicker than `1.0f` requires you to enable +the `wideLines` GPU feature. + +```c++ +rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; +rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE; +``` + +The `cullMode` variable determines the type of face culling to use. You can +disable culling, cull the front faces, cull the back faces or both. The +`frontFace` variable specifies the vertex order for faces to be considered +front-facing and can be clockwise or counterclockwise. + +```c++ +rasterizer.depthBiasEnable = VK_FALSE; +rasterizer.depthBiasConstantFactor = 0.0f; // Optional +rasterizer.depthBiasClamp = 0.0f; // Optional +rasterizer.depthBiasSlopeFactor = 0.0f; // Optional +``` + +The rasterizer can alter the depth values by adding a constant value or biasing +them based on a fragment's slope. This is sometimes used for shadow mapping, but +we won't be using it. Just set `depthBiasEnable` to `VK_FALSE`. + +## Multisampling + +The `VkPipelineMultisampleStateCreateInfo` struct configures multisampling, +which is one of the ways to perform [anti-aliasing](https://en.wikipedia.org/wiki/Multisample_anti-aliasing). +It works by combining the fragment shader results of multiple polygons that +rasterize to the same pixel. This mainly occurs along edges, which is also where +the most noticeable aliasing artifacts occur. Because it doesn't need to run the +fragment shader multiple times if only one polygon maps to a pixel, it is +significantly less expensive than simply rendering to a higher resolution and +then downscaling. Enabling it requires enabling a GPU feature. + +```c++ +VkPipelineMultisampleStateCreateInfo multisampling{}; +multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; +multisampling.sampleShadingEnable = VK_FALSE; +multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; +multisampling.minSampleShading = 1.0f; // Optional +multisampling.pSampleMask = nullptr; // Optional +multisampling.alphaToCoverageEnable = VK_FALSE; // Optional +multisampling.alphaToOneEnable = VK_FALSE; // Optional +``` + +We'll revisit multisampling in later chapter, for now let's keep it disabled. + +## Depth and stencil testing + +If you are using a depth and/or stencil buffer, then you also need to configure +the depth and stencil tests using `VkPipelineDepthStencilStateCreateInfo`. We +don't have one right now, so we can simply pass a `nullptr` instead of a pointer +to such a struct. We'll get back to it in the depth buffering chapter. + +## Color blending + +After a fragment shader has returned a color, it needs to be combined with the +color that is already in the framebuffer. This transformation is known as color +blending and there are two ways to do it: + +* Mix the old and new value to produce a final color +* Combine the old and new value using a bitwise operation + +There are two types of structs to configure color blending. The first struct, +`VkPipelineColorBlendAttachmentState` contains the configuration per attached +framebuffer and the second struct, `VkPipelineColorBlendStateCreateInfo` +contains the *global* color blending settings. In our case we only have one +framebuffer: + +```c++ +VkPipelineColorBlendAttachmentState colorBlendAttachment{}; +colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; +colorBlendAttachment.blendEnable = VK_FALSE; +colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; // Optional +colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional +colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; // Optional +colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; // Optional +colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional +colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // Optional +``` + +This per-framebuffer struct allows you to configure the first way of color +blending. The operations that will be performed are best demonstrated using the +following pseudocode: + +```c++ +if (blendEnable) { + finalColor.rgb = (srcColorBlendFactor * newColor.rgb) (dstColorBlendFactor * oldColor.rgb); + finalColor.a = (srcAlphaBlendFactor * newColor.a) (dstAlphaBlendFactor * oldColor.a); +} else { + finalColor = newColor; +} + +finalColor = finalColor & colorWriteMask; +``` + +If `blendEnable` is set to `VK_FALSE`, then the new color from the fragment +shader is passed through unmodified. Otherwise, the two mixing operations are +performed to compute a new color. The resulting color is AND'd with the +`colorWriteMask` to determine which channels are actually passed through. + +The most common way to use color blending is to implement alpha blending, where +we want the new color to be blended with the old color based on its opacity. The +`finalColor` should then be computed as follows: + +```c++ +finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor; +finalColor.a = newAlpha.a; +``` + +This can be accomplished with the following parameters: + +```c++ +colorBlendAttachment.blendEnable = VK_TRUE; +colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; +colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; +colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; +colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; +colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; +colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; +``` + +You can find all of the possible operations in the `VkBlendFactor` and +`VkBlendOp` enumerations in the specification. + +The second structure references the array of structures for all of the +framebuffers and allows you to set blend constants that you can use as blend +factors in the aforementioned calculations. + +```c++ +VkPipelineColorBlendStateCreateInfo colorBlending{}; +colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; +colorBlending.logicOpEnable = VK_FALSE; +colorBlending.logicOp = VK_LOGIC_OP_COPY; // Optional +colorBlending.attachmentCount = 1; +colorBlending.pAttachments = &colorBlendAttachment; +colorBlending.blendConstants[0] = 0.0f; // Optional +colorBlending.blendConstants[1] = 0.0f; // Optional +colorBlending.blendConstants[2] = 0.0f; // Optional +colorBlending.blendConstants[3] = 0.0f; // Optional +``` + +If you want to use the second method of blending (bitwise combination), then you +should set `logicOpEnable` to `VK_TRUE`. The bitwise operation can then be +specified in the `logicOp` field. Note that this will automatically disable the +first method, as if you had set `blendEnable` to `VK_FALSE` for every +attached framebuffer! The `colorWriteMask` will also be used in this mode to +determine which channels in the framebuffer will actually be affected. It is +also possible to disable both modes, as we've done here, in which case the +fragment colors will be written to the framebuffer unmodified. + +## Pipeline layout + +You can use `uniform` values in shaders, which are globals similar to dynamic +state variables that can be changed at drawing time to alter the behavior of +your shaders without having to recreate them. They are commonly used to pass the +transformation matrix to the vertex shader, or to create texture samplers in the +fragment shader. + +These uniform values need to be specified during pipeline creation by creating a +`VkPipelineLayout` object. Even though we won't be using them until a future +chapter, we are still required to create an empty pipeline layout. + +Create a class member to hold this object, because we'll refer to it from other +functions at a later point in time: + +```c++ +VkPipelineLayout pipelineLayout; +``` + +And then create the object in the `createGraphicsPipeline` function: + +```c++ +VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; +pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; +pipelineLayoutInfo.setLayoutCount = 0; // Optional +pipelineLayoutInfo.pSetLayouts = nullptr; // Optional +pipelineLayoutInfo.pushConstantRangeCount = 0; // Optional +pipelineLayoutInfo.pPushConstantRanges = nullptr; // Optional + +if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) { + throw std::runtime_error("failed to create pipeline layout!"); +} +``` + +The structure also specifies *push constants*, which are another way of passing +dynamic values to shaders that we may get into in a future chapter. The pipeline +layout will be referenced throughout the program's lifetime, so it should be +destroyed at the end: + +```c++ +void cleanup() { + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + ... +} +``` + +## Conclusion + +That's it for all of the fixed-function state! It's a lot of work to set all of +this up from scratch, but the advantage is that we're now nearly fully aware of +everything that is going on in the graphics pipeline! This reduces the chance of +running into unexpected behavior because the default state of certain components +is not what you expect. + +There is however one more object to create before we can finally create the +graphics pipeline and that is a [render pass](!en/Drawing_a_triangle/Graphics_pipeline_basics/Render_passes). + +[C++ code](/code/10_fixed_functions.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md new file mode 100644 index 00000000..a635d32f --- /dev/null +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md @@ -0,0 +1,215 @@ +## Setup + +Before we can finish creating the pipeline, we need to tell Vulkan about the +framebuffer attachments that will be used while rendering. We need to specify +how many color and depth buffers there will be, how many samples to use for each +of them and how their contents should be handled throughout the rendering +operations. All of this information is wrapped in a *render pass* object, for +which we'll create a new `createRenderPass` function. Call this function from +`initVulkan` before `createGraphicsPipeline`. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); +} + +... + +void createRenderPass() { + +} +``` + +## Attachment description + +In our case we'll have just a single color buffer attachment represented by one +of the images from the swap chain. + +```c++ +void createRenderPass() { + VkAttachmentDescription colorAttachment{}; + colorAttachment.format = swapChainImageFormat; + colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; +} +``` + +The `format` of the color attachment should match the format of the swap chain +images, and we're not doing anything with multisampling yet, so we'll stick to 1 +sample. + +```c++ +colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; +colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; +``` + +The `loadOp` and `storeOp` determine what to do with the data in the attachment +before rendering and after rendering. We have the following choices for +`loadOp`: + +* `VK_ATTACHMENT_LOAD_OP_LOAD`: Preserve the existing contents of the attachment +* `VK_ATTACHMENT_LOAD_OP_CLEAR`: Clear the values to a constant at the start +* `VK_ATTACHMENT_LOAD_OP_DONT_CARE`: Existing contents are undefined; we don't +care about them + +In our case we're going to use the clear operation to clear the framebuffer to +black before drawing a new frame. There are only two possibilities for the +`storeOp`: + +* `VK_ATTACHMENT_STORE_OP_STORE`: Rendered contents will be stored in memory and +can be read later +* `VK_ATTACHMENT_STORE_OP_DONT_CARE`: Contents of the framebuffer will be +undefined after the rendering operation + +We're interested in seeing the rendered triangle on the screen, so we're going +with the store operation here. + +```c++ +colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; +colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; +``` + +The `loadOp` and `storeOp` apply to color and depth data, and `stencilLoadOp` / +`stencilStoreOp` apply to stencil data. Our application won't do anything with +the stencil buffer, so the results of loading and storing are irrelevant. + +```c++ +colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; +colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; +``` + +Textures and framebuffers in Vulkan are represented by `VkImage` objects with a +certain pixel format, however the layout of the pixels in memory can change +based on what you're trying to do with an image. + +Some of the most common layouts are: + +* `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`: Images used as color attachment +* `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`: Images to be presented in the swap chain +* `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`: Images to be used as destination for a +memory copy operation + +We'll discuss this topic in more depth in the texturing chapter, but what's +important to know right now is that images need to be transitioned to specific +layouts that are suitable for the operation that they're going to be involved in +next. + +The `initialLayout` specifies which layout the image will have before the render +pass begins. The `finalLayout` specifies the layout to automatically transition +to when the render pass finishes. Using `VK_IMAGE_LAYOUT_UNDEFINED` for +`initialLayout` means that we don't care what previous layout the image was in. +The caveat of this special value is that the contents of the image are not +guaranteed to be preserved, but that doesn't matter since we're going to clear +it anyway. We want the image to be ready for presentation using the swap chain +after rendering, which is why we use `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR` as +`finalLayout`. + +## Subpasses and attachment references + +A single render pass can consist of multiple subpasses. Subpasses are subsequent +rendering operations that depend on the contents of framebuffers in previous +passes, for example a sequence of post-processing effects that are applied one +after another. If you group these rendering operations into one render pass, +then Vulkan is able to reorder the operations and conserve memory bandwidth for +possibly better performance. For our very first triangle, however, we'll stick +to a single subpass. + +Every subpass references one or more of the attachments that we've described +using the structure in the previous sections. These references are themselves +`VkAttachmentReference` structs that look like this: + +```c++ +VkAttachmentReference colorAttachmentRef{}; +colorAttachmentRef.attachment = 0; +colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; +``` + +The `attachment` parameter specifies which attachment to reference by its index +in the attachment descriptions array. Our array consists of a single +`VkAttachmentDescription`, so its index is `0`. The `layout` specifies which +layout we would like the attachment to have during a subpass that uses this +reference. Vulkan will automatically transition the attachment to this layout +when the subpass is started. We intend to use the attachment to function as a +color buffer and the `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL` layout will give +us the best performance, as its name implies. + +The subpass is described using a `VkSubpassDescription` structure: + +```c++ +VkSubpassDescription subpass{}; +subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; +``` + +Vulkan may also support compute subpasses in the future, so we have to be +explicit about this being a graphics subpass. Next, we specify the reference to +the color attachment: + +```c++ +subpass.colorAttachmentCount = 1; +subpass.pColorAttachments = &colorAttachmentRef; +``` + +The index of the attachment in this array is directly referenced from the +fragment shader with the `layout(location = 0) out vec4 outColor` directive! + +The following other types of attachments can be referenced by a subpass: + +* `pInputAttachments`: Attachments that are read from a shader +* `pResolveAttachments`: Attachments used for multisampling color attachments +* `pDepthStencilAttachment`: Attachment for depth and stencil data +* `pPreserveAttachments`: Attachments that are not used by this subpass, but for +which the data must be preserved + +## Render pass + +Now that the attachment and a basic subpass referencing it have been described, +we can create the render pass itself. Create a new class member variable to hold +the `VkRenderPass` object right above the `pipelineLayout` variable: + +```c++ +VkRenderPass renderPass; +VkPipelineLayout pipelineLayout; +``` + +The render pass object can then be created by filling in the +`VkRenderPassCreateInfo` structure with an array of attachments and subpasses. +The `VkAttachmentReference` objects reference attachments using the indices of +this array. + +```c++ +VkRenderPassCreateInfo renderPassInfo{}; +renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; +renderPassInfo.attachmentCount = 1; +renderPassInfo.pAttachments = &colorAttachment; +renderPassInfo.subpassCount = 1; +renderPassInfo.pSubpasses = &subpass; + +if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) { + throw std::runtime_error("failed to create render pass!"); +} +``` + +Just like the pipeline layout, the render pass will be referenced throughout the +program, so it should only be cleaned up at the end: + +```c++ +void cleanup() { + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + vkDestroyRenderPass(device, renderPass, nullptr); + ... +} +``` + +That was a lot of work, but in the next chapter it all comes together to finally +create the graphics pipeline object! + +[C++ code](/code/11_render_passes.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md new file mode 100644 index 00000000..4a16585e --- /dev/null +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md @@ -0,0 +1,122 @@ +We can now combine all of the structures and objects from the previous chapters +to create the graphics pipeline! Here's the types of objects we have now, as a +quick recap: + +* Shader stages: the shader modules that define the functionality of the +programmable stages of the graphics pipeline +* Fixed-function state: all of the structures that define the fixed-function +stages of the pipeline, like input assembly, rasterizer, viewport and color +blending +* Pipeline layout: the uniform and push values referenced by the shader that can +be updated at draw time +* Render pass: the attachments referenced by the pipeline stages and their usage + +All of these combined fully define the functionality of the graphics pipeline, +so we can now begin filling in the `VkGraphicsPipelineCreateInfo` structure at +the end of the `createGraphicsPipeline` function. But before the calls to +`vkDestroyShaderModule` because these are still to be used during the creation. + +```c++ +VkGraphicsPipelineCreateInfo pipelineInfo{}; +pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; +pipelineInfo.stageCount = 2; +pipelineInfo.pStages = shaderStages; +``` + +We start by referencing the array of `VkPipelineShaderStageCreateInfo` structs. + +```c++ +pipelineInfo.pVertexInputState = &vertexInputInfo; +pipelineInfo.pInputAssemblyState = &inputAssembly; +pipelineInfo.pViewportState = &viewportState; +pipelineInfo.pRasterizationState = &rasterizer; +pipelineInfo.pMultisampleState = &multisampling; +pipelineInfo.pDepthStencilState = nullptr; // Optional +pipelineInfo.pColorBlendState = &colorBlending; +pipelineInfo.pDynamicState = &dynamicState; +``` + +Then we reference all of the structures describing the fixed-function stage. + +```c++ +pipelineInfo.layout = pipelineLayout; +``` + +After that comes the pipeline layout, which is a Vulkan handle rather than a +struct pointer. + +```c++ +pipelineInfo.renderPass = renderPass; +pipelineInfo.subpass = 0; +``` + +And finally we have the reference to the render pass and the index of the sub +pass where this graphics pipeline will be used. It is also possible to use other +render passes with this pipeline instead of this specific instance, but they +have to be *compatible* with `renderPass`. The requirements for compatibility +are described [here](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap8.html#renderpass-compatibility), +but we won't be using that feature in this tutorial. + +```c++ +pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional +pipelineInfo.basePipelineIndex = -1; // Optional +``` + +There are actually two more parameters: `basePipelineHandle` and +`basePipelineIndex`. Vulkan allows you to create a new graphics pipeline by +deriving from an existing pipeline. The idea of pipeline derivatives is that it +is less expensive to set up pipelines when they have much functionality in +common with an existing pipeline and switching between pipelines from the same +parent can also be done quicker. You can either specify the handle of an +existing pipeline with `basePipelineHandle` or reference another pipeline that +is about to be created by index with `basePipelineIndex`. Right now there is +only a single pipeline, so we'll simply specify a null handle and an invalid +index. These values are only used if the `VK_PIPELINE_CREATE_DERIVATIVE_BIT` +flag is also specified in the `flags` field of `VkGraphicsPipelineCreateInfo`. + +Now prepare for the final step by creating a class member to hold the +`VkPipeline` object: + +```c++ +VkPipeline graphicsPipeline; +``` + +And finally create the graphics pipeline: + +```c++ +if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) { + throw std::runtime_error("failed to create graphics pipeline!"); +} +``` + +The `vkCreateGraphicsPipelines` function actually has more parameters than the +usual object creation functions in Vulkan. It is designed to take multiple +`VkGraphicsPipelineCreateInfo` objects and create multiple `VkPipeline` objects +in a single call. + +The second parameter, for which we've passed the `VK_NULL_HANDLE` argument, +references an optional `VkPipelineCache` object. A pipeline cache can be used to +store and reuse data relevant to pipeline creation across multiple calls to +`vkCreateGraphicsPipelines` and even across program executions if the cache is +stored to a file. This makes it possible to significantly speed up pipeline +creation at a later time. We'll get into this in the pipeline cache chapter. + +The graphics pipeline is required for all common drawing operations, so it +should also only be destroyed at the end of the program: + +```c++ +void cleanup() { + vkDestroyPipeline(device, graphicsPipeline, nullptr); + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + ... +} +``` + +Now run your program to confirm that all this hard work has resulted in a +successful pipeline creation! We are already getting quite close to seeing +something pop up on the screen. In the next couple of chapters we'll set up the +actual framebuffers from the swap chain images and prepare the drawing commands. + +[C++ code](/code/12_graphics_pipeline_complete.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md b/ko/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md new file mode 100644 index 00000000..bf7f84a7 --- /dev/null +++ b/ko/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md @@ -0,0 +1,107 @@ +We've talked a lot about framebuffers in the past few chapters and we've set up +the render pass to expect a single framebuffer with the same format as the swap +chain images, but we haven't actually created any yet. + +The attachments specified during render pass creation are bound by wrapping them +into a `VkFramebuffer` object. A framebuffer object references all of the +`VkImageView` objects that represent the attachments. In our case that will be +only a single one: the color attachment. However, the image that we have to use +for the attachment depends on which image the swap chain returns when we retrieve one +for presentation. That means that we have to create a framebuffer for all of the +images in the swap chain and use the one that corresponds to the retrieved image +at drawing time. + +To that end, create another `std::vector` class member to hold the framebuffers: + +```c++ +std::vector swapChainFramebuffers; +``` + +We'll create the objects for this array in a new function `createFramebuffers` +that is called from `initVulkan` right after creating the graphics pipeline: + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); +} + +... + +void createFramebuffers() { + +} +``` + +Start by resizing the container to hold all of the framebuffers: + +```c++ +void createFramebuffers() { + swapChainFramebuffers.resize(swapChainImageViews.size()); +} +``` + +We'll then iterate through the image views and create framebuffers from them: + +```c++ +for (size_t i = 0; i < swapChainImageViews.size(); i++) { + VkImageView attachments[] = { + swapChainImageViews[i] + }; + + VkFramebufferCreateInfo framebufferInfo{}; + framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + framebufferInfo.renderPass = renderPass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = attachments; + framebufferInfo.width = swapChainExtent.width; + framebufferInfo.height = swapChainExtent.height; + framebufferInfo.layers = 1; + + if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &swapChainFramebuffers[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create framebuffer!"); + } +} +``` + +As you can see, creation of framebuffers is quite straightforward. We first need +to specify with which `renderPass` the framebuffer needs to be compatible. You +can only use a framebuffer with the render passes that it is compatible with, +which roughly means that they use the same number and type of attachments. + +The `attachmentCount` and `pAttachments` parameters specify the `VkImageView` +objects that should be bound to the respective attachment descriptions in +the render pass `pAttachment` array. + +The `width` and `height` parameters are self-explanatory and `layers` refers to +the number of layers in image arrays. Our swap chain images are single images, +so the number of layers is `1`. + +We should delete the framebuffers before the image views and render pass that +they are based on, but only after we've finished rendering: + +```c++ +void cleanup() { + for (auto framebuffer : swapChainFramebuffers) { + vkDestroyFramebuffer(device, framebuffer, nullptr); + } + + ... +} +``` + +We've now reached the milestone where we have all of the objects that are +required for rendering. In the next chapter we're going to write the first +actual drawing commands. + +[C++ code](/code/13_framebuffers.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md b/ko/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md new file mode 100644 index 00000000..61a40b4f --- /dev/null +++ b/ko/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md @@ -0,0 +1,344 @@ +Commands in Vulkan, like drawing operations and memory transfers, are not +executed directly using function calls. You have to record all of the operations +you want to perform in command buffer objects. The advantage of this is that when +we are ready to tell the Vulkan what we want to do, all of the commands are +submitted together and Vulkan can more efficiently process the commands since all +of them are available together. In addition, this allows command recording to +happen in multiple threads if so desired. + +## Command pools + +We have to create a command pool before we can create command buffers. Command +pools manage the memory that is used to store the buffers and command buffers +are allocated from them. Add a new class member to store a `VkCommandPool`: + +```c++ +VkCommandPool commandPool; +``` + +Then create a new function `createCommandPool` and call it from `initVulkan` +after the framebuffers were created. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); +} + +... + +void createCommandPool() { + +} +``` + +Command pool creation only takes two parameters: + +```c++ +QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice); + +VkCommandPoolCreateInfo poolInfo{}; +poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; +poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; +poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value(); +``` + +There are two possible flags for command pools: + +* `VK_COMMAND_POOL_CREATE_TRANSIENT_BIT`: Hint that command buffers are +rerecorded with new commands very often (may change memory allocation behavior) +* `VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT`: Allow command buffers to be +rerecorded individually, without this flag they all have to be reset together + +We will be recording a command buffer every frame, so we want to be able to +reset and rerecord over it. Thus, we need to set the +`VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT` flag bit for our command pool. + +Command buffers are executed by submitting them on one of the device queues, +like the graphics and presentation queues we retrieved. Each command pool can +only allocate command buffers that are submitted on a single type of queue. +We're going to record commands for drawing, which is why we've chosen the +graphics queue family. + + +```c++ +if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) { + throw std::runtime_error("failed to create command pool!"); +} +``` + +Finish creating the command pool using the `vkCreateCommandPool` function. It +doesn't have any special parameters. Commands will be used throughout the +program to draw things on the screen, so the pool should only be destroyed at +the end: + +```c++ +void cleanup() { + vkDestroyCommandPool(device, commandPool, nullptr); + + ... +} +``` + +## Command buffer allocation + +We can now start allocating command buffers. + +Create a `VkCommandBuffer` object as a class member. Command buffers +will be automatically freed when their command pool is destroyed, so we don't +need explicit cleanup. + +```c++ +VkCommandBuffer commandBuffer; +``` + +We'll now start working on a `createCommandBuffer` function to allocate a single +command buffer from the command pool. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); + createCommandBuffer(); +} + +... + +void createCommandBuffer() { + +} +``` + +Command buffers are allocated with the `vkAllocateCommandBuffers` function, +which takes a `VkCommandBufferAllocateInfo` struct as parameter that specifies +the command pool and number of buffers to allocate: + +```c++ +VkCommandBufferAllocateInfo allocInfo{}; +allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; +allocInfo.commandPool = commandPool; +allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; +allocInfo.commandBufferCount = 1; + +if (vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate command buffers!"); +} +``` + +The `level` parameter specifies if the allocated command buffers are primary or +secondary command buffers. + +* `VK_COMMAND_BUFFER_LEVEL_PRIMARY`: Can be submitted to a queue for execution, +but cannot be called from other command buffers. +* `VK_COMMAND_BUFFER_LEVEL_SECONDARY`: Cannot be submitted directly, but can be +called from primary command buffers. + +We won't make use of the secondary command buffer functionality here, but you +can imagine that it's helpful to reuse common operations from primary command +buffers. + +Since we are only allocating one command buffer, the `commandBufferCount` parameter +is just one. + +## Command buffer recording + +We'll now start working on the `recordCommandBuffer` function that writes the +commands we want to execute into a command buffer. The `VkCommandBuffer` used +will be passed in as a parameter, as well as the index of the current swapchain +image we want to write to. + +```c++ +void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) { + +} +``` + +We always begin recording a command buffer by calling `vkBeginCommandBuffer` +with a small `VkCommandBufferBeginInfo` structure as argument that specifies +some details about the usage of this specific command buffer. + +```c++ +VkCommandBufferBeginInfo beginInfo{}; +beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; +beginInfo.flags = 0; // Optional +beginInfo.pInheritanceInfo = nullptr; // Optional + +if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) { + throw std::runtime_error("failed to begin recording command buffer!"); +} +``` + +The `flags` parameter specifies how we're going to use the command buffer. The +following values are available: + +* `VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT`: The command buffer will be +rerecorded right after executing it once. +* `VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT`: This is a secondary +command buffer that will be entirely within a single render pass. +* `VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT`: The command buffer can be +resubmitted while it is also already pending execution. + +None of these flags are applicable for us right now. + +The `pInheritanceInfo` parameter is only relevant for secondary command buffers. +It specifies which state to inherit from the calling primary command buffers. + +If the command buffer was already recorded once, then a call to +`vkBeginCommandBuffer` will implicitly reset it. It's not possible to append +commands to a buffer at a later time. + +## Starting a render pass + +Drawing starts by beginning the render pass with `vkCmdBeginRenderPass`. The +render pass is configured using some parameters in a `VkRenderPassBeginInfo` +struct. + +```c++ +VkRenderPassBeginInfo renderPassInfo{}; +renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; +renderPassInfo.renderPass = renderPass; +renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex]; +``` + +The first parameters are the render pass itself and the attachments to bind. We +created a framebuffer for each swap chain image where it is specified as a color +attachment. Thus we need to bind the framebuffer for the swapchain image we want +to draw to. Using the imageIndex parameter which was passed in, we can pick the +right framebuffer for the current swapchain image. + +```c++ +renderPassInfo.renderArea.offset = {0, 0}; +renderPassInfo.renderArea.extent = swapChainExtent; +``` + +The next two parameters define the size of the render area. The render area +defines where shader loads and stores will take place. The pixels outside this +region will have undefined values. It should match the size of the attachments +for best performance. + +```c++ +VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}}; +renderPassInfo.clearValueCount = 1; +renderPassInfo.pClearValues = &clearColor; +``` + +The last two parameters define the clear values to use for +`VK_ATTACHMENT_LOAD_OP_CLEAR`, which we used as load operation for the color +attachment. I've defined the clear color to simply be black with 100% opacity. + +```c++ +vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); +``` + +The render pass can now begin. All of the functions that record commands can be +recognized by their `vkCmd` prefix. They all return `void`, so there will be no +error handling until we've finished recording. + +The first parameter for every command is always the command buffer to record the +command to. The second parameter specifies the details of the render pass we've +just provided. The final parameter controls how the drawing commands within the +render pass will be provided. It can have one of two values: + +* `VK_SUBPASS_CONTENTS_INLINE`: The render pass commands will be embedded in +the primary command buffer itself and no secondary command buffers will be +executed. +* `VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS`: The render pass commands will +be executed from secondary command buffers. + +We will not be using secondary command buffers, so we'll go with the first +option. + +## Basic drawing commands + +We can now bind the graphics pipeline: + +```c++ +vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); +``` + +The second parameter specifies if the pipeline object is a graphics or compute +pipeline. We've now told Vulkan which operations to execute in the graphics +pipeline and which attachment to use in the fragment shader. + +As noted in the [fixed functions chapter](../02_Graphics_pipeline_basics/02_Fixed_functions.md#dynamic-state), +we did specify viewport and scissor state for this pipeline to be dynamic. +So we need to set them in the command buffer before issuing our draw command: + +```c++ +VkViewport viewport{}; +viewport.x = 0.0f; +viewport.y = 0.0f; +viewport.width = static_cast(swapChainExtent.width); +viewport.height = static_cast(swapChainExtent.height); +viewport.minDepth = 0.0f; +viewport.maxDepth = 1.0f; +vkCmdSetViewport(commandBuffer, 0, 1, &viewport); + +VkRect2D scissor{}; +scissor.offset = {0, 0}; +scissor.extent = swapChainExtent; +vkCmdSetScissor(commandBuffer, 0, 1, &scissor); +``` + +Now we are ready to issue the draw command for the triangle: + +```c++ +vkCmdDraw(commandBuffer, 3, 1, 0, 0); +``` + +The actual `vkCmdDraw` function is a bit anticlimactic, but it's so simple +because of all the information we specified in advance. It has the following +parameters, aside from the command buffer: + +* `vertexCount`: Even though we don't have a vertex buffer, we technically still +have 3 vertices to draw. +* `instanceCount`: Used for instanced rendering, use `1` if you're not doing +that. +* `firstVertex`: Used as an offset into the vertex buffer, defines the lowest +value of `gl_VertexIndex`. +* `firstInstance`: Used as an offset for instanced rendering, defines the lowest +value of `gl_InstanceIndex`. + +## Finishing up + +The render pass can now be ended: + +```c++ +vkCmdEndRenderPass(commandBuffer); +``` + +And we've finished recording the command buffer: + +```c++ +if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to record command buffer!"); +} +``` + + + +In the next chapter we'll write the code for the main loop, which will acquire +an image from the swap chain, record and execute a command buffer, then return the +finished image to the swap chain. + +[C++ code](/code/14_command_buffers.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md b/ko/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md new file mode 100644 index 00000000..233c059d --- /dev/null +++ b/ko/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md @@ -0,0 +1,577 @@ + +This is the chapter where everything is going to come together. We're going to +write the `drawFrame` function that will be called from the main loop to put the +triangle on the screen. Let's start by creating the function and call it from +`mainLoop`: + +```c++ +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + drawFrame(); + } +} + +... + +void drawFrame() { + +} +``` + +## Outline of a frame + +At a high level, rendering a frame in Vulkan consists of a common set of steps: + +* Wait for the previous frame to finish +* Acquire an image from the swap chain +* Record a command buffer which draws the scene onto that image +* Submit the recorded command buffer +* Present the swap chain image + +While we will expand the drawing function in later chapters, for now this is the +core of our render loop. + + + +## Synchronization + + + +A core design philosophy in Vulkan is that synchronization of execution on +the GPU is explicit. The order of operations is up to us to define using various +synchronization primitives which tell the driver the order we want things to run +in. This means that many Vulkan API calls which start executing work on the GPU +are asynchronous, the functions will return before the operation has finished. + +In this chapter there are a number of events that we need to order explicitly +because they happen on the GPU, such as: + +* Acquire an image from the swap chain +* Execute commands that draw onto the acquired image +* Present that image to the screen for presentation, returning it to the swapchain + +Each of these events is set in motion using a single function call, but are all +executed asynchronously. The function calls will return before the operations +are actually finished and the order of execution is also undefined. That is +unfortunate, because each of the operations depends on the previous one +finishing. Thus we need to explore which primitives we can use to achieve +the desired ordering. + +### Semaphores + +A semaphore is used to add order between queue operations. Queue operations +refer to the work we submit to a queue, either in a command buffer or from +within a function as we will see later. Examples of queues are the graphics +queue and the presentation queue. Semaphores are used both to order work inside +the same queue and between different queues. + +There happens to be two kinds of semaphores in Vulkan, binary and timeline. +Because only binary semaphores will be used in this tutorial, we will not +discuss timeline semaphores. Further mention of the term semaphore exclusively +refers to binary semaphores. + +A semaphore is either unsignaled or signaled. It begins life as unsignaled. The +way we use a semaphore to order queue operations is by providing the same +semaphore as a 'signal' semaphore in one queue operation and as a 'wait' +semaphore in another queue operation. For example, lets say we have semaphore S +and queue operations A and B that we want to execute in order. What we tell +Vulkan is that operation A will 'signal' semaphore S when it finishes executing, +and operation B will 'wait' on semaphore S before it begins executing. When +operation A finishes, semaphore S will be signaled, while operation B wont +start until S is signaled. After operation B begins executing, semaphore S +is automatically reset back to being unsignaled, allowing it to be used again. + +Pseudo-code of what was just described: +``` +VkCommandBuffer A, B = ... // record command buffers +VkSemaphore S = ... // create a semaphore + +// enqueue A, signal S when done - starts executing immediately +vkQueueSubmit(work: A, signal: S, wait: None) + +// enqueue B, wait on S to start +vkQueueSubmit(work: B, signal: None, wait: S) +``` + +Note that in this code snippet, both calls to `vkQueueSubmit()` return +immediately - the waiting only happens on the GPU. The CPU continues running +without blocking. To make the CPU wait, we need a different synchronization +primitive, which we will now describe. + +### Fences + +A fence has a similar purpose, in that it is used to synchronize execution, but +it is for ordering the execution on the CPU, otherwise known as the host. +Simply put, if the host needs to know when the GPU has finished something, we +use a fence. + +Similar to semaphores, fences are either in a signaled or unsignaled state. +Whenever we submit work to execute, we can attach a fence to that work. When +the work is finished, the fence will be signaled. Then we can make the host +wait for the fence to be signaled, guaranteeing that the work has finished +before the host continues. + +A concrete example is taking a screenshot. Say we have already done the +necessary work on the GPU. Now need to transfer the image from the GPU over +to the host and then save the memory to a file. We have command buffer A which +executes the transfer and fence F. We submit command buffer A with fence F, +then immediately tell the host to wait for F to signal. This causes the host to +block until command buffer A finishes execution. Thus we are safe to let the +host save the file to disk, as the memory transfer has completed. + +Pseudo-code for what was described: +``` +VkCommandBuffer A = ... // record command buffer with the transfer +VkFence F = ... // create the fence + +// enqueue A, start work immediately, signal F when done +vkQueueSubmit(work: A, fence: F) + +vkWaitForFence(F) // blocks execution until A has finished executing + +save_screenshot_to_disk() // can't run until the transfer has finished +``` + +Unlike the semaphore example, this example *does* block host execution. This +means the host won't do anything except wait until execution has finished. For +this case, we had to make sure the transfer was complete before we could save +the screenshot to disk. + +In general, it is preferable to not block the host unless necessary. We want to +feed the GPU and the host with useful work to do. Waiting on fences to signal +is not useful work. Thus we prefer semaphores, or other synchronization +primitives not yet covered, to synchronize our work. + +Fences must be reset manually to put them back into the unsignaled state. This +is because fences are used to control the execution of the host, and so the +host gets to decide when to reset the fence. Contrast this to semaphores which +are used to order work on the GPU without the host being involved. + +In summary, semaphores are used to specify the execution order of operations on +the GPU while fences are used to keep the CPU and GPU in sync with each-other. + +### What to choose? + +We have two synchronization primitives to use and conveniently two places to +apply synchronization: Swapchain operations and waiting for the previous frame +to finish. We want to use semaphores for swapchain operations because they +happen on the GPU, thus we don't want to make the host wait around if we can +help it. For waiting on the previous frame to finish, we want to use fences +for the opposite reason, because we need the host to wait. This is so we don't +draw more than one frame at a time. Because we re-record the command buffer +every frame, we cannot record the next frame's work to the command buffer +until the current frame has finished executing, as we don't want to overwrite +the current contents of the command buffer while the GPU is using it. + +## Creating the synchronization objects + +We'll need one semaphore to signal that an image has been acquired from the +swapchain and is ready for rendering, another one to signal that rendering has +finished and presentation can happen, and a fence to make sure only one frame +is rendering at a time. + +Create three class members to store these semaphore objects and fence object: + +```c++ +VkSemaphore imageAvailableSemaphore; +VkSemaphore renderFinishedSemaphore; +VkFence inFlightFence; +``` + +To create the semaphores, we'll add the last `create` function for this part of +the tutorial: `createSyncObjects`: + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); + createCommandBuffer(); + createSyncObjects(); +} + +... + +void createSyncObjects() { + +} +``` + +Creating semaphores requires filling in the `VkSemaphoreCreateInfo`, but in the +current version of the API it doesn't actually have any required fields besides +`sType`: + +```c++ +void createSyncObjects() { + VkSemaphoreCreateInfo semaphoreInfo{}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; +} +``` + +Future versions of the Vulkan API or extensions may add functionality for the +`flags` and `pNext` parameters like it does for the other structures. + +Creating a fence requires filling in the `VkFenceCreateInfo`: + +```c++ +VkFenceCreateInfo fenceInfo{}; +fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; +``` + +Creating the semaphores and fence follows the familiar pattern with +`vkCreateSemaphore` & `vkCreateFence`: + +```c++ +if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphore) != VK_SUCCESS || + vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphore) != VK_SUCCESS || + vkCreateFence(device, &fenceInfo, nullptr, &inFlightFence) != VK_SUCCESS) { + throw std::runtime_error("failed to create semaphores!"); +} +``` + +The semaphores and fence should be cleaned up at the end of the program, when +all commands have finished and no more synchronization is necessary: + +```c++ +void cleanup() { + vkDestroySemaphore(device, imageAvailableSemaphore, nullptr); + vkDestroySemaphore(device, renderFinishedSemaphore, nullptr); + vkDestroyFence(device, inFlightFence, nullptr); +``` + +Onto the main drawing function! + +## Waiting for the previous frame + +At the start of the frame, we want to wait until the previous frame has +finished, so that the command buffer and semaphores are available to use. To do +that, we call `vkWaitForFences`: + +```c++ +void drawFrame() { + vkWaitForFences(device, 1, &inFlightFence, VK_TRUE, UINT64_MAX); +} +``` + +The `vkWaitForFences` function takes an array of fences and waits on the host +for either any or all of the fences to be signaled before returning. The +`VK_TRUE` we pass here indicates that we want to wait for all fences, but in +the case of a single one it doesn't matter. This function also has a timeout +parameter that we set to the maximum value of a 64 bit unsigned integer, +`UINT64_MAX`, which effectively disables the timeout. + +After waiting, we need to manually reset the fence to the unsignaled state with +the `vkResetFences` call: +```c++ + vkResetFences(device, 1, &inFlightFence); +``` + +Before we can proceed, there is a slight hiccup in our design. On the first +frame we call `drawFrame()`, which immediately waits on `inFlightFence` to +be signaled. `inFlightFence` is only signaled after a frame has finished +rendering, yet since this is the first frame, there are no previous frames in +which to signal the fence! Thus `vkWaitForFences()` blocks indefinitely, +waiting on something which will never happen. + +Of the many solutions to this dilemma, there is a clever workaround built into +the API. Create the fence in the signaled state, so that the first call to +`vkWaitForFences()` returns immediately since the fence is already signaled. + +To do this, we add the `VK_FENCE_CREATE_SIGNALED_BIT` flag to the `VkFenceCreateInfo`: + +```c++ +void createSyncObjects() { + ... + + VkFenceCreateInfo fenceInfo{}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + + ... +} +``` + +## Acquiring an image from the swap chain + +The next thing we need to do in the `drawFrame` function is acquire an image +from the swap chain. Recall that the swap chain is an extension feature, so we +must use a function with the `vk*KHR` naming convention: + +```c++ +void drawFrame() { + ... + + uint32_t imageIndex; + vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex); +} +``` + +The first two parameters of `vkAcquireNextImageKHR` are the logical device and +the swap chain from which we wish to acquire an image. The third parameter +specifies a timeout in nanoseconds for an image to become available. Using the +maximum value of a 64 bit unsigned integer means we effectively disable the +timeout. + +The next two parameters specify synchronization objects that are to be signaled +when the presentation engine is finished using the image. That's the point in +time where we can start drawing to it. It is possible to specify a semaphore, +fence or both. We're going to use our `imageAvailableSemaphore` for that purpose +here. + +The last parameter specifies a variable to output the index of the swap chain +image that has become available. The index refers to the `VkImage` in our +`swapChainImages` array. We're going to use that index to pick the `VkFrameBuffer`. + +## Recording the command buffer + +With the imageIndex specifying the swap chain image to use in hand, we can now +record the command buffer. First, we call `vkResetCommandBuffer` on the command +buffer to make sure it is able to be recorded. + +```c++ +vkResetCommandBuffer(commandBuffer, 0); +``` + +The second parameter of `vkResetCommandBuffer` is a `VkCommandBufferResetFlagBits` +flag. Since we don't want to do anything special, we leave it as 0. + +Now call the function `recordCommandBuffer` to record the commands we want. + +```c++ +recordCommandBuffer(commandBuffer, imageIndex); +``` + +With a fully recorded command buffer, we can now submit it. + +## Submitting the command buffer + +Queue submission and synchronization is configured through parameters in the +`VkSubmitInfo` structure. + +```c++ +VkSubmitInfo submitInfo{}; +submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + +VkSemaphore waitSemaphores[] = {imageAvailableSemaphore}; +VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; +submitInfo.waitSemaphoreCount = 1; +submitInfo.pWaitSemaphores = waitSemaphores; +submitInfo.pWaitDstStageMask = waitStages; +``` + +The first three parameters specify which semaphores to wait on before execution +begins and in which stage(s) of the pipeline to wait. We want to wait with +writing colors to the image until it's available, so we're specifying the stage +of the graphics pipeline that writes to the color attachment. That means that +theoretically the implementation can already start executing our vertex shader +and such while the image is not yet available. Each entry in the `waitStages` +array corresponds to the semaphore with the same index in `pWaitSemaphores`. + +```c++ +submitInfo.commandBufferCount = 1; +submitInfo.pCommandBuffers = &commandBuffer; +``` + +The next two parameters specify which command buffers to actually submit for +execution. We simply submit the single command buffer we have. + +```c++ +VkSemaphore signalSemaphores[] = {renderFinishedSemaphore}; +submitInfo.signalSemaphoreCount = 1; +submitInfo.pSignalSemaphores = signalSemaphores; +``` + +The `signalSemaphoreCount` and `pSignalSemaphores` parameters specify which +semaphores to signal once the command buffer(s) have finished execution. In our +case we're using the `renderFinishedSemaphore` for that purpose. + +```c++ +if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFence) != VK_SUCCESS) { + throw std::runtime_error("failed to submit draw command buffer!"); +} +``` + +We can now submit the command buffer to the graphics queue using +`vkQueueSubmit`. The function takes an array of `VkSubmitInfo` structures as +argument for efficiency when the workload is much larger. The last parameter +references an optional fence that will be signaled when the command buffers +finish execution. This allows us to know when it is safe for the command +buffer to be reused, thus we want to give it `inFlightFence`. Now on the next +frame, the CPU will wait for this command buffer to finish executing before it +records new commands into it. + +## Subpass dependencies + +Remember that the subpasses in a render pass automatically take care of image +layout transitions. These transitions are controlled by *subpass dependencies*, +which specify memory and execution dependencies between subpasses. We have only +a single subpass right now, but the operations right before and right after this +subpass also count as implicit "subpasses". + +There are two built-in dependencies that take care of the transition at the +start of the render pass and at the end of the render pass, but the former does +not occur at the right time. It assumes that the transition occurs at the start +of the pipeline, but we haven't acquired the image yet at that point! There are +two ways to deal with this problem. We could change the `waitStages` for the +`imageAvailableSemaphore` to `VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT` to ensure that +the render passes don't begin until the image is available, or we can make the +render pass wait for the `VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT` stage. +I've decided to go with the second option here, because it's a good excuse to +have a look at subpass dependencies and how they work. + +Subpass dependencies are specified in `VkSubpassDependency` structs. Go to the +`createRenderPass` function and add one: + +```c++ +VkSubpassDependency dependency{}; +dependency.srcSubpass = VK_SUBPASS_EXTERNAL; +dependency.dstSubpass = 0; +``` + +The first two fields specify the indices of the dependency and the dependent +subpass. The special value `VK_SUBPASS_EXTERNAL` refers to the implicit subpass +before or after the render pass depending on whether it is specified in +`srcSubpass` or `dstSubpass`. The index `0` refers to our subpass, which is the +first and only one. The `dstSubpass` must always be higher than `srcSubpass` to +prevent cycles in the dependency graph (unless one of the subpasses is +`VK_SUBPASS_EXTERNAL`). + +```c++ +dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; +dependency.srcAccessMask = 0; +``` + +The next two fields specify the operations to wait on and the stages in which +these operations occur. We need to wait for the swap chain to finish reading +from the image before we can access it. This can be accomplished by waiting on +the color attachment output stage itself. + +```c++ +dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; +dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; +``` + +The operations that should wait on this are in the color attachment stage and +involve the writing of the color attachment. These settings will +prevent the transition from happening until it's actually necessary (and +allowed): when we want to start writing colors to it. + +```c++ +renderPassInfo.dependencyCount = 1; +renderPassInfo.pDependencies = &dependency; +``` + +The `VkRenderPassCreateInfo` struct has two fields to specify an array of +dependencies. + +## Presentation + +The last step of drawing a frame is submitting the result back to the swap chain +to have it eventually show up on the screen. Presentation is configured through +a `VkPresentInfoKHR` structure at the end of the `drawFrame` function. + +```c++ +VkPresentInfoKHR presentInfo{}; +presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + +presentInfo.waitSemaphoreCount = 1; +presentInfo.pWaitSemaphores = signalSemaphores; +``` + +The first two parameters specify which semaphores to wait on before presentation +can happen, just like `VkSubmitInfo`. Since we want to wait on the command buffer +to finish execution, thus our triangle being drawn, we take the semaphores +which will be signalled and wait on them, thus we use `signalSemaphores`. + + +```c++ +VkSwapchainKHR swapChains[] = {swapChain}; +presentInfo.swapchainCount = 1; +presentInfo.pSwapchains = swapChains; +presentInfo.pImageIndices = &imageIndex; +``` + +The next two parameters specify the swap chains to present images to and the +index of the image for each swap chain. This will almost always be a single one. + +```c++ +presentInfo.pResults = nullptr; // Optional +``` + +There is one last optional parameter called `pResults`. It allows you to specify +an array of `VkResult` values to check for every individual swap chain if +presentation was successful. It's not necessary if you're only using a single +swap chain, because you can simply use the return value of the present function. + +```c++ +vkQueuePresentKHR(presentQueue, &presentInfo); +``` + +The `vkQueuePresentKHR` function submits the request to present an image to the +swap chain. We'll add error handling for both `vkAcquireNextImageKHR` and +`vkQueuePresentKHR` in the next chapter, because their failure does not +necessarily mean that the program should terminate, unlike the functions we've +seen so far. + +If you did everything correctly up to this point, then you should now see +something resembling the following when you run your program: + +![](/images/triangle.png) + +>This colored triangle may look a bit different from the one you're used to seeing in graphics tutorials. That's because this tutorial lets the shader interpolate in linear color space and converts to sRGB color space afterwards. See [this blog post](https://medium.com/@heypete/hello-triangle-meet-swift-and-wide-color-6f9e246616d9) for a discussion of the difference. + +Yay! Unfortunately, you'll see that when validation layers are enabled, the +program crashes as soon as you close it. The messages printed to the terminal +from `debugCallback` tell us why: + +![](/images/semaphore_in_use.png) + +Remember that all of the operations in `drawFrame` are asynchronous. That means +that when we exit the loop in `mainLoop`, drawing and presentation operations +may still be going on. Cleaning up resources while that is happening is a bad +idea. + +To fix that problem, we should wait for the logical device to finish operations +before exiting `mainLoop` and destroying the window: + +```c++ +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + drawFrame(); + } + + vkDeviceWaitIdle(device); +} +``` + +You can also wait for operations in a specific command queue to be finished with +`vkQueueWaitIdle`. These functions can be used as a very rudimentary way to +perform synchronization. You'll see that the program now exits without problems +when closing the window. + +## Conclusion + +A little over 900 lines of code later, we've finally gotten to the stage of seeing +something pop up on the screen! Bootstrapping a Vulkan program is definitely a +lot of work, but the take-away message is that Vulkan gives you an immense +amount of control through its explicitness. I recommend you to take some time +now to reread the code and build a mental model of the purpose of all of the +Vulkan objects in the program and how they relate to each other. We'll be +building on top of that knowledge to extend the functionality of the program +from this point on. + +The next chapter will expand the render loop to handle multiple frames in flight. + +[C++ code](/code/15_hello_triangle.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md b/ko/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md new file mode 100644 index 00000000..e2345e31 --- /dev/null +++ b/ko/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md @@ -0,0 +1,176 @@ +## Frames in flight + +Right now our render loop has one glaring flaw. We are required to wait on the +previous frame to finish before we can start rendering the next which results +in unnecessary idling of the host. + + + +The way to fix this is to allow multiple frames to be *in-flight* at once, that +is to say, allow the rendering of one frame to not interfere with the recording +of the next. How do we do this? Any resource that is accessed and modified +during rendering must be duplicated. Thus, we need multiple command buffers, +semaphores, and fences. In later chapters we will also add multiple instances +of other resources, so we will see this concept reappear. + +Start by adding a constant at the top of the program that defines how many +frames should be processed concurrently: + +```c++ +const int MAX_FRAMES_IN_FLIGHT = 2; +``` + +We choose the number 2 because we don't want the CPU to get *too* far ahead of +the GPU. With 2 frames in flight, the CPU and the GPU can be working on their +own tasks at the same time. If the CPU finishes early, it will wait till the +GPU finishes rendering before submitting more work. With 3 or more frames in +flight, the CPU could get ahead of the GPU, adding frames of latency. +Generally, extra latency isn't desired. But giving the application control over +the number of frames in flight is another example of Vulkan being explicit. + +Each frame should have its own command buffer, set of semaphores, and fence. +Rename and then change them to be `std::vector`s of the objects: + +```c++ +std::vector commandBuffers; + +... + +std::vector imageAvailableSemaphores; +std::vector renderFinishedSemaphores; +std::vector inFlightFences; +``` + +Then we need to create multiple command buffers. Rename `createCommandBuffer` +to `createCommandBuffers`. Next we need to resize the command buffers vector +to the size of `MAX_FRAMES_IN_FLIGHT`, alter the `VkCommandBufferAllocateInfo` +to contain that many command buffers, and then change the destination to our +vector of command buffers: + +```c++ +void createCommandBuffers() { + commandBuffers.resize(MAX_FRAMES_IN_FLIGHT); + ... + allocInfo.commandBufferCount = (uint32_t) commandBuffers.size(); + + if (vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate command buffers!"); + } +} +``` + +The `createSyncObjects` function should be changed to create all of the objects: + +```c++ +void createSyncObjects() { + imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT); + renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT); + inFlightFences.resize(MAX_FRAMES_IN_FLIGHT); + + VkSemaphoreCreateInfo semaphoreInfo{}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + + VkFenceCreateInfo fenceInfo{}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS || + vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS || + vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) != VK_SUCCESS) { + + throw std::runtime_error("failed to create synchronization objects for a frame!"); + } + } +} +``` + +Similarly, they should also all be cleaned up: + +```c++ +void cleanup() { + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr); + vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr); + vkDestroyFence(device, inFlightFences[i], nullptr); + } + + ... +} +``` + +Remember, because command buffers are freed for us when we free the command +pool, there is nothing extra to do for command buffer cleanup. + +To use the right objects every frame, we need to keep track of the current +frame. We will use a frame index for that purpose: + +```c++ +uint32_t currentFrame = 0; +``` + +The `drawFrame` function can now be modified to use the right objects: + +```c++ +void drawFrame() { + vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); + vkResetFences(device, 1, &inFlightFences[currentFrame]); + + vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); + + ... + + vkResetCommandBuffer(commandBuffers[currentFrame], 0); + recordCommandBuffer(commandBuffers[currentFrame], imageIndex); + + ... + + submitInfo.pCommandBuffers = &commandBuffers[currentFrame]; + + ... + + VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]}; + + ... + + VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]}; + + ... + + if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) { +} +``` + +Of course, we shouldn't forget to advance to the next frame every time: + +```c++ +void drawFrame() { + ... + + currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +} +``` + +By using the modulo (%) operator, we ensure that the frame index loops around +after every `MAX_FRAMES_IN_FLIGHT` enqueued frames. + + + +We've now implemented all the needed synchronization to ensure that there are +no more than `MAX_FRAMES_IN_FLIGHT` frames of work enqueued and that these +frames are not stepping over eachother. Note that it is fine for other parts of +the code, like the final cleanup, to rely on more rough synchronization like +`vkDeviceWaitIdle`. You should decide on which approach to use based on +performance requirements. + +To learn more about synchronization through examples, have a look at [this extensive overview](https://github.com/KhronosGroup/Vulkan-Docs/wiki/Synchronization-Examples#swapchain-image-acquire-and-present) by Khronos. + + +In the next chapter we'll deal with one more small thing that is required for a +well-behaved Vulkan program. + + +[C++ code](/code/16_frames_in_flight.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko/03_Drawing_a_triangle/04_Swap_chain_recreation.md b/ko/03_Drawing_a_triangle/04_Swap_chain_recreation.md new file mode 100644 index 00000000..ce58528b --- /dev/null +++ b/ko/03_Drawing_a_triangle/04_Swap_chain_recreation.md @@ -0,0 +1,280 @@ +## Introduction + +The application we have now successfully draws a triangle, but there are some +circumstances that it isn't handling properly yet. It is possible for the window +surface to change such that the swap chain is no longer compatible with it. One +of the reasons that could cause this to happen is the size of the window +changing. We have to catch these events and recreate the swap chain. + +## Recreating the swap chain + +Create a new `recreateSwapChain` function that calls `createSwapChain` and all +of the creation functions for the objects that depend on the swap chain or the +window size. + +```c++ +void recreateSwapChain() { + vkDeviceWaitIdle(device); + + createSwapChain(); + createImageViews(); + createFramebuffers(); +} +``` + +We first call `vkDeviceWaitIdle`, because just like in the last chapter, we +shouldn't touch resources that may still be in use. Obviously, we'll have to recreate +the swap chain itself. The image views need to be recreated because they are based +directly on the swap chain images. Finally, the framebuffers directly depend on the +swap chain images, and thus must be recreated as well. + +To make sure that the old versions of these objects are cleaned up before +recreating them, we should move some of the cleanup code to a separate function +that we can call from the `recreateSwapChain` function. Let's call it +`cleanupSwapChain`: + +```c++ +void cleanupSwapChain() { + +} + +void recreateSwapChain() { + vkDeviceWaitIdle(device); + + cleanupSwapChain(); + + createSwapChain(); + createImageViews(); + createFramebuffers(); +} +``` + +Note that we don't recreate the renderpass here for simplicity. In theory it can be possible for the swap chain image format to change during an applications' lifetime, e.g. when moving a window from a standard range to a high dynamic range monitor. This may require the application to recreate the renderpass to make sure the change between dynamic ranges is properly reflected. + +We'll move the cleanup code of all objects that are recreated as part of a swap +chain refresh from `cleanup` to `cleanupSwapChain`: + +```c++ +void cleanupSwapChain() { + for (auto framebuffer : swapChainFramebuffers) { + vkDestroyFramebuffer(device, framebuffer, nullptr); + } + + for (auto imageView : swapChainImageViews) { + vkDestroyImageView(device, imageView, nullptr); + } + + vkDestroySwapchainKHR(device, swapChain, nullptr); +} + +void cleanup() { + cleanupSwapChain(); + + vkDestroyPipeline(device, graphicsPipeline, nullptr); + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + + vkDestroyRenderPass(device, renderPass, nullptr); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr); + vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr); + vkDestroyFence(device, inFlightFences[i], nullptr); + } + + vkDestroyCommandPool(device, commandPool, nullptr); + + vkDestroyDevice(device, nullptr); + + if (enableValidationLayers) { + DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr); + } + + vkDestroySurfaceKHR(instance, surface, nullptr); + vkDestroyInstance(instance, nullptr); + + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +Note that in `chooseSwapExtent` we already query the new window resolution to +make sure that the swap chain images have the (new) right size, so there's no +need to modify `chooseSwapExtent` (remember that we already had to use +`glfwGetFramebufferSize` to get the resolution of the surface in pixels when +creating the swap chain). + +That's all it takes to recreate the swap chain! However, the disadvantage of +this approach is that we need to stop all rendering before creating the new swap +chain. It is possible to create a new swap chain while drawing commands on an +image from the old swap chain are still in-flight. You need to pass the previous +swap chain to the `oldSwapChain` field in the `VkSwapchainCreateInfoKHR` struct +and destroy the old swap chain as soon as you've finished using it. + +## Suboptimal or out-of-date swap chain + +Now we just need to figure out when swap chain recreation is necessary and call +our new `recreateSwapChain` function. Luckily, Vulkan will usually just tell us that the swap chain is no longer adequate during presentation. The `vkAcquireNextImageKHR` and +`vkQueuePresentKHR` functions can return the following special values to +indicate this. + +* `VK_ERROR_OUT_OF_DATE_KHR`: The swap chain has become incompatible with the +surface and can no longer be used for rendering. Usually happens after a window resize. +* `VK_SUBOPTIMAL_KHR`: The swap chain can still be used to successfully present +to the surface, but the surface properties are no longer matched exactly. + +```c++ +VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); + +if (result == VK_ERROR_OUT_OF_DATE_KHR) { + recreateSwapChain(); + return; +} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { + throw std::runtime_error("failed to acquire swap chain image!"); +} +``` + +If the swap chain turns out to be out of date when attempting to acquire an +image, then it is no longer possible to present to it. Therefore we should +immediately recreate the swap chain and try again in the next `drawFrame` call. + +You could also decide to do that if the swap chain is suboptimal, but I've +chosen to proceed anyway in that case because we've already acquired an image. +Both `VK_SUCCESS` and `VK_SUBOPTIMAL_KHR` are considered "success" return codes. + +```c++ +result = vkQueuePresentKHR(presentQueue, &presentInfo); + +if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { + recreateSwapChain(); +} else if (result != VK_SUCCESS) { + throw std::runtime_error("failed to present swap chain image!"); +} + +currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +``` + +The `vkQueuePresentKHR` function returns the same values with the same meaning. +In this case we will also recreate the swap chain if it is suboptimal, because +we want the best possible result. + +## Fixing a deadlock + +If we try to run the code now, it is possible to encounter a deadlock. +Debugging the code, we find that the application reaches `vkWaitForFences` but +never continues past it. This is because when `vkAcquireNextImageKHR` returns +`VK_ERROR_OUT_OF_DATE_KHR`, we recreate the swapchain and then return from +`drawFrame`. But before that happens, the current frame's fence was waited upon +and reset. Since we return immediately, no work is submitted for execution and +the fence will never be signaled, causing `vkWaitForFences` to halt forever. + +There is a simple fix thankfully. Delay resetting the fence until after we +know for sure we will be submitting work with it. Thus, if we return early, the +fence is still signaled and `vkWaitForFences` wont deadlock the next time we +use the same fence object. + +The beginning of `drawFrame` should now look like this: +```c++ +vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); + +uint32_t imageIndex; +VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); + +if (result == VK_ERROR_OUT_OF_DATE_KHR) { + recreateSwapChain(); + return; +} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { + throw std::runtime_error("failed to acquire swap chain image!"); +} + +// Only reset the fence if we are submitting work +vkResetFences(device, 1, &inFlightFences[currentFrame]); +``` + +## Handling resizes explicitly + +Although many drivers and platforms trigger `VK_ERROR_OUT_OF_DATE_KHR` automatically after a window resize, it is not guaranteed to happen. That's why we'll add some extra code to also handle resizes explicitly. First add a new member variable that flags that a resize has happened: + +```c++ +std::vector inFlightFences; + +bool framebufferResized = false; +``` + +The `drawFrame` function should then be modified to also check for this flag: + +```c++ +if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) { + framebufferResized = false; + recreateSwapChain(); +} else if (result != VK_SUCCESS) { + ... +} +``` + +It is important to do this after `vkQueuePresentKHR` to ensure that the semaphores are in a consistent state, otherwise a signaled semaphore may never be properly waited upon. Now to actually detect resizes we can use the `glfwSetFramebufferSizeCallback` function in the GLFW framework to set up a callback: + +```c++ +void initWindow() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + + window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); + glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); +} + +static void framebufferResizeCallback(GLFWwindow* window, int width, int height) { + +} +``` + +The reason that we're creating a `static` function as a callback is because GLFW does not know how to properly call a member function with the right `this` pointer to our `HelloTriangleApplication` instance. + +However, we do get a reference to the `GLFWwindow` in the callback and there is another GLFW function that allows you to store an arbitrary pointer inside of it: `glfwSetWindowUserPointer`: + +```c++ +window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); +glfwSetWindowUserPointer(window, this); +glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); +``` + +This value can now be retrieved from within the callback with `glfwGetWindowUserPointer` to properly set the flag: + +```c++ +static void framebufferResizeCallback(GLFWwindow* window, int width, int height) { + auto app = reinterpret_cast(glfwGetWindowUserPointer(window)); + app->framebufferResized = true; +} +``` + +Now try to run the program and resize the window to see if the framebuffer is indeed resized properly with the window. + +## Handling minimization + +There is another case where a swap chain may become out of date and that is a special kind of window resizing: window minimization. This case is special because it will result in a frame buffer size of `0`. In this tutorial we will handle that by pausing until the window is in the foreground again by extending the `recreateSwapChain` function: + +```c++ +void recreateSwapChain() { + int width = 0, height = 0; + glfwGetFramebufferSize(window, &width, &height); + while (width == 0 || height == 0) { + glfwGetFramebufferSize(window, &width, &height); + glfwWaitEvents(); + } + + vkDeviceWaitIdle(device); + + ... +} +``` + +The initial call to `glfwGetFramebufferSize` handles the case where the size is already correct and `glfwWaitEvents` would have nothing to wait on. + +Congratulations, you've now finished your very first well-behaved Vulkan +program! In the next chapter we're going to get rid of the hardcoded vertices in +the vertex shader and actually use a vertex buffer. + +[C++ code](/code/17_swap_chain_recreation.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/ko/04_Vertex_buffers/00_Vertex_input_description.md b/ko/04_Vertex_buffers/00_Vertex_input_description.md new file mode 100644 index 00000000..e7da3e4f --- /dev/null +++ b/ko/04_Vertex_buffers/00_Vertex_input_description.md @@ -0,0 +1,225 @@ +## Introduction + +In the next few chapters, we're going to replace the hardcoded vertex data in +the vertex shader with a vertex buffer in memory. We'll start with the easiest +approach of creating a CPU visible buffer and using `memcpy` to copy the vertex +data into it directly, and after that we'll see how to use a staging buffer to +copy the vertex data to high performance memory. + +## Vertex shader + +First change the vertex shader to no longer include the vertex data in the +shader code itself. The vertex shader takes input from a vertex buffer using the +`in` keyword. + +```glsl +#version 450 + +layout(location = 0) in vec2 inPosition; +layout(location = 1) in vec3 inColor; + +layout(location = 0) out vec3 fragColor; + +void main() { + gl_Position = vec4(inPosition, 0.0, 1.0); + fragColor = inColor; +} +``` + +The `inPosition` and `inColor` variables are *vertex attributes*. They're +properties that are specified per-vertex in the vertex buffer, just like we +manually specified a position and color per vertex using the two arrays. Make +sure to recompile the vertex shader! + +Just like `fragColor`, the `layout(location = x)` annotations assign indices to +the inputs that we can later use to reference them. It is important to know that +some types, like `dvec3` 64 bit vectors, use multiple *slots*. That means that +the index after it must be at least 2 higher: + +```glsl +layout(location = 0) in dvec3 inPosition; +layout(location = 2) in vec3 inColor; +``` + +You can find more info about the layout qualifier in the [OpenGL wiki](https://www.khronos.org/opengl/wiki/Layout_Qualifier_(GLSL)). + +## Vertex data + +We're moving the vertex data from the shader code to an array in the code of our +program. Start by including the GLM library, which provides us with linear +algebra related types like vectors and matrices. We're going to use these types +to specify the position and color vectors. + +```c++ +#include +``` + +Create a new structure called `Vertex` with the two attributes that we're going +to use in the vertex shader inside it: + +```c++ +struct Vertex { + glm::vec2 pos; + glm::vec3 color; +}; +``` + +GLM conveniently provides us with C++ types that exactly match the vector types +used in the shader language. + +```c++ +const std::vector vertices = { + {{0.0f, -0.5f}, {1.0f, 0.0f, 0.0f}}, + {{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}}, + {{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}} +}; +``` + +Now use the `Vertex` structure to specify an array of vertex data. We're using +exactly the same position and color values as before, but now they're combined +into one array of vertices. This is known as *interleaving* vertex attributes. + +## Binding descriptions + +The next step is to tell Vulkan how to pass this data format to the vertex +shader once it's been uploaded into GPU memory. There are two types of +structures needed to convey this information. + +The first structure is `VkVertexInputBindingDescription` and we'll add a member +function to the `Vertex` struct to populate it with the right data. + +```c++ +struct Vertex { + glm::vec2 pos; + glm::vec3 color; + + static VkVertexInputBindingDescription getBindingDescription() { + VkVertexInputBindingDescription bindingDescription{}; + + return bindingDescription; + } +}; +``` + +A vertex binding describes at which rate to load data from memory throughout the +vertices. It specifies the number of bytes between data entries and whether to +move to the next data entry after each vertex or after each instance. + +```c++ +VkVertexInputBindingDescription bindingDescription{}; +bindingDescription.binding = 0; +bindingDescription.stride = sizeof(Vertex); +bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; +``` + +All of our per-vertex data is packed together in one array, so we're only going +to have one binding. The `binding` parameter specifies the index of the binding +in the array of bindings. The `stride` parameter specifies the number of bytes +from one entry to the next, and the `inputRate` parameter can have one of the +following values: + +* `VK_VERTEX_INPUT_RATE_VERTEX`: Move to the next data entry after each vertex +* `VK_VERTEX_INPUT_RATE_INSTANCE`: Move to the next data entry after each +instance + +We're not going to use instanced rendering, so we'll stick to per-vertex data. + +## Attribute descriptions + +The second structure that describes how to handle vertex input is +`VkVertexInputAttributeDescription`. We're going to add another helper function +to `Vertex` to fill in these structs. + +```c++ +#include + +... + +static std::array getAttributeDescriptions() { + std::array attributeDescriptions{}; + + return attributeDescriptions; +} +``` + +As the function prototype indicates, there are going to be two of these +structures. An attribute description struct describes how to extract a vertex +attribute from a chunk of vertex data originating from a binding description. We +have two attributes, position and color, so we need two attribute description +structs. + +```c++ +attributeDescriptions[0].binding = 0; +attributeDescriptions[0].location = 0; +attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; +attributeDescriptions[0].offset = offsetof(Vertex, pos); +``` + +The `binding` parameter tells Vulkan from which binding the per-vertex data +comes. The `location` parameter references the `location` directive of the +input in the vertex shader. The input in the vertex shader with location `0` is +the position, which has two 32-bit float components. + +The `format` parameter describes the type of data for the attribute. A bit +confusingly, the formats are specified using the same enumeration as color +formats. The following shader types and formats are commonly used together: + +* `float`: `VK_FORMAT_R32_SFLOAT` +* `vec2`: `VK_FORMAT_R32G32_SFLOAT` +* `vec3`: `VK_FORMAT_R32G32B32_SFLOAT` +* `vec4`: `VK_FORMAT_R32G32B32A32_SFLOAT` + +As you can see, you should use the format where the amount of color channels +matches the number of components in the shader data type. It is allowed to use +more channels than the number of components in the shader, but they will be +silently discarded. If the number of channels is lower than the number of +components, then the BGA components will use default values of `(0, 0, 1)`. The +color type (`SFLOAT`, `UINT`, `SINT`) and bit width should also match the type +of the shader input. See the following examples: + +* `ivec2`: `VK_FORMAT_R32G32_SINT`, a 2-component vector of 32-bit signed +integers +* `uvec4`: `VK_FORMAT_R32G32B32A32_UINT`, a 4-component vector of 32-bit +unsigned integers +* `double`: `VK_FORMAT_R64_SFLOAT`, a double-precision (64-bit) float + +The `format` parameter implicitly defines the byte size of attribute data and +the `offset` parameter specifies the number of bytes since the start of the +per-vertex data to read from. The binding is loading one `Vertex` at a time and +the position attribute (`pos`) is at an offset of `0` bytes from the beginning +of this struct. This is automatically calculated using the `offsetof` macro. + +```c++ +attributeDescriptions[1].binding = 0; +attributeDescriptions[1].location = 1; +attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; +attributeDescriptions[1].offset = offsetof(Vertex, color); +``` + +The color attribute is described in much the same way. + +## Pipeline vertex input + +We now need to set up the graphics pipeline to accept vertex data in this format +by referencing the structures in `createGraphicsPipeline`. Find the +`vertexInputInfo` struct and modify it to reference the two descriptions: + +```c++ +auto bindingDescription = Vertex::getBindingDescription(); +auto attributeDescriptions = Vertex::getAttributeDescriptions(); + +vertexInputInfo.vertexBindingDescriptionCount = 1; +vertexInputInfo.vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()); +vertexInputInfo.pVertexBindingDescriptions = &bindingDescription; +vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data(); +``` + +The pipeline is now ready to accept vertex data in the format of the `vertices` +container and pass it on to our vertex shader. If you run the program now with +validation layers enabled, you'll see that it complains that there is no vertex +buffer bound to the binding. The next step is to create a vertex buffer and move +the vertex data to it so the GPU is able to access it. + +[C++ code](/code/18_vertex_input.cpp) / +[Vertex shader](/code/18_shader_vertexbuffer.vert) / +[Fragment shader](/code/18_shader_vertexbuffer.frag) diff --git a/ko/04_Vertex_buffers/01_Vertex_buffer_creation.md b/ko/04_Vertex_buffers/01_Vertex_buffer_creation.md new file mode 100644 index 00000000..77122c50 --- /dev/null +++ b/ko/04_Vertex_buffers/01_Vertex_buffer_creation.md @@ -0,0 +1,342 @@ +## Introduction + +Buffers in Vulkan are regions of memory used for storing arbitrary data that can +be read by the graphics card. They can be used to store vertex data, which we'll +do in this chapter, but they can also be used for many other purposes that we'll +explore in future chapters. Unlike the Vulkan objects we've been dealing with so +far, buffers do not automatically allocate memory for themselves. The work from +the previous chapters has shown that the Vulkan API puts the programmer in +control of almost everything and memory management is one of those things. + +## Buffer creation + +Create a new function `createVertexBuffer` and call it from `initVulkan` right +before `createCommandBuffers`. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); + createVertexBuffer(); + createCommandBuffers(); + createSyncObjects(); +} + +... + +void createVertexBuffer() { + +} +``` + +Creating a buffer requires us to fill a `VkBufferCreateInfo` structure. + +```c++ +VkBufferCreateInfo bufferInfo{}; +bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; +bufferInfo.size = sizeof(vertices[0]) * vertices.size(); +``` + +The first field of the struct is `size`, which specifies the size of the buffer +in bytes. Calculating the byte size of the vertex data is straightforward with +`sizeof`. + +```c++ +bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; +``` + +The second field is `usage`, which indicates for which purposes the data in the +buffer is going to be used. It is possible to specify multiple purposes using a +bitwise or. Our use case will be a vertex buffer, we'll look at other types of +usage in future chapters. + +```c++ +bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; +``` + +Just like the images in the swap chain, buffers can also be owned by a specific +queue family or be shared between multiple at the same time. The buffer will +only be used from the graphics queue, so we can stick to exclusive access. + +The `flags` parameter is used to configure sparse buffer memory, which is not +relevant right now. We'll leave it at the default value of `0`. + +We can now create the buffer with `vkCreateBuffer`. Define a class member to +hold the buffer handle and call it `vertexBuffer`. + +```c++ +VkBuffer vertexBuffer; + +... + +void createVertexBuffer() { + VkBufferCreateInfo bufferInfo{}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = sizeof(vertices[0]) * vertices.size(); + bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateBuffer(device, &bufferInfo, nullptr, &vertexBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to create vertex buffer!"); + } +} +``` + +The buffer should be available for use in rendering commands until the end of +the program and it does not depend on the swap chain, so we'll clean it up in +the original `cleanup` function: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyBuffer(device, vertexBuffer, nullptr); + + ... +} +``` + +## Memory requirements + +The buffer has been created, but it doesn't actually have any memory assigned to +it yet. The first step of allocating memory for the buffer is to query its +memory requirements using the aptly named `vkGetBufferMemoryRequirements` +function. + +```c++ +VkMemoryRequirements memRequirements; +vkGetBufferMemoryRequirements(device, vertexBuffer, &memRequirements); +``` + +The `VkMemoryRequirements` struct has three fields: + +* `size`: The size of the required amount of memory in bytes, may differ from +`bufferInfo.size`. +* `alignment`: The offset in bytes where the buffer begins in the allocated +region of memory, depends on `bufferInfo.usage` and `bufferInfo.flags`. +* `memoryTypeBits`: Bit field of the memory types that are suitable for the +buffer. + +Graphics cards can offer different types of memory to allocate from. Each type +of memory varies in terms of allowed operations and performance characteristics. +We need to combine the requirements of the buffer and our own application +requirements to find the right type of memory to use. Let's create a new +function `findMemoryType` for this purpose. + +```c++ +uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) { + +} +``` + +First we need to query info about the available types of memory using +`vkGetPhysicalDeviceMemoryProperties`. + +```c++ +VkPhysicalDeviceMemoryProperties memProperties; +vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties); +``` + +The `VkPhysicalDeviceMemoryProperties` structure has two arrays `memoryTypes` +and `memoryHeaps`. Memory heaps are distinct memory resources like dedicated +VRAM and swap space in RAM for when VRAM runs out. The different types of memory +exist within these heaps. Right now we'll only concern ourselves with the type +of memory and not the heap it comes from, but you can imagine that this can +affect performance. + +Let's first find a memory type that is suitable for the buffer itself: + +```c++ +for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if (typeFilter & (1 << i)) { + return i; + } +} + +throw std::runtime_error("failed to find suitable memory type!"); +``` + +The `typeFilter` parameter will be used to specify the bit field of memory types +that are suitable. That means that we can find the index of a suitable memory +type by simply iterating over them and checking if the corresponding bit is set +to `1`. + +However, we're not just interested in a memory type that is suitable for the +vertex buffer. We also need to be able to write our vertex data to that memory. +The `memoryTypes` array consists of `VkMemoryType` structs that specify the heap +and properties of each type of memory. The properties define special features +of the memory, like being able to map it so we can write to it from the CPU. +This property is indicated with `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT`, but we +also need to use the `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` property. We'll see +why when we map the memory. + +We can now modify the loop to also check for the support of this property: + +```c++ +for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } +} +``` + +We may have more than one desirable property, so we should check if the result +of the bitwise AND is not just non-zero, but equal to the desired properties bit +field. If there is a memory type suitable for the buffer that also has all of +the properties we need, then we return its index, otherwise we throw an +exception. + +## Memory allocation + +We now have a way to determine the right memory type, so we can actually +allocate the memory by filling in the `VkMemoryAllocateInfo` structure. + +```c++ +VkMemoryAllocateInfo allocInfo{}; +allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; +allocInfo.allocationSize = memRequirements.size; +allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); +``` + +Memory allocation is now as simple as specifying the size and type, both of +which are derived from the memory requirements of the vertex buffer and the +desired property. Create a class member to store the handle to the memory and +allocate it with `vkAllocateMemory`. + +```c++ +VkBuffer vertexBuffer; +VkDeviceMemory vertexBufferMemory; + +... + +if (vkAllocateMemory(device, &allocInfo, nullptr, &vertexBufferMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate vertex buffer memory!"); +} +``` + +If memory allocation was successful, then we can now associate this memory with +the buffer using `vkBindBufferMemory`: + +```c++ +vkBindBufferMemory(device, vertexBuffer, vertexBufferMemory, 0); +``` + +The first three parameters are self-explanatory and the fourth parameter is the +offset within the region of memory. Since this memory is allocated specifically +for this the vertex buffer, the offset is simply `0`. If the offset is non-zero, +then it is required to be divisible by `memRequirements.alignment`. + +Of course, just like dynamic memory allocation in C++, the memory should be +freed at some point. Memory that is bound to a buffer object may be freed once +the buffer is no longer used, so let's free it after the buffer has been +destroyed: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyBuffer(device, vertexBuffer, nullptr); + vkFreeMemory(device, vertexBufferMemory, nullptr); +``` + +## Filling the vertex buffer + +It is now time to copy the vertex data to the buffer. This is done by [mapping +the buffer memory](https://en.wikipedia.org/wiki/Memory-mapped_I/O) into CPU +accessible memory with `vkMapMemory`. + +```c++ +void* data; +vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data); +``` + +This function allows us to access a region of the specified memory resource +defined by an offset and size. The offset and size here are `0` and +`bufferInfo.size`, respectively. It is also possible to specify the special +value `VK_WHOLE_SIZE` to map all of the memory. The second to last parameter can +be used to specify flags, but there aren't any available yet in the current API. +It must be set to the value `0`. The last parameter specifies the output for the +pointer to the mapped memory. + +```c++ +void* data; +vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data); + memcpy(data, vertices.data(), (size_t) bufferInfo.size); +vkUnmapMemory(device, vertexBufferMemory); +``` + +You can now simply `memcpy` the vertex data to the mapped memory and unmap it +again using `vkUnmapMemory`. Unfortunately the driver may not immediately copy +the data into the buffer memory, for example because of caching. It is also +possible that writes to the buffer are not visible in the mapped memory yet. +There are two ways to deal with that problem: + +* Use a memory heap that is host coherent, indicated with +`VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` +* Call `vkFlushMappedMemoryRanges` after writing to the mapped memory, and +call `vkInvalidateMappedMemoryRanges` before reading from the mapped memory + +We went for the first approach, which ensures that the mapped memory always +matches the contents of the allocated memory. Do keep in mind that this may lead +to slightly worse performance than explicit flushing, but we'll see why that +doesn't matter in the next chapter. + +Flushing memory ranges or using a coherent memory heap means that the driver will be aware of our writes to the buffer, but it doesn't mean that they are actually visible on the GPU yet. The transfer of data to the GPU is an operation that happens in the background and the specification simply [tells us](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-submission-host-writes) that it is guaranteed to be complete as of the next call to `vkQueueSubmit`. + +## Binding the vertex buffer + +All that remains now is binding the vertex buffer during rendering operations. +We're going to extend the `recordCommandBuffer` function to do that. + +```c++ +vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); + +VkBuffer vertexBuffers[] = {vertexBuffer}; +VkDeviceSize offsets[] = {0}; +vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets); + +vkCmdDraw(commandBuffer, static_cast(vertices.size()), 1, 0, 0); +``` + +The `vkCmdBindVertexBuffers` function is used to bind vertex buffers to +bindings, like the one we set up in the previous chapter. The first two +parameters, besides the command buffer, specify the offset and number of +bindings we're going to specify vertex buffers for. The last two parameters +specify the array of vertex buffers to bind and the byte offsets to start +reading vertex data from. You should also change the call to `vkCmdDraw` to pass +the number of vertices in the buffer as opposed to the hardcoded number `3`. + +Now run the program and you should see the familiar triangle again: + +![](/images/triangle.png) + +Try changing the color of the top vertex to white by modifying the `vertices` +array: + +```c++ +const std::vector vertices = { + {{0.0f, -0.5f}, {1.0f, 1.0f, 1.0f}}, + {{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}}, + {{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}} +}; +``` + +Run the program again and you should see the following: + +![](/images/triangle_white.png) + +In the next chapter we'll look at a different way to copy vertex data to a +vertex buffer that results in better performance, but takes some more work. + +[C++ code](/code/19_vertex_buffer.cpp) / +[Vertex shader](/code/18_shader_vertexbuffer.vert) / +[Fragment shader](/code/18_shader_vertexbuffer.frag) diff --git a/ko/04_Vertex_buffers/02_Staging_buffer.md b/ko/04_Vertex_buffers/02_Staging_buffer.md new file mode 100644 index 00000000..289e74d4 --- /dev/null +++ b/ko/04_Vertex_buffers/02_Staging_buffer.md @@ -0,0 +1,267 @@ +## Introduction + +The vertex buffer we have right now works correctly, but the memory type that +allows us to access it from the CPU may not be the most optimal memory type for +the graphics card itself to read from. The most optimal memory has the +`VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT` flag and is usually not accessible by the +CPU on dedicated graphics cards. In this chapter we're going to create two +vertex buffers. One *staging buffer* in CPU accessible memory to upload the data +from the vertex array to, and the final vertex buffer in device local memory. +We'll then use a buffer copy command to move the data from the staging buffer to +the actual vertex buffer. + +## Transfer queue + +The buffer copy command requires a queue family that supports transfer +operations, which is indicated using `VK_QUEUE_TRANSFER_BIT`. The good news is +that any queue family with `VK_QUEUE_GRAPHICS_BIT` or `VK_QUEUE_COMPUTE_BIT` +capabilities already implicitly support `VK_QUEUE_TRANSFER_BIT` operations. The +implementation is not required to explicitly list it in `queueFlags` in those +cases. + +If you like a challenge, then you can still try to use a different queue family +specifically for transfer operations. It will require you to make the following +modifications to your program: + +* Modify `QueueFamilyIndices` and `findQueueFamilies` to explicitly look for a +queue family with the `VK_QUEUE_TRANSFER_BIT` bit, but not the +`VK_QUEUE_GRAPHICS_BIT`. +* Modify `createLogicalDevice` to request a handle to the transfer queue +* Create a second command pool for command buffers that are submitted on the +transfer queue family +* Change the `sharingMode` of resources to be `VK_SHARING_MODE_CONCURRENT` and +specify both the graphics and transfer queue families +* Submit any transfer commands like `vkCmdCopyBuffer` (which we'll be using in +this chapter) to the transfer queue instead of the graphics queue + +It's a bit of work, but it'll teach you a lot about how resources are shared +between queue families. + +## Abstracting buffer creation + +Because we're going to create multiple buffers in this chapter, it's a good idea +to move buffer creation to a helper function. Create a new function +`createBuffer` and move the code in `createVertexBuffer` (except mapping) to it. + +```c++ +void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, VkDeviceMemory& bufferMemory) { + VkBufferCreateInfo bufferInfo{}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = size; + bufferInfo.usage = usage; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) { + throw std::runtime_error("failed to create buffer!"); + } + + VkMemoryRequirements memRequirements; + vkGetBufferMemoryRequirements(device, buffer, &memRequirements); + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); + + if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate buffer memory!"); + } + + vkBindBufferMemory(device, buffer, bufferMemory, 0); +} +``` + +Make sure to add parameters for the buffer size, memory properties and usage so +that we can use this function to create many different types of buffers. The +last two parameters are output variables to write the handles to. + +You can now remove the buffer creation and memory allocation code from +`createVertexBuffer` and just call `createBuffer` instead: + +```c++ +void createVertexBuffer() { + VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); + createBuffer(bufferSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, vertexBuffer, vertexBufferMemory); + + void* data; + vkMapMemory(device, vertexBufferMemory, 0, bufferSize, 0, &data); + memcpy(data, vertices.data(), (size_t) bufferSize); + vkUnmapMemory(device, vertexBufferMemory); +} +``` + +Run your program to make sure that the vertex buffer still works properly. + +## Using a staging buffer + +We're now going to change `createVertexBuffer` to only use a host visible buffer +as temporary buffer and use a device local one as actual vertex buffer. + +```c++ +void createVertexBuffer() { + VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); + + VkBuffer stagingBuffer; + VkDeviceMemory stagingBufferMemory; + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); + + void* data; + vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data); + memcpy(data, vertices.data(), (size_t) bufferSize); + vkUnmapMemory(device, stagingBufferMemory); + + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory); +} +``` + +We're now using a new `stagingBuffer` with `stagingBufferMemory` for mapping and +copying the vertex data. In this chapter we're going to use two new buffer usage +flags: + +* `VK_BUFFER_USAGE_TRANSFER_SRC_BIT`: Buffer can be used as source in a memory +transfer operation. +* `VK_BUFFER_USAGE_TRANSFER_DST_BIT`: Buffer can be used as destination in a +memory transfer operation. + +The `vertexBuffer` is now allocated from a memory type that is device local, +which generally means that we're not able to use `vkMapMemory`. However, we can +copy data from the `stagingBuffer` to the `vertexBuffer`. We have to indicate +that we intend to do that by specifying the transfer source flag for the +`stagingBuffer` and the transfer destination flag for the `vertexBuffer`, along +with the vertex buffer usage flag. + +We're now going to write a function to copy the contents from one buffer to +another, called `copyBuffer`. + +```c++ +void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { + +} +``` + +Memory transfer operations are executed using command buffers, just like drawing +commands. Therefore we must first allocate a temporary command buffer. You may +wish to create a separate command pool for these kinds of short-lived buffers, +because the implementation may be able to apply memory allocation optimizations. +You should use the `VK_COMMAND_POOL_CREATE_TRANSIENT_BIT` flag during command +pool generation in that case. + +```c++ +void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandPool = commandPool; + allocInfo.commandBufferCount = 1; + + VkCommandBuffer commandBuffer; + vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); +} +``` + +And immediately start recording the command buffer: + +```c++ +VkCommandBufferBeginInfo beginInfo{}; +beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; +beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + +vkBeginCommandBuffer(commandBuffer, &beginInfo); +``` + +We're only going to use the command buffer once and wait with returning from the function until the copy +operation has finished executing. It's good practice to tell the driver about +our intent using `VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT`. + +```c++ +VkBufferCopy copyRegion{}; +copyRegion.srcOffset = 0; // Optional +copyRegion.dstOffset = 0; // Optional +copyRegion.size = size; +vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region); +``` + +Contents of buffers are transferred using the `vkCmdCopyBuffer` command. It +takes the source and destination buffers as arguments, and an array of regions +to copy. The regions are defined in `VkBufferCopy` structs and consist of a +source buffer offset, destination buffer offset and size. It is not possible to +specify `VK_WHOLE_SIZE` here, unlike the `vkMapMemory` command. + +```c++ +vkEndCommandBuffer(commandBuffer); +``` + +This command buffer only contains the copy command, so we can stop recording +right after that. Now execute the command buffer to complete the transfer: + +```c++ +VkSubmitInfo submitInfo{}; +submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; +submitInfo.commandBufferCount = 1; +submitInfo.pCommandBuffers = &commandBuffer; + +vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); +vkQueueWaitIdle(graphicsQueue); +``` + +Unlike the draw commands, there are no events we need to wait on this time. We +just want to execute the transfer on the buffers immediately. There are again +two possible ways to wait on this transfer to complete. We could use a fence and +wait with `vkWaitForFences`, or simply wait for the transfer queue to become +idle with `vkQueueWaitIdle`. A fence would allow you to schedule multiple +transfers simultaneously and wait for all of them complete, instead of executing +one at a time. That may give the driver more opportunities to optimize. + +```c++ +vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); +``` + +Don't forget to clean up the command buffer used for the transfer operation. + +We can now call `copyBuffer` from the `createVertexBuffer` function to move the +vertex data to the device local buffer: + +```c++ +createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory); + +copyBuffer(stagingBuffer, vertexBuffer, bufferSize); +``` + +After copying the data from the staging buffer to the device buffer, we should +clean it up: + +```c++ + ... + + copyBuffer(stagingBuffer, vertexBuffer, bufferSize); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingBufferMemory, nullptr); +} +``` + +Run your program to verify that you're seeing the familiar triangle again. The +improvement may not be visible right now, but its vertex data is now being +loaded from high performance memory. This will matter when we're going to start +rendering more complex geometry. + +## Conclusion + +It should be noted that in a real world application, you're not supposed to +actually call `vkAllocateMemory` for every individual buffer. The maximum number +of simultaneous memory allocations is limited by the `maxMemoryAllocationCount` +physical device limit, which may be as low as `4096` even on high end hardware +like an NVIDIA GTX 1080. The right way to allocate memory for a large number of +objects at the same time is to create a custom allocator that splits up a single +allocation among many different objects by using the `offset` parameters that +we've seen in many functions. + +You can either implement such an allocator yourself, or use the +[VulkanMemoryAllocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator) +library provided by the GPUOpen initiative. However, for this tutorial it's okay +to use a separate allocation for every resource, because we won't come close to +hitting any of these limits for now. + +[C++ code](/code/20_staging_buffer.cpp) / +[Vertex shader](/code/18_shader_vertexbuffer.vert) / +[Fragment shader](/code/18_shader_vertexbuffer.frag) diff --git a/ko/04_Vertex_buffers/03_Index_buffer.md b/ko/04_Vertex_buffers/03_Index_buffer.md new file mode 100644 index 00000000..088263db --- /dev/null +++ b/ko/04_Vertex_buffers/03_Index_buffer.md @@ -0,0 +1,179 @@ +## Introduction + +The 3D meshes you'll be rendering in a real world application will often share +vertices between multiple triangles. This already happens even with something +simple like drawing a rectangle: + +![](/images/vertex_vs_index.svg) + +Drawing a rectangle takes two triangles, which means that we need a vertex +buffer with 6 vertices. The problem is that the data of two vertices needs to be +duplicated resulting in 50% redundancy. It only gets worse with more complex +meshes, where vertices are reused in an average number of 3 triangles. The +solution to this problem is to use an *index buffer*. + +An index buffer is essentially an array of pointers into the vertex buffer. It +allows you to reorder the vertex data, and reuse existing data for multiple +vertices. The illustration above demonstrates what the index buffer would look +like for the rectangle if we have a vertex buffer containing each of the four +unique vertices. The first three indices define the upper-right triangle and the +last three indices define the vertices for the bottom-left triangle. + +## Index buffer creation + +In this chapter we're going to modify the vertex data and add index data to +draw a rectangle like the one in the illustration. Modify the vertex data to +represent the four corners: + +```c++ +const std::vector vertices = { + {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}}, + {{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}}, + {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}}, + {{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}} +}; +``` + +The top-left corner is red, top-right is green, bottom-right is blue and the +bottom-left is white. We'll add a new array `indices` to represent the contents +of the index buffer. It should match the indices in the illustration to draw the +upper-right triangle and bottom-left triangle. + +```c++ +const std::vector indices = { + 0, 1, 2, 2, 3, 0 +}; +``` + +It is possible to use either `uint16_t` or `uint32_t` for your index buffer +depending on the number of entries in `vertices`. We can stick to `uint16_t` for +now because we're using less than 65535 unique vertices. + +Just like the vertex data, the indices need to be uploaded into a `VkBuffer` for +the GPU to be able to access them. Define two new class members to hold the +resources for the index buffer: + +```c++ +VkBuffer vertexBuffer; +VkDeviceMemory vertexBufferMemory; +VkBuffer indexBuffer; +VkDeviceMemory indexBufferMemory; +``` + +The `createIndexBuffer` function that we'll add now is almost identical to +`createVertexBuffer`: + +```c++ +void initVulkan() { + ... + createVertexBuffer(); + createIndexBuffer(); + ... +} + +void createIndexBuffer() { + VkDeviceSize bufferSize = sizeof(indices[0]) * indices.size(); + + VkBuffer stagingBuffer; + VkDeviceMemory stagingBufferMemory; + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); + + void* data; + vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data); + memcpy(data, indices.data(), (size_t) bufferSize); + vkUnmapMemory(device, stagingBufferMemory); + + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, indexBuffer, indexBufferMemory); + + copyBuffer(stagingBuffer, indexBuffer, bufferSize); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingBufferMemory, nullptr); +} +``` + +There are only two notable differences. The `bufferSize` is now equal to the +number of indices times the size of the index type, either `uint16_t` or +`uint32_t`. The usage of the `indexBuffer` should be +`VK_BUFFER_USAGE_INDEX_BUFFER_BIT` instead of +`VK_BUFFER_USAGE_VERTEX_BUFFER_BIT`, which makes sense. Other than that, the +process is exactly the same. We create a staging buffer to copy the contents of +`indices` to and then copy it to the final device local index buffer. + +The index buffer should be cleaned up at the end of the program, just like the +vertex buffer: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyBuffer(device, indexBuffer, nullptr); + vkFreeMemory(device, indexBufferMemory, nullptr); + + vkDestroyBuffer(device, vertexBuffer, nullptr); + vkFreeMemory(device, vertexBufferMemory, nullptr); + + ... +} +``` + +## Using an index buffer + +Using an index buffer for drawing involves two changes to +`recordCommandBuffer`. We first need to bind the index buffer, just like we did +for the vertex buffer. The difference is that you can only have a single index +buffer. It's unfortunately not possible to use different indices for each vertex +attribute, so we do still have to completely duplicate vertex data even if just +one attribute varies. + +```c++ +vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets); + +vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT16); +``` + +An index buffer is bound with `vkCmdBindIndexBuffer` which has the index buffer, +a byte offset into it, and the type of index data as parameters. As mentioned +before, the possible types are `VK_INDEX_TYPE_UINT16` and +`VK_INDEX_TYPE_UINT32`. + +Just binding an index buffer doesn't change anything yet, we also need to change +the drawing command to tell Vulkan to use the index buffer. Remove the +`vkCmdDraw` line and replace it with `vkCmdDrawIndexed`: + +```c++ +vkCmdDrawIndexed(commandBuffer, static_cast(indices.size()), 1, 0, 0, 0); +``` + +A call to this function is very similar to `vkCmdDraw`. The first two parameters +specify the number of indices and the number of instances. We're not using +instancing, so just specify `1` instance. The number of indices represents the +number of vertices that will be passed to the vertex shader. The next parameter +specifies an offset into the index buffer, using a value of `1` would cause the +graphics card to start reading at the second index. The second to last parameter +specifies an offset to add to the indices in the index buffer. The final +parameter specifies an offset for instancing, which we're not using. + +Now run your program and you should see the following: + +![](/images/indexed_rectangle.png) + +You now know how to save memory by reusing vertices with index buffers. This +will become especially important in a future chapter where we're going to load +complex 3D models. + +The previous chapter already mentioned that you should allocate multiple +resources like buffers from a single memory allocation, but in fact you should +go a step further. [Driver developers recommend](https://developer.nvidia.com/vulkan-memory-management) +that you also store multiple buffers, like the vertex and index buffer, into a +single `VkBuffer` and use offsets in commands like `vkCmdBindVertexBuffers`. The +advantage is that your data is more cache friendly in that case, because it's +closer together. It is even possible to reuse the same chunk of memory for +multiple resources if they are not used during the same render operations, +provided that their data is refreshed, of course. This is known as *aliasing* +and some Vulkan functions have explicit flags to specify that you want to do +this. + +[C++ code](/code/21_index_buffer.cpp) / +[Vertex shader](/code/18_shader_vertexbuffer.vert) / +[Fragment shader](/code/18_shader_vertexbuffer.frag) diff --git a/ko/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md b/ko/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md new file mode 100644 index 00000000..2bdcc2dc --- /dev/null +++ b/ko/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md @@ -0,0 +1,416 @@ +## Introduction + +We're now able to pass arbitrary attributes to the vertex shader for each +vertex, but what about global variables? We're going to move on to 3D graphics +from this chapter on and that requires a model-view-projection matrix. We could +include it as vertex data, but that's a waste of memory and it would require us +to update the vertex buffer whenever the transformation changes. The +transformation could easily change every single frame. + +The right way to tackle this in Vulkan is to use *resource descriptors*. A +descriptor is a way for shaders to freely access resources like buffers and +images. We're going to set up a buffer that contains the transformation matrices +and have the vertex shader access them through a descriptor. Usage of +descriptors consists of three parts: + +* Specify a descriptor set layout during pipeline creation +* Allocate a descriptor set from a descriptor pool +* Bind the descriptor set during rendering + +The *descriptor set layout* specifies the types of resources that are going to be +accessed by the pipeline, just like a render pass specifies the types of +attachments that will be accessed. A *descriptor set* specifies the actual +buffer or image resources that will be bound to the descriptors, just like a +framebuffer specifies the actual image views to bind to render pass attachments. +The descriptor set is then bound for the drawing commands just like the vertex +buffers and framebuffer. + +There are many types of descriptors, but in this chapter we'll work with uniform +buffer objects (UBO). We'll look at other types of descriptors in future +chapters, but the basic process is the same. Let's say we have the data we want +the vertex shader to have in a C struct like this: + +```c++ +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; +``` + +Then we can copy the data to a `VkBuffer` and access it through a uniform buffer +object descriptor from the vertex shader like this: + +```glsl +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; + +void main() { + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0); + fragColor = inColor; +} +``` + +We're going to update the model, view and projection matrices every frame to +make the rectangle from the previous chapter spin around in 3D. + +## Vertex shader + +Modify the vertex shader to include the uniform buffer object like it was +specified above. I will assume that you are familiar with MVP transformations. +If you're not, see [the resource](https://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/) +mentioned in the first chapter. + +```glsl +#version 450 + +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; + +layout(location = 0) in vec2 inPosition; +layout(location = 1) in vec3 inColor; + +layout(location = 0) out vec3 fragColor; + +void main() { + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0); + fragColor = inColor; +} +``` + +Note that the order of the `uniform`, `in` and `out` declarations doesn't +matter. The `binding` directive is similar to the `location` directive for +attributes. We're going to reference this binding in the descriptor set layout. The +line with `gl_Position` is changed to use the transformations to compute the +final position in clip coordinates. Unlike the 2D triangles, the last component +of the clip coordinates may not be `1`, which will result in a division when +converted to the final normalized device coordinates on the screen. This is used +in perspective projection as the *perspective division* and is essential for +making closer objects look larger than objects that are further away. + +## Descriptor set layout + +The next step is to define the UBO on the C++ side and to tell Vulkan about this +descriptor in the vertex shader. + +```c++ +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; +``` + +We can exactly match the definition in the shader using data types in GLM. The +data in the matrices is binary compatible with the way the shader expects it, so +we can later just `memcpy` a `UniformBufferObject` to a `VkBuffer`. + +We need to provide details about every descriptor binding used in the shaders +for pipeline creation, just like we had to do for every vertex attribute and its +`location` index. We'll set up a new function to define all of this information +called `createDescriptorSetLayout`. It should be called right before pipeline +creation, because we're going to need it there. + +```c++ +void initVulkan() { + ... + createDescriptorSetLayout(); + createGraphicsPipeline(); + ... +} + +... + +void createDescriptorSetLayout() { + +} +``` + +Every binding needs to be described through a `VkDescriptorSetLayoutBinding` +struct. + +```c++ +void createDescriptorSetLayout() { + VkDescriptorSetLayoutBinding uboLayoutBinding{}; + uboLayoutBinding.binding = 0; + uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + uboLayoutBinding.descriptorCount = 1; +} +``` + +The first two fields specify the `binding` used in the shader and the type of +descriptor, which is a uniform buffer object. It is possible for the shader +variable to represent an array of uniform buffer objects, and `descriptorCount` +specifies the number of values in the array. This could be used to specify a +transformation for each of the bones in a skeleton for skeletal animation, for +example. Our MVP transformation is in a single uniform buffer object, so we're +using a `descriptorCount` of `1`. + +```c++ +uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; +``` + +We also need to specify in which shader stages the descriptor is going to be +referenced. The `stageFlags` field can be a combination of `VkShaderStageFlagBits` values +or the value `VK_SHADER_STAGE_ALL_GRAPHICS`. In our case, we're only referencing +the descriptor from the vertex shader. + +```c++ +uboLayoutBinding.pImmutableSamplers = nullptr; // Optional +``` + +The `pImmutableSamplers` field is only relevant for image sampling related +descriptors, which we'll look at later. You can leave this to its default value. + +All of the descriptor bindings are combined into a single +`VkDescriptorSetLayout` object. Define a new class member above +`pipelineLayout`: + +```c++ +VkDescriptorSetLayout descriptorSetLayout; +VkPipelineLayout pipelineLayout; +``` + +We can then create it using `vkCreateDescriptorSetLayout`. This function accepts +a simple `VkDescriptorSetLayoutCreateInfo` with the array of bindings: + +```c++ +VkDescriptorSetLayoutCreateInfo layoutInfo{}; +layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; +layoutInfo.bindingCount = 1; +layoutInfo.pBindings = &uboLayoutBinding; + +if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) { + throw std::runtime_error("failed to create descriptor set layout!"); +} +``` + +We need to specify the descriptor set layout during pipeline creation to tell +Vulkan which descriptors the shaders will be using. Descriptor set layouts are +specified in the pipeline layout object. Modify the `VkPipelineLayoutCreateInfo` +to reference the layout object: + +```c++ +VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; +pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; +pipelineLayoutInfo.setLayoutCount = 1; +pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout; +``` + +You may be wondering why it's possible to specify multiple descriptor set +layouts here, because a single one already includes all of the bindings. We'll +get back to that in the next chapter, where we'll look into descriptor pools and +descriptor sets. + +The descriptor set layout should stick around while we may create new graphics +pipelines i.e. until the program ends: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); + + ... +} +``` + +## Uniform buffer + +In the next chapter we'll specify the buffer that contains the UBO data for the +shader, but we need to create this buffer first. We're going to copy new data to +the uniform buffer every frame, so it doesn't really make any sense to have a +staging buffer. It would just add extra overhead in this case and likely degrade +performance instead of improving it. + +We should have multiple buffers, because multiple frames may be in flight at the same +time and we don't want to update the buffer in preparation of the next frame while a +previous one is still reading from it! Thus, we need to have as many uniform buffers +as we have frames in flight, and write to a uniform buffer that is not currently +being read by the GPU. + +To that end, add new class members for `uniformBuffers`, and `uniformBuffersMemory`: + +```c++ +VkBuffer indexBuffer; +VkDeviceMemory indexBufferMemory; + +std::vector uniformBuffers; +std::vector uniformBuffersMemory; +std::vector uniformBuffersMapped; +``` + +Similarly, create a new function `createUniformBuffers` that is called after +`createIndexBuffer` and allocates the buffers: + +```c++ +void initVulkan() { + ... + createVertexBuffer(); + createIndexBuffer(); + createUniformBuffers(); + ... +} + +... + +void createUniformBuffers() { + VkDeviceSize bufferSize = sizeof(UniformBufferObject); + + uniformBuffers.resize(MAX_FRAMES_IN_FLIGHT); + uniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT); + uniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, uniformBuffers[i], uniformBuffersMemory[i]); + + vkMapMemory(device, uniformBuffersMemory[i], 0, bufferSize, 0, &uniformBuffersMapped[i]); + } +} +``` + +We map the buffer right after creation using `vkMapMemory` to get a pointer to which we can write the data later on. The buffer stays mapped to this pointer for the application's whole lifetime. This technique is called **"persistent mapping"** and works on all Vulkan implementations. Not having to map the buffer every time we need to update it increases performances, as mapping is not free. + +The uniform data will be used for all draw calls, so the buffer containing it should only be destroyed when we stop rendering. + +```c++ +void cleanup() { + ... + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vkDestroyBuffer(device, uniformBuffers[i], nullptr); + vkFreeMemory(device, uniformBuffersMemory[i], nullptr); + } + + vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); + + ... + +} +``` + +## Updating uniform data + +Create a new function `updateUniformBuffer` and add a call to it from the `drawFrame` function before submitting the next frame: + +```c++ +void drawFrame() { + ... + + updateUniformBuffer(currentFrame); + + ... + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + + ... +} + +... + +void updateUniformBuffer(uint32_t currentImage) { + +} +``` + +This function will generate a new transformation every frame to make the +geometry spin around. We need to include two new headers to implement this +functionality: + +```c++ +#define GLM_FORCE_RADIANS +#include +#include + +#include +``` + +The `glm/gtc/matrix_transform.hpp` header exposes functions that can be used to +generate model transformations like `glm::rotate`, view transformations like +`glm::lookAt` and projection transformations like `glm::perspective`. The +`GLM_FORCE_RADIANS` definition is necessary to make sure that functions like +`glm::rotate` use radians as arguments, to avoid any possible confusion. + +The `chrono` standard library header exposes functions to do precise +timekeeping. We'll use this to make sure that the geometry rotates 90 degrees +per second regardless of frame rate. + +```c++ +void updateUniformBuffer(uint32_t currentImage) { + static auto startTime = std::chrono::high_resolution_clock::now(); + + auto currentTime = std::chrono::high_resolution_clock::now(); + float time = std::chrono::duration(currentTime - startTime).count(); +} +``` + +The `updateUniformBuffer` function will start out with some logic to calculate +the time in seconds since rendering has started with floating point accuracy. + +We will now define the model, view and projection transformations in the +uniform buffer object. The model rotation will be a simple rotation around the +Z-axis using the `time` variable: + +```c++ +UniformBufferObject ubo{}; +ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f)); +``` + +The `glm::rotate` function takes an existing transformation, rotation angle and +rotation axis as parameters. The `glm::mat4(1.0f)` constructor returns an +identity matrix. Using a rotation angle of `time * glm::radians(90.0f)` +accomplishes the purpose of rotation 90 degrees per second. + +```c++ +ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)); +``` + +For the view transformation I've decided to look at the geometry from above at a +45 degree angle. The `glm::lookAt` function takes the eye position, center +position and up axis as parameters. + +```c++ +ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f); +``` + +I've chosen to use a perspective projection with a 45 degree vertical +field-of-view. The other parameters are the aspect ratio, near and far +view planes. It is important to use the current swap chain extent to calculate +the aspect ratio to take into account the new width and height of the window +after a resize. + +```c++ +ubo.proj[1][1] *= -1; +``` + +GLM was originally designed for OpenGL, where the Y coordinate of the clip +coordinates is inverted. The easiest way to compensate for that is to flip the +sign on the scaling factor of the Y axis in the projection matrix. If you don't +do this, then the image will be rendered upside down. + +All of the transformations are defined now, so we can copy the data in the +uniform buffer object to the current uniform buffer. This happens in exactly the same +way as we did for vertex buffers, except without a staging buffer. As noted earlier, we only map the uniform buffer once, so we can directly write to it without having to map again: + +```c++ +memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); +``` + +Using a UBO this way is not the most efficient way to pass frequently changing +values to the shader. A more efficient way to pass a small buffer of data to +shaders are *push constants*. We may look at these in a future chapter. + +In the next chapter we'll look at descriptor sets, which will actually bind the +`VkBuffer`s to the uniform buffer descriptors so that the shader can access this +transformation data. + +[C++ code](/code/22_descriptor_set_layout.cpp) / +[Vertex shader](/code/22_shader_ubo.vert) / +[Fragment shader](/code/22_shader_ubo.frag) diff --git a/ko/05_Uniform_buffers/01_Descriptor_pool_and_sets.md b/ko/05_Uniform_buffers/01_Descriptor_pool_and_sets.md new file mode 100644 index 00000000..b204db24 --- /dev/null +++ b/ko/05_Uniform_buffers/01_Descriptor_pool_and_sets.md @@ -0,0 +1,391 @@ +## Introduction + +The descriptor set layout from the previous chapter describes the type of +descriptors that can be bound. In this chapter we're going to create +a descriptor set for each `VkBuffer` resource to bind it to the +uniform buffer descriptor. + +## Descriptor pool + +Descriptor sets can't be created directly, they must be allocated from a pool +like command buffers. The equivalent for descriptor sets is unsurprisingly +called a *descriptor pool*. We'll write a new function `createDescriptorPool` +to set it up. + +```c++ +void initVulkan() { + ... + createUniformBuffers(); + createDescriptorPool(); + ... +} + +... + +void createDescriptorPool() { + +} +``` + +We first need to describe which descriptor types our descriptor sets are going +to contain and how many of them, using `VkDescriptorPoolSize` structures. + +```c++ +VkDescriptorPoolSize poolSize{}; +poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +poolSize.descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); +``` + +We will allocate one of these descriptors for every frame. This +pool size structure is referenced by the main `VkDescriptorPoolCreateInfo`: + +```c++ +VkDescriptorPoolCreateInfo poolInfo{}; +poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; +poolInfo.poolSizeCount = 1; +poolInfo.pPoolSizes = &poolSize; +``` + +Aside from the maximum number of individual descriptors that are available, we +also need to specify the maximum number of descriptor sets that may be +allocated: + +```c++ +poolInfo.maxSets = static_cast(MAX_FRAMES_IN_FLIGHT); +``` + +The structure has an optional flag similar to command pools that determines if +individual descriptor sets can be freed or not: +`VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT`. We're not going to touch +the descriptor set after creating it, so we don't need this flag. You can leave +`flags` to its default value of `0`. + +```c++ +VkDescriptorPool descriptorPool; + +... + +if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) { + throw std::runtime_error("failed to create descriptor pool!"); +} +``` + +Add a new class member to store the handle of the descriptor pool and call +`vkCreateDescriptorPool` to create it. + +## Descriptor set + +We can now allocate the descriptor sets themselves. Add a `createDescriptorSets` +function for that purpose: + +```c++ +void initVulkan() { + ... + createDescriptorPool(); + createDescriptorSets(); + ... +} + +... + +void createDescriptorSets() { + +} +``` + +A descriptor set allocation is described with a `VkDescriptorSetAllocateInfo` +struct. You need to specify the descriptor pool to allocate from, the number of +descriptor sets to allocate, and the descriptor set layout to base them on: + +```c++ +std::vector layouts(MAX_FRAMES_IN_FLIGHT, descriptorSetLayout); +VkDescriptorSetAllocateInfo allocInfo{}; +allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; +allocInfo.descriptorPool = descriptorPool; +allocInfo.descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT); +allocInfo.pSetLayouts = layouts.data(); +``` + +In our case we will create one descriptor set for each frame in flight, all with the same layout. +Unfortunately we do need all the copies of the layout because the next function expects an array matching the number of sets. + +Add a class member to hold the descriptor set handles and allocate them with +`vkAllocateDescriptorSets`: + +```c++ +VkDescriptorPool descriptorPool; +std::vector descriptorSets; + +... + +descriptorSets.resize(MAX_FRAMES_IN_FLIGHT); +if (vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate descriptor sets!"); +} +``` + +You don't need to explicitly clean up descriptor sets, because they will be +automatically freed when the descriptor pool is destroyed. The call to +`vkAllocateDescriptorSets` will allocate descriptor sets, each with one uniform +buffer descriptor. + +```c++ +void cleanup() { + ... + vkDestroyDescriptorPool(device, descriptorPool, nullptr); + + vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); + ... +} +``` + +The descriptor sets have been allocated now, but the descriptors within still need +to be configured. We'll now add a loop to populate every descriptor: + +```c++ +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + +} +``` + +Descriptors that refer to buffers, like our uniform buffer +descriptor, are configured with a `VkDescriptorBufferInfo` struct. This +structure specifies the buffer and the region within it that contains the data +for the descriptor. + +```c++ +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + VkDescriptorBufferInfo bufferInfo{}; + bufferInfo.buffer = uniformBuffers[i]; + bufferInfo.offset = 0; + bufferInfo.range = sizeof(UniformBufferObject); +} +``` + +If you're overwriting the whole buffer, like we are in this case, then it is also possible to use the `VK_WHOLE_SIZE` value for the range. The configuration of descriptors is updated using the `vkUpdateDescriptorSets` +function, which takes an array of `VkWriteDescriptorSet` structs as parameter. + +```c++ +VkWriteDescriptorSet descriptorWrite{}; +descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; +descriptorWrite.dstSet = descriptorSets[i]; +descriptorWrite.dstBinding = 0; +descriptorWrite.dstArrayElement = 0; +``` + +The first two fields specify the descriptor set to update and the binding. We +gave our uniform buffer binding index `0`. Remember that descriptors can be +arrays, so we also need to specify the first index in the array that we want to +update. We're not using an array, so the index is simply `0`. + +```c++ +descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +descriptorWrite.descriptorCount = 1; +``` + +We need to specify the type of descriptor again. It's possible to update +multiple descriptors at once in an array, starting at index `dstArrayElement`. +The `descriptorCount` field specifies how many array elements you want to +update. + +```c++ +descriptorWrite.pBufferInfo = &bufferInfo; +descriptorWrite.pImageInfo = nullptr; // Optional +descriptorWrite.pTexelBufferView = nullptr; // Optional +``` + +The last field references an array with `descriptorCount` structs that actually +configure the descriptors. It depends on the type of descriptor which one of the +three you actually need to use. The `pBufferInfo` field is used for descriptors +that refer to buffer data, `pImageInfo` is used for descriptors that refer to +image data, and `pTexelBufferView` is used for descriptors that refer to buffer +views. Our descriptor is based on buffers, so we're using `pBufferInfo`. + +```c++ +vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr); +``` + +The updates are applied using `vkUpdateDescriptorSets`. It accepts two kinds of +arrays as parameters: an array of `VkWriteDescriptorSet` and an array of +`VkCopyDescriptorSet`. The latter can be used to copy descriptors to each other, +as its name implies. + +## Using descriptor sets + +We now need to update the `recordCommandBuffer` function to actually bind the +right descriptor set for each frame to the descriptors in the shader with `vkCmdBindDescriptorSets`. This needs to be done before the `vkCmdDrawIndexed` call: + +```c++ +vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSets[currentFrame], 0, nullptr); +vkCmdDrawIndexed(commandBuffer, static_cast(indices.size()), 1, 0, 0, 0); +``` + +Unlike vertex and index buffers, descriptor sets are not unique to graphics +pipelines. Therefore we need to specify if we want to bind descriptor sets to +the graphics or compute pipeline. The next parameter is the layout that the +descriptors are based on. The next three parameters specify the index of the +first descriptor set, the number of sets to bind, and the array of sets to bind. +We'll get back to this in a moment. The last two parameters specify an array of +offsets that are used for dynamic descriptors. We'll look at these in a future +chapter. + +If you run your program now, then you'll notice that unfortunately nothing is +visible. The problem is that because of the Y-flip we did in the projection +matrix, the vertices are now being drawn in counter-clockwise order instead of +clockwise order. This causes backface culling to kick in and prevents +any geometry from being drawn. Go to the `createGraphicsPipeline` function and +modify the `frontFace` in `VkPipelineRasterizationStateCreateInfo` to correct +this: + +```c++ +rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; +rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; +``` + +Run your program again and you should now see the following: + +![](/images/spinning_quad.png) + +The rectangle has changed into a square because the projection matrix now +corrects for aspect ratio. The `updateUniformBuffer` takes care of screen +resizing, so we don't need to recreate the descriptor set in +`recreateSwapChain`. + +## Alignment requirements + +One thing we've glossed over so far is how exactly the data in the C++ structure should match with the uniform definition in the shader. It seems obvious enough to simply use the same types in both: + +```c++ +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; + +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; +``` + +However, that's not all there is to it. For example, try modifying the struct and shader to look like this: + +```c++ +struct UniformBufferObject { + glm::vec2 foo; + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; + +layout(binding = 0) uniform UniformBufferObject { + vec2 foo; + mat4 model; + mat4 view; + mat4 proj; +} ubo; +``` + +Recompile your shader and your program and run it and you'll find that the colorful square you worked so far has disappeared! That's because we haven't taken into account the *alignment requirements*. + +Vulkan expects the data in your structure to be aligned in memory in a specific way, for example: + +* Scalars have to be aligned by N (= 4 bytes given 32 bit floats). +* A `vec2` must be aligned by 2N (= 8 bytes) +* A `vec3` or `vec4` must be aligned by 4N (= 16 bytes) +* A nested structure must be aligned by the base alignment of its members rounded up to a multiple of 16. +* A `mat4` matrix must have the same alignment as a `vec4`. + +You can find the full list of alignment requirements in [the specification](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap15.html#interfaces-resources-layout). + +Our original shader with just three `mat4` fields already met the alignment requirements. As each `mat4` is 4 x 4 x 4 = 64 bytes in size, `model` has an offset of `0`, `view` has an offset of 64 and `proj` has an offset of 128. All of these are multiples of 16 and that's why it worked fine. + +The new structure starts with a `vec2` which is only 8 bytes in size and therefore throws off all of the offsets. Now `model` has an offset of `8`, `view` an offset of `72` and `proj` an offset of `136`, none of which are multiples of 16. To fix this problem we can use the [`alignas`](https://en.cppreference.com/w/cpp/language/alignas) specifier introduced in C++11: + +```c++ +struct UniformBufferObject { + glm::vec2 foo; + alignas(16) glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; +``` + +If you now compile and run your program again you should see that the shader correctly receives its matrix values once again. + +Luckily there is a way to not have to think about these alignment requirements *most* of the time. We can define `GLM_FORCE_DEFAULT_ALIGNED_GENTYPES` right before including GLM: + +```c++ +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES +#include +``` + +This will force GLM to use a version of `vec2` and `mat4` that has the alignment requirements already specified for us. If you add this definition then you can remove the `alignas` specifier and your program should still work. + +Unfortunately this method can break down if you start using nested structures. Consider the following definition in the C++ code: + +```c++ +struct Foo { + glm::vec2 v; +}; + +struct UniformBufferObject { + Foo f1; + Foo f2; +}; +``` + +And the following shader definition: + +```c++ +struct Foo { + vec2 v; +}; + +layout(binding = 0) uniform UniformBufferObject { + Foo f1; + Foo f2; +} ubo; +``` + +In this case `f2` will have an offset of `8` whereas it should have an offset of `16` since it is a nested structure. In this case you must specify the alignment yourself: + +```c++ +struct UniformBufferObject { + Foo f1; + alignas(16) Foo f2; +}; +``` + +These gotchas are a good reason to always be explicit about alignment. That way you won't be caught offguard by the strange symptoms of alignment errors. + +```c++ +struct UniformBufferObject { + alignas(16) glm::mat4 model; + alignas(16) glm::mat4 view; + alignas(16) glm::mat4 proj; +}; +``` + +Don't forget to recompile your shader after removing the `foo` field. + +## Multiple descriptor sets + +As some of the structures and function calls hinted at, it is actually possible +to bind multiple descriptor sets simultaneously. You need to specify a descriptor set layout for +each descriptor set when creating the pipeline layout. Shaders can then +reference specific descriptor sets like this: + +```c++ +layout(set = 0, binding = 0) uniform UniformBufferObject { ... } +``` + +You can use this feature to put descriptors that vary per-object and descriptors +that are shared into separate descriptor sets. In that case you avoid rebinding +most of the descriptors across draw calls which is potentially more efficient. + +[C++ code](/code/23_descriptor_sets.cpp) / +[Vertex shader](/code/22_shader_ubo.vert) / +[Fragment shader](/code/22_shader_ubo.frag) diff --git a/ko/06_Texture_mapping/00_Images.md b/ko/06_Texture_mapping/00_Images.md new file mode 100644 index 00000000..8c9967f6 --- /dev/null +++ b/ko/06_Texture_mapping/00_Images.md @@ -0,0 +1,769 @@ +## Introduction + +The geometry has been colored using per-vertex colors so far, which is a rather +limited approach. In this part of the tutorial we're going to implement texture +mapping to make the geometry look more interesting. This will also allow us to +load and draw basic 3D models in a future chapter. + +Adding a texture to our application will involve the following steps: + +* Create an image object backed by device memory +* Fill it with pixels from an image file +* Create an image sampler +* Add a combined image sampler descriptor to sample colors from the texture + +We've already worked with image objects before, but those were automatically +created by the swap chain extension. This time we'll have to create one by +ourselves. Creating an image and filling it with data is similar to vertex +buffer creation. We'll start by creating a staging resource and filling it with +pixel data and then we copy this to the final image object that we'll use for +rendering. Although it is possible to create a staging image for this purpose, +Vulkan also allows you to copy pixels from a `VkBuffer` to an image and the API +for this is actually [faster on some hardware](https://developer.nvidia.com/vulkan-memory-management). +We'll first create this buffer and fill it with pixel values, and then we'll +create an image to copy the pixels to. Creating an image is not very different +from creating buffers. It involves querying the memory requirements, allocating +device memory and binding it, just like we've seen before. + +However, there is something extra that we'll have to take care of when working +with images. Images can have different *layouts* that affect how the pixels are +organized in memory. Due to the way graphics hardware works, simply storing the +pixels row by row may not lead to the best performance, for example. When +performing any operation on images, you must make sure that they have the layout +that is optimal for use in that operation. We've actually already seen some of +these layouts when we specified the render pass: + +* `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`: Optimal for presentation +* `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`: Optimal as attachment for writing +colors from the fragment shader +* `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`: Optimal as source in a transfer +operation, like `vkCmdCopyImageToBuffer` +* `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`: Optimal as destination in a transfer +operation, like `vkCmdCopyBufferToImage` +* `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`: Optimal for sampling from a shader + +One of the most common ways to transition the layout of an image is a *pipeline +barrier*. Pipeline barriers are primarily used for synchronizing access to +resources, like making sure that an image was written to before it is read, but +they can also be used to transition layouts. In this chapter we'll see how +pipeline barriers are used for this purpose. Barriers can additionally be used +to transfer queue family ownership when using `VK_SHARING_MODE_EXCLUSIVE`. + +## Image library + +There are many libraries available for loading images, and you can even write +your own code to load simple formats like BMP and PPM. In this tutorial we'll be +using the stb_image library from the [stb collection](https://github.com/nothings/stb). +The advantage of it is that all of the code is in a single file, so it doesn't +require any tricky build configuration. Download `stb_image.h` and store it in a +convenient location, like the directory where you saved GLFW and GLM. Add the +location to your include path. + +**Visual Studio** + +Add the directory with `stb_image.h` in it to the `Additional Include +Directories` paths. + +![](/images/include_dirs_stb.png) + +**Makefile** + +Add the directory with `stb_image.h` to the include directories for GCC: + +```text +VULKAN_SDK_PATH = /home/user/VulkanSDK/x.x.x.x/x86_64 +STB_INCLUDE_PATH = /home/user/libraries/stb + +... + +CFLAGS = -std=c++17 -I$(VULKAN_SDK_PATH)/include -I$(STB_INCLUDE_PATH) +``` + +## Loading an image + +Include the image library like this: + +```c++ +#define STB_IMAGE_IMPLEMENTATION +#include +``` + +The header only defines the prototypes of the functions by default. One code +file needs to include the header with the `STB_IMAGE_IMPLEMENTATION` definition +to include the function bodies, otherwise we'll get linking errors. + +```c++ +void initVulkan() { + ... + createCommandPool(); + createTextureImage(); + createVertexBuffer(); + ... +} + +... + +void createTextureImage() { + +} +``` + +Create a new function `createTextureImage` where we'll load an image and upload +it into a Vulkan image object. We're going to use command buffers, so it should +be called after `createCommandPool`. + +Create a new directory `textures` next to the `shaders` directory to store +texture images in. We're going to load an image called `texture.jpg` from that +directory. I've chosen to use the following +[CC0 licensed image](https://pixabay.com/en/statue-sculpture-fig-historically-1275469/) +resized to 512 x 512 pixels, but feel free to pick any image you want. The +library supports most common image file formats, like JPEG, PNG, BMP and GIF. + +![](/images/texture.jpg) + +Loading an image with this library is really easy: + +```c++ +void createTextureImage() { + int texWidth, texHeight, texChannels; + stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); + VkDeviceSize imageSize = texWidth * texHeight * 4; + + if (!pixels) { + throw std::runtime_error("failed to load texture image!"); + } +} +``` + +The `stbi_load` function takes the file path and number of channels to load as +arguments. The `STBI_rgb_alpha` value forces the image to be loaded with an +alpha channel, even if it doesn't have one, which is nice for consistency with +other textures in the future. The middle three parameters are outputs for the +width, height and actual number of channels in the image. The pointer that is +returned is the first element in an array of pixel values. The pixels are laid +out row by row with 4 bytes per pixel in the case of `STBI_rgb_alpha` for a +total of `texWidth * texHeight * 4` values. + +## Staging buffer + +We're now going to create a buffer in host visible memory so that we can use +`vkMapMemory` and copy the pixels to it. Add variables for this temporary buffer +to the `createTextureImage` function: + +```c++ +VkBuffer stagingBuffer; +VkDeviceMemory stagingBufferMemory; +``` + +The buffer should be in host visible memory so that we can map it and it should +be usable as a transfer source so that we can copy it to an image later on: + +```c++ +createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); +``` + +We can then directly copy the pixel values that we got from the image loading +library to the buffer: + +```c++ +void* data; +vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data); + memcpy(data, pixels, static_cast(imageSize)); +vkUnmapMemory(device, stagingBufferMemory); +``` + +Don't forget to clean up the original pixel array now: + +```c++ +stbi_image_free(pixels); +``` + +## Texture Image + +Although we could set up the shader to access the pixel values in the buffer, +it's better to use image objects in Vulkan for this purpose. Image objects will +make it easier and faster to retrieve colors by allowing us to use 2D +coordinates, for one. Pixels within an image object are known as texels and +we'll use that name from this point on. Add the following new class members: + +```c++ +VkImage textureImage; +VkDeviceMemory textureImageMemory; +``` + +The parameters for an image are specified in a `VkImageCreateInfo` struct: + +```c++ +VkImageCreateInfo imageInfo{}; +imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; +imageInfo.imageType = VK_IMAGE_TYPE_2D; +imageInfo.extent.width = static_cast(texWidth); +imageInfo.extent.height = static_cast(texHeight); +imageInfo.extent.depth = 1; +imageInfo.mipLevels = 1; +imageInfo.arrayLayers = 1; +``` + +The image type, specified in the `imageType` field, tells Vulkan with what kind +of coordinate system the texels in the image are going to be addressed. It is +possible to create 1D, 2D and 3D images. One dimensional images can be used to +store an array of data or gradient, two dimensional images are mainly used for +textures, and three dimensional images can be used to store voxel volumes, for +example. The `extent` field specifies the dimensions of the image, basically how +many texels there are on each axis. That's why `depth` must be `1` instead of +`0`. Our texture will not be an array and we won't be using mipmapping for now. + +```c++ +imageInfo.format = VK_FORMAT_R8G8B8A8_SRGB; +``` + +Vulkan supports many possible image formats, but we should use the same format +for the texels as the pixels in the buffer, otherwise the copy operation will +fail. + +```c++ +imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; +``` + +The `tiling` field can have one of two values: + +* `VK_IMAGE_TILING_LINEAR`: Texels are laid out in row-major order like our +`pixels` array +* `VK_IMAGE_TILING_OPTIMAL`: Texels are laid out in an implementation defined +order for optimal access + +Unlike the layout of an image, the tiling mode cannot be changed at a later +time. If you want to be able to directly access texels in the memory of the +image, then you must use `VK_IMAGE_TILING_LINEAR`. We will be using a staging +buffer instead of a staging image, so this won't be necessary. We will be using +`VK_IMAGE_TILING_OPTIMAL` for efficient access from the shader. + +```c++ +imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; +``` + +There are only two possible values for the `initialLayout` of an image: + +* `VK_IMAGE_LAYOUT_UNDEFINED`: Not usable by the GPU and the very first +transition will discard the texels. +* `VK_IMAGE_LAYOUT_PREINITIALIZED`: Not usable by the GPU, but the first +transition will preserve the texels. + +There are few situations where it is necessary for the texels to be preserved +during the first transition. One example, however, would be if you wanted to use +an image as a staging image in combination with the `VK_IMAGE_TILING_LINEAR` +layout. In that case, you'd want to upload the texel data to it and then +transition the image to be a transfer source without losing the data. In our +case, however, we're first going to transition the image to be a transfer +destination and then copy texel data to it from a buffer object, so we don't +need this property and can safely use `VK_IMAGE_LAYOUT_UNDEFINED`. + +```c++ +imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; +``` + +The `usage` field has the same semantics as the one during buffer creation. The +image is going to be used as destination for the buffer copy, so it should be +set up as a transfer destination. We also want to be able to access the image +from the shader to color our mesh, so the usage should include +`VK_IMAGE_USAGE_SAMPLED_BIT`. + +```c++ +imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; +``` + +The image will only be used by one queue family: the one that supports graphics +(and therefore also) transfer operations. + +```c++ +imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; +imageInfo.flags = 0; // Optional +``` + +The `samples` flag is related to multisampling. This is only relevant for images +that will be used as attachments, so stick to one sample. There are some +optional flags for images that are related to sparse images. Sparse images are +images where only certain regions are actually backed by memory. If you were +using a 3D texture for a voxel terrain, for example, then you could use this to +avoid allocating memory to store large volumes of "air" values. We won't be +using it in this tutorial, so leave it to its default value of `0`. + +```c++ +if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) { + throw std::runtime_error("failed to create image!"); +} +``` + +The image is created using `vkCreateImage`, which doesn't have any particularly +noteworthy parameters. It is possible that the `VK_FORMAT_R8G8B8A8_SRGB` format +is not supported by the graphics hardware. You should have a list of acceptable +alternatives and go with the best one that is supported. However, support for +this particular format is so widespread that we'll skip this step. Using +different formats would also require annoying conversions. We will get back to +this in the depth buffer chapter, where we'll implement such a system. + +```c++ +VkMemoryRequirements memRequirements; +vkGetImageMemoryRequirements(device, textureImage, &memRequirements); + +VkMemoryAllocateInfo allocInfo{}; +allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; +allocInfo.allocationSize = memRequirements.size; +allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + +if (vkAllocateMemory(device, &allocInfo, nullptr, &textureImageMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate image memory!"); +} + +vkBindImageMemory(device, textureImage, textureImageMemory, 0); +``` + +Allocating memory for an image works in exactly the same way as allocating +memory for a buffer. Use `vkGetImageMemoryRequirements` instead of +`vkGetBufferMemoryRequirements`, and use `vkBindImageMemory` instead of +`vkBindBufferMemory`. + +This function is already getting quite large and there'll be a need to create +more images in later chapters, so we should abstract image creation into a +`createImage` function, like we did for buffers. Create the function and move +the image object creation and memory allocation to it: + +```c++ +void createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) { + VkImageCreateInfo imageInfo{}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.extent.width = width; + imageInfo.extent.height = height; + imageInfo.extent.depth = 1; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.format = format; + imageInfo.tiling = tiling; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageInfo.usage = usage; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateImage(device, &imageInfo, nullptr, &image) != VK_SUCCESS) { + throw std::runtime_error("failed to create image!"); + } + + VkMemoryRequirements memRequirements; + vkGetImageMemoryRequirements(device, image, &memRequirements); + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); + + if (vkAllocateMemory(device, &allocInfo, nullptr, &imageMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate image memory!"); + } + + vkBindImageMemory(device, image, imageMemory, 0); +} +``` + +I've made the width, height, format, tiling mode, usage, and memory properties +parameters, because these will all vary between the images we'll be creating +throughout this tutorial. + +The `createTextureImage` function can now be simplified to: + +```c++ +void createTextureImage() { + int texWidth, texHeight, texChannels; + stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); + VkDeviceSize imageSize = texWidth * texHeight * 4; + + if (!pixels) { + throw std::runtime_error("failed to load texture image!"); + } + + VkBuffer stagingBuffer; + VkDeviceMemory stagingBufferMemory; + createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); + + void* data; + vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data); + memcpy(data, pixels, static_cast(imageSize)); + vkUnmapMemory(device, stagingBufferMemory); + + stbi_image_free(pixels); + + createImage(texWidth, texHeight, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory); +} +``` + +## Layout transitions + +The function we're going to write now involves recording and executing a command +buffer again, so now's a good time to move that logic into a helper function or +two: + +```c++ +VkCommandBuffer beginSingleTimeCommands() { + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandPool = commandPool; + allocInfo.commandBufferCount = 1; + + VkCommandBuffer commandBuffer; + vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); + + VkCommandBufferBeginInfo beginInfo{}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + + vkBeginCommandBuffer(commandBuffer, &beginInfo); + + return commandBuffer; +} + +void endSingleTimeCommands(VkCommandBuffer commandBuffer) { + vkEndCommandBuffer(commandBuffer); + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &commandBuffer; + + vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); + vkQueueWaitIdle(graphicsQueue); + + vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); +} +``` + +The code for these functions is based on the existing code in `copyBuffer`. You +can now simplify that function to: + +```c++ +void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { + VkCommandBuffer commandBuffer = beginSingleTimeCommands(); + + VkBufferCopy copyRegion{}; + copyRegion.size = size; + vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region); + + endSingleTimeCommands(commandBuffer); +} +``` + +If we were still using buffers, then we could now write a function to record and +execute `vkCmdCopyBufferToImage` to finish the job, but this command requires +the image to be in the right layout first. Create a new function to handle +layout transitions: + +```c++ +void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout) { + VkCommandBuffer commandBuffer = beginSingleTimeCommands(); + + endSingleTimeCommands(commandBuffer); +} +``` + +One of the most common ways to perform layout transitions is using an *image +memory barrier*. A pipeline barrier like that is generally used to synchronize +access to resources, like ensuring that a write to a buffer completes before +reading from it, but it can also be used to transition image layouts and +transfer queue family ownership when `VK_SHARING_MODE_EXCLUSIVE` is used. There +is an equivalent *buffer memory barrier* to do this for buffers. + +```c++ +VkImageMemoryBarrier barrier{}; +barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; +barrier.oldLayout = oldLayout; +barrier.newLayout = newLayout; +``` + +The first two fields specify layout transition. It is possible to use +`VK_IMAGE_LAYOUT_UNDEFINED` as `oldLayout` if you don't care about the existing +contents of the image. + +```c++ +barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; +barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; +``` + +If you are using the barrier to transfer queue family ownership, then these two +fields should be the indices of the queue families. They must be set to +`VK_QUEUE_FAMILY_IGNORED` if you don't want to do this (not the default value!). + +```c++ +barrier.image = image; +barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +barrier.subresourceRange.baseMipLevel = 0; +barrier.subresourceRange.levelCount = 1; +barrier.subresourceRange.baseArrayLayer = 0; +barrier.subresourceRange.layerCount = 1; +``` + +The `image` and `subresourceRange` specify the image that is affected and the +specific part of the image. Our image is not an array and does not have mipmapping +levels, so only one level and layer are specified. + +```c++ +barrier.srcAccessMask = 0; // TODO +barrier.dstAccessMask = 0; // TODO +``` + +Barriers are primarily used for synchronization purposes, so you must specify +which types of operations that involve the resource must happen before the +barrier, and which operations that involve the resource must wait on the +barrier. We need to do that despite already using `vkQueueWaitIdle` to manually +synchronize. The right values depend on the old and new layout, so we'll get +back to this once we've figured out which transitions we're going to use. + +```c++ +vkCmdPipelineBarrier( + commandBuffer, + 0 /* TODO */, 0 /* TODO */, + 0, + 0, nullptr, + 0, nullptr, + 1, &barrier +); +``` + +All types of pipeline barriers are submitted using the same function. The first +parameter after the command buffer specifies in which pipeline stage the +operations occur that should happen before the barrier. The second parameter +specifies the pipeline stage in which operations will wait on the barrier. The +pipeline stages that you are allowed to specify before and after the barrier +depend on how you use the resource before and after the barrier. The allowed +values are listed in [this table](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-access-types-supported) +of the specification. For example, if you're going to read from a uniform after +the barrier, you would specify a usage of `VK_ACCESS_UNIFORM_READ_BIT` and the +earliest shader that will read from the uniform as pipeline stage, for example +`VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT`. It would not make sense to specify +a non-shader pipeline stage for this type of usage and the validation layers +will warn you when you specify a pipeline stage that does not match the type of +usage. + +The third parameter is either `0` or `VK_DEPENDENCY_BY_REGION_BIT`. The latter +turns the barrier into a per-region condition. That means that the +implementation is allowed to already begin reading from the parts of a resource +that were written so far, for example. + +The last three pairs of parameters reference arrays of pipeline barriers of the +three available types: memory barriers, buffer memory barriers, and image memory +barriers like the one we're using here. Note that we're not using the `VkFormat` +parameter yet, but we'll be using that one for special transitions in the depth +buffer chapter. + +## Copying buffer to image + +Before we get back to `createTextureImage`, we're going to write one more helper +function: `copyBufferToImage`: + +```c++ +void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) { + VkCommandBuffer commandBuffer = beginSingleTimeCommands(); + + endSingleTimeCommands(commandBuffer); +} +``` + +Just like with buffer copies, you need to specify which part of the buffer is +going to be copied to which part of the image. This happens through +`VkBufferImageCopy` structs: + +```c++ +VkBufferImageCopy region{}; +region.bufferOffset = 0; +region.bufferRowLength = 0; +region.bufferImageHeight = 0; + +region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +region.imageSubresource.mipLevel = 0; +region.imageSubresource.baseArrayLayer = 0; +region.imageSubresource.layerCount = 1; + +region.imageOffset = {0, 0, 0}; +region.imageExtent = { + width, + height, + 1 +}; +``` + +Most of these fields are self-explanatory. The `bufferOffset` specifies the byte +offset in the buffer at which the pixel values start. The `bufferRowLength` and +`bufferImageHeight` fields specify how the pixels are laid out in memory. For +example, you could have some padding bytes between rows of the image. Specifying +`0` for both indicates that the pixels are simply tightly packed like they are +in our case. The `imageSubresource`, `imageOffset` and `imageExtent` fields +indicate to which part of the image we want to copy the pixels. + +Buffer to image copy operations are enqueued using the `vkCmdCopyBufferToImage` +function: + +```c++ +vkCmdCopyBufferToImage( + commandBuffer, + buffer, + image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, + ®ion +); +``` + +The fourth parameter indicates which layout the image is currently using. I'm +assuming here that the image has already been transitioned to the layout that is +optimal for copying pixels to. Right now we're only copying one chunk of pixels +to the whole image, but it's possible to specify an array of `VkBufferImageCopy` +to perform many different copies from this buffer to the image in one operation. + +## Preparing the texture image + +We now have all of the tools we need to finish setting up the texture image, so +we're going back to the `createTextureImage` function. The last thing we did +there was creating the texture image. The next step is to copy the staging +buffer to the texture image. This involves two steps: + +* Transition the texture image to `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL` +* Execute the buffer to image copy operation + +This is easy to do with the functions we just created: + +```c++ +transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); +copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast(texHeight)); +``` + +The image was created with the `VK_IMAGE_LAYOUT_UNDEFINED` layout, so that one +should be specified as old layout when transitioning `textureImage`. Remember +that we can do this because we don't care about its contents before performing +the copy operation. + +To be able to start sampling from the texture image in the shader, we need one +last transition to prepare it for shader access: + +```c++ +transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); +``` + +## Transition barrier masks + +If you run your application with validation layers enabled now, then you'll see that +it complains about the access masks and pipeline stages in +`transitionImageLayout` being invalid. We still need to set those based on the +layouts in the transition. + +There are two transitions we need to handle: + +* Undefined → transfer destination: transfer writes that don't need to wait on +anything +* Transfer destination → shader reading: shader reads should wait on transfer +writes, specifically the shader reads in the fragment shader, because that's +where we're going to use the texture + +These rules are specified using the following access masks and pipeline stages: + +```c++ +VkPipelineStageFlags sourceStage; +VkPipelineStageFlags destinationStage; + +if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) { + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + + sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; +} else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) { + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; +} else { + throw std::invalid_argument("unsupported layout transition!"); +} + +vkCmdPipelineBarrier( + commandBuffer, + sourceStage, destinationStage, + 0, + 0, nullptr, + 0, nullptr, + 1, &barrier +); +``` + +As you can see in the aforementioned table, transfer writes must occur in the +pipeline transfer stage. Since the writes don't have to wait on anything, you +may specify an empty access mask and the earliest possible pipeline stage +`VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT` for the pre-barrier operations. It should be +noted that `VK_PIPELINE_STAGE_TRANSFER_BIT` is not a *real* stage within the +graphics and compute pipelines. It is more of a pseudo-stage where transfers +happen. See [the documentation](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#VkPipelineStageFlagBits) +for more information and other examples of pseudo-stages. + +The image will be written in the same pipeline stage and subsequently read by +the fragment shader, which is why we specify shader reading access in the +fragment shader pipeline stage. + +If we need to do more transitions in the future, then we'll extend the function. +The application should now run successfully, although there are of course no +visual changes yet. + +One thing to note is that command buffer submission results in implicit +`VK_ACCESS_HOST_WRITE_BIT` synchronization at the beginning. Since the +`transitionImageLayout` function executes a command buffer with only a single +command, you could use this implicit synchronization and set `srcAccessMask` to +`0` if you ever needed a `VK_ACCESS_HOST_WRITE_BIT` dependency in a layout +transition. It's up to you if you want to be explicit about it or not, but I'm +personally not a fan of relying on these OpenGL-like "hidden" operations. + +There is actually a special type of image layout that supports all operations, +`VK_IMAGE_LAYOUT_GENERAL`. The problem with it, of course, is that it doesn't +necessarily offer the best performance for any operation. It is required for +some special cases, like using an image as both input and output, or for reading +an image after it has left the preinitialized layout. + +All of the helper functions that submit commands so far have been set up to +execute synchronously by waiting for the queue to become idle. For practical +applications it is recommended to combine these operations in a single command +buffer and execute them asynchronously for higher throughput, especially the +transitions and copy in the `createTextureImage` function. Try to experiment +with this by creating a `setupCommandBuffer` that the helper functions record +commands into, and add a `flushSetupCommands` to execute the commands that have +been recorded so far. It's best to do this after the texture mapping works to +check if the texture resources are still set up correctly. + +## Cleanup + +Finish the `createTextureImage` function by cleaning up the staging buffer and +its memory at the end: + +```c++ + transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingBufferMemory, nullptr); +} +``` + +The main texture image is used until the end of the program: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyImage(device, textureImage, nullptr); + vkFreeMemory(device, textureImageMemory, nullptr); + + ... +} +``` + +The image now contains the texture, but we still need a way to access it from +the graphics pipeline. We'll work on that in the next chapter. + +[C++ code](/code/24_texture_image.cpp) / +[Vertex shader](/code/22_shader_ubo.vert) / +[Fragment shader](/code/22_shader_ubo.frag) diff --git a/ko/06_Texture_mapping/01_Image_view_and_sampler.md b/ko/06_Texture_mapping/01_Image_view_and_sampler.md new file mode 100644 index 00000000..9d98c9e4 --- /dev/null +++ b/ko/06_Texture_mapping/01_Image_view_and_sampler.md @@ -0,0 +1,369 @@ +In this chapter we're going to create two more resources that are needed for the +graphics pipeline to sample an image. The first resource is one that we've +already seen before while working with the swap chain images, but the second one +is new - it relates to how the shader will read texels from the image. + +## Texture image view + +We've seen before, with the swap chain images and the framebuffer, that images +are accessed through image views rather than directly. We will also need to +create such an image view for the texture image. + +Add a class member to hold a `VkImageView` for the texture image and create a +new function `createTextureImageView` where we'll create it: + +```c++ +VkImageView textureImageView; + +... + +void initVulkan() { + ... + createTextureImage(); + createTextureImageView(); + createVertexBuffer(); + ... +} + +... + +void createTextureImageView() { + +} +``` + +The code for this function can be based directly on `createImageViews`. The only +two changes you have to make are the `format` and the `image`: + +```c++ +VkImageViewCreateInfo viewInfo{}; +viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; +viewInfo.image = textureImage; +viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; +viewInfo.format = VK_FORMAT_R8G8B8A8_SRGB; +viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +viewInfo.subresourceRange.baseMipLevel = 0; +viewInfo.subresourceRange.levelCount = 1; +viewInfo.subresourceRange.baseArrayLayer = 0; +viewInfo.subresourceRange.layerCount = 1; +``` + +I've left out the explicit `viewInfo.components` initialization, because +`VK_COMPONENT_SWIZZLE_IDENTITY` is defined as `0` anyway. Finish creating the +image view by calling `vkCreateImageView`: + +```c++ +if (vkCreateImageView(device, &viewInfo, nullptr, &textureImageView) != VK_SUCCESS) { + throw std::runtime_error("failed to create texture image view!"); +} +``` + +Because so much of the logic is duplicated from `createImageViews`, you may wish +to abstract it into a new `createImageView` function: + +```c++ +VkImageView createImageView(VkImage image, VkFormat format) { + VkImageViewCreateInfo viewInfo{}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + viewInfo.subresourceRange.baseMipLevel = 0; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.baseArrayLayer = 0; + viewInfo.subresourceRange.layerCount = 1; + + VkImageView imageView; + if (vkCreateImageView(device, &viewInfo, nullptr, &imageView) != VK_SUCCESS) { + throw std::runtime_error("failed to create image view!"); + } + + return imageView; +} +``` + +The `createTextureImageView` function can now be simplified to: + +```c++ +void createTextureImageView() { + textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_SRGB); +} +``` + +And `createImageViews` can be simplified to: + +```c++ +void createImageViews() { + swapChainImageViews.resize(swapChainImages.size()); + + for (uint32_t i = 0; i < swapChainImages.size(); i++) { + swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat); + } +} +``` + +Make sure to destroy the image view at the end of the program, right before +destroying the image itself: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyImageView(device, textureImageView, nullptr); + + vkDestroyImage(device, textureImage, nullptr); + vkFreeMemory(device, textureImageMemory, nullptr); +``` + +## Samplers + +It is possible for shaders to read texels directly from images, but that is not +very common when they are used as textures. Textures are usually accessed +through samplers, which will apply filtering and transformations to compute the +final color that is retrieved. + +These filters are helpful to deal with problems like oversampling. Consider a +texture that is mapped to geometry with more fragments than texels. If you +simply took the closest texel for the texture coordinate in each fragment, then +you would get a result like the first image: + +![](/images/texture_filtering.png) + +If you combined the 4 closest texels through linear interpolation, then you +would get a smoother result like the one on the right. Of course your +application may have art style requirements that fit the left style more (think +Minecraft), but the right is preferred in conventional graphics applications. A +sampler object automatically applies this filtering for you when reading a color +from the texture. + +Undersampling is the opposite problem, where you have more texels than +fragments. This will lead to artifacts when sampling high frequency patterns +like a checkerboard texture at a sharp angle: + +![](/images/anisotropic_filtering.png) + +As shown in the left image, the texture turns into a blurry mess in the +distance. The solution to this is [anisotropic filtering](https://en.wikipedia.org/wiki/Anisotropic_filtering), +which can also be applied automatically by a sampler. + +Aside from these filters, a sampler can also take care of transformations. It +determines what happens when you try to read texels outside the image through +its *addressing mode*. The image below displays some of the possibilities: + +![](/images/texture_addressing.png) + +We will now create a function `createTextureSampler` to set up such a sampler +object. We'll be using that sampler to read colors from the texture in the +shader later on. + +```c++ +void initVulkan() { + ... + createTextureImage(); + createTextureImageView(); + createTextureSampler(); + ... +} + +... + +void createTextureSampler() { + +} +``` + +Samplers are configured through a `VkSamplerCreateInfo` structure, which +specifies all filters and transformations that it should apply. + +```c++ +VkSamplerCreateInfo samplerInfo{}; +samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; +samplerInfo.magFilter = VK_FILTER_LINEAR; +samplerInfo.minFilter = VK_FILTER_LINEAR; +``` + +The `magFilter` and `minFilter` fields specify how to interpolate texels that +are magnified or minified. Magnification concerns the oversampling problem +describes above, and minification concerns undersampling. The choices are +`VK_FILTER_NEAREST` and `VK_FILTER_LINEAR`, corresponding to the modes +demonstrated in the images above. + +```c++ +samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; +samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; +samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; +``` + +The addressing mode can be specified per axis using the `addressMode` fields. +The available values are listed below. Most of these are demonstrated in the +image above. Note that the axes are called U, V and W instead of X, Y and Z. +This is a convention for texture space coordinates. + +* `VK_SAMPLER_ADDRESS_MODE_REPEAT`: Repeat the texture when going beyond the +image dimensions. +* `VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT`: Like repeat, but inverts the +coordinates to mirror the image when going beyond the dimensions. +* `VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE`: Take the color of the edge closest to +the coordinate beyond the image dimensions. +* `VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE`: Like clamp to edge, but +instead uses the edge opposite to the closest edge. +* `VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER`: Return a solid color when sampling +beyond the dimensions of the image. + +It doesn't really matter which addressing mode we use here, because we're not +going to sample outside of the image in this tutorial. However, the repeat mode +is probably the most common mode, because it can be used to tile textures like +floors and walls. + +```c++ +samplerInfo.anisotropyEnable = VK_TRUE; +samplerInfo.maxAnisotropy = ???; +``` + +These two fields specify if anisotropic filtering should be used. There is no +reason not to use this unless performance is a concern. The `maxAnisotropy` +field limits the amount of texel samples that can be used to calculate the final +color. A lower value results in better performance, but lower quality results. +To figure out which value we can use, we need to retrieve the properties of the physical device like so: + +```c++ +VkPhysicalDeviceProperties properties{}; +vkGetPhysicalDeviceProperties(physicalDevice, &properties); +``` + +If you look at the documentation for the `VkPhysicalDeviceProperties` structure, you'll see that it contains a `VkPhysicalDeviceLimits` member named `limits`. This struct in turn has a member called `maxSamplerAnisotropy` and this is the maximum value we can specify for `maxAnisotropy`. If we want to go for maximum quality, we can simply use that value directly: + +```c++ +samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy; +``` + +You can either query the properties at the beginning of your program and pass them around to the functions that need them, or query them in the `createTextureSampler` function itself. + +```c++ +samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; +``` + +The `borderColor` field specifies which color is returned when sampling beyond +the image with clamp to border addressing mode. It is possible to return black, +white or transparent in either float or int formats. You cannot specify an +arbitrary color. + +```c++ +samplerInfo.unnormalizedCoordinates = VK_FALSE; +``` + +The `unnormalizedCoordinates` field specifies which coordinate system you want +to use to address texels in an image. If this field is `VK_TRUE`, then you can +simply use coordinates within the `[0, texWidth)` and `[0, texHeight)` range. If +it is `VK_FALSE`, then the texels are addressed using the `[0, 1)` range on all +axes. Real-world applications almost always use normalized coordinates, because +then it's possible to use textures of varying resolutions with the exact same +coordinates. + +```c++ +samplerInfo.compareEnable = VK_FALSE; +samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS; +``` + +If a comparison function is enabled, then texels will first be compared to a +value, and the result of that comparison is used in filtering operations. This +is mainly used for [percentage-closer filtering](https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch11.html) +on shadow maps. We'll look at this in a future chapter. + +```c++ +samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; +samplerInfo.mipLodBias = 0.0f; +samplerInfo.minLod = 0.0f; +samplerInfo.maxLod = 0.0f; +``` + +All of these fields apply to mipmapping. We will look at mipmapping in a [later +chapter](/Generating_Mipmaps), but basically it's another type of filter that can be applied. + +The functioning of the sampler is now fully defined. Add a class member to +hold the handle of the sampler object and create the sampler with +`vkCreateSampler`: + +```c++ +VkImageView textureImageView; +VkSampler textureSampler; + +... + +void createTextureSampler() { + ... + + if (vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler) != VK_SUCCESS) { + throw std::runtime_error("failed to create texture sampler!"); + } +} +``` + +Note the sampler does not reference a `VkImage` anywhere. The sampler is a +distinct object that provides an interface to extract colors from a texture. It +can be applied to any image you want, whether it is 1D, 2D or 3D. This is +different from many older APIs, which combined texture images and filtering into +a single state. + +Destroy the sampler at the end of the program when we'll no longer be accessing +the image: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroySampler(device, textureSampler, nullptr); + vkDestroyImageView(device, textureImageView, nullptr); + + ... +} +``` + +## Anisotropy device feature + +If you run your program right now, you'll see a validation layer message like +this: + +![](/images/validation_layer_anisotropy.png) + +That's because anisotropic filtering is actually an optional device feature. We +need to update the `createLogicalDevice` function to request it: + +```c++ +VkPhysicalDeviceFeatures deviceFeatures{}; +deviceFeatures.samplerAnisotropy = VK_TRUE; +``` + +And even though it is very unlikely that a modern graphics card will not support +it, we should update `isDeviceSuitable` to check if it is available: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + ... + + VkPhysicalDeviceFeatures supportedFeatures; + vkGetPhysicalDeviceFeatures(device, &supportedFeatures); + + return indices.isComplete() && extensionsSupported && swapChainAdequate && supportedFeatures.samplerAnisotropy; +} +``` + +The `vkGetPhysicalDeviceFeatures` repurposes the `VkPhysicalDeviceFeatures` +struct to indicate which features are supported rather than requested by setting +the boolean values. + +Instead of enforcing the availability of anisotropic filtering, it's also +possible to simply not use it by conditionally setting: + +```c++ +samplerInfo.anisotropyEnable = VK_FALSE; +samplerInfo.maxAnisotropy = 1.0f; +``` + +In the next chapter we will expose the image and sampler objects to the shaders +to draw the texture onto the square. + +[C++ code](/code/25_sampler.cpp) / +[Vertex shader](/code/22_shader_ubo.vert) / +[Fragment shader](/code/22_shader_ubo.frag) diff --git a/ko/06_Texture_mapping/02_Combined_image_sampler.md b/ko/06_Texture_mapping/02_Combined_image_sampler.md new file mode 100644 index 00000000..0f1e5496 --- /dev/null +++ b/ko/06_Texture_mapping/02_Combined_image_sampler.md @@ -0,0 +1,296 @@ +## Introduction + +We looked at descriptors for the first time in the uniform buffers part of the +tutorial. In this chapter we will look at a new type of descriptor: *combined +image sampler*. This descriptor makes it possible for shaders to access an image +resource through a sampler object like the one we created in the previous +chapter. + +We'll start by modifying the descriptor set layout, descriptor pool and descriptor +set to include such a combined image sampler descriptor. After that, we're going +to add texture coordinates to `Vertex` and modify the fragment shader to read +colors from the texture instead of just interpolating the vertex colors. + +## Updating the descriptors + +Browse to the `createDescriptorSetLayout` function and add a +`VkDescriptorSetLayoutBinding` for a combined image sampler descriptor. We'll +simply put it in the binding after the uniform buffer: + +```c++ +VkDescriptorSetLayoutBinding samplerLayoutBinding{}; +samplerLayoutBinding.binding = 1; +samplerLayoutBinding.descriptorCount = 1; +samplerLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; +samplerLayoutBinding.pImmutableSamplers = nullptr; +samplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + +std::array bindings = {uboLayoutBinding, samplerLayoutBinding}; +VkDescriptorSetLayoutCreateInfo layoutInfo{}; +layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; +layoutInfo.bindingCount = static_cast(bindings.size()); +layoutInfo.pBindings = bindings.data(); +``` + +Make sure to set the `stageFlags` to indicate that we intend to use the combined +image sampler descriptor in the fragment shader. That's where the color of the +fragment is going to be determined. It is possible to use texture sampling in +the vertex shader, for example to dynamically deform a grid of vertices by a +[heightmap](https://en.wikipedia.org/wiki/Heightmap). + +We must also create a larger descriptor pool to make room for the allocation +of the combined image sampler by adding another `VkPoolSize` of type +`VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER` to the +`VkDescriptorPoolCreateInfo`. Go to the `createDescriptorPool` function and +modify it to include a `VkDescriptorPoolSize` for this descriptor: + +```c++ +std::array poolSizes{}; +poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +poolSizes[0].descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); +poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; +poolSizes[1].descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); + +VkDescriptorPoolCreateInfo poolInfo{}; +poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; +poolInfo.poolSizeCount = static_cast(poolSizes.size()); +poolInfo.pPoolSizes = poolSizes.data(); +poolInfo.maxSets = static_cast(MAX_FRAMES_IN_FLIGHT); +``` + +Inadequate descriptor pools are a good example of a problem that the validation +layers will not catch: As of Vulkan 1.1, `vkAllocateDescriptorSets` may fail +with the error code `VK_ERROR_POOL_OUT_OF_MEMORY` if the pool is not +sufficiently large, but the driver may also try to solve the problem internally. +This means that sometimes (depending on hardware, pool size and allocation size) +the driver will let us get away with an allocation that exceeds the limits of +our descriptor pool. Other times, `vkAllocateDescriptorSets` will fail and +return `VK_ERROR_POOL_OUT_OF_MEMORY`. This can be particularly frustrating if +the allocation succeeds on some machines, but fails on others. + +Since Vulkan shifts the responsiblity for the allocation to the driver, it is no +longer a strict requirement to only allocate as many descriptors of a certain +type (`VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER`, etc.) as specified by the +corresponding `descriptorCount` members for the creation of the descriptor pool. +However, it remains best practise to do so, and in the future, +`VK_LAYER_KHRONOS_validation` will warn about this type of problem if you enable +[Best Practice Validation](https://vulkan.lunarg.com/doc/view/1.4.304.0/linux/best_practices.html). + +The final step is to bind the actual image and sampler resources to the +descriptors in the descriptor set. Go to the `createDescriptorSets` function. + +```c++ +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + VkDescriptorBufferInfo bufferInfo{}; + bufferInfo.buffer = uniformBuffers[i]; + bufferInfo.offset = 0; + bufferInfo.range = sizeof(UniformBufferObject); + + VkDescriptorImageInfo imageInfo{}; + imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imageInfo.imageView = textureImageView; + imageInfo.sampler = textureSampler; + + ... +} +``` + +The resources for a combined image sampler structure must be specified in a +`VkDescriptorImageInfo` struct, just like the buffer resource for a uniform +buffer descriptor is specified in a `VkDescriptorBufferInfo` struct. This is +where the objects from the previous chapter come together. + +```c++ +std::array descriptorWrites{}; + +descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; +descriptorWrites[0].dstSet = descriptorSets[i]; +descriptorWrites[0].dstBinding = 0; +descriptorWrites[0].dstArrayElement = 0; +descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +descriptorWrites[0].descriptorCount = 1; +descriptorWrites[0].pBufferInfo = &bufferInfo; + +descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; +descriptorWrites[1].dstSet = descriptorSets[i]; +descriptorWrites[1].dstBinding = 1; +descriptorWrites[1].dstArrayElement = 0; +descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; +descriptorWrites[1].descriptorCount = 1; +descriptorWrites[1].pImageInfo = &imageInfo; + +vkUpdateDescriptorSets(device, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); +``` + +The descriptors must be updated with this image info, just like the buffer. This +time we're using the `pImageInfo` array instead of `pBufferInfo`. The descriptors +are now ready to be used by the shaders! + +## Texture coordinates + +There is one important ingredient for texture mapping that is still missing, and +that's the actual texture coordinates for each vertex. The texture coordinates determine how the +image is actually mapped to the geometry. + +```c++ +struct Vertex { + glm::vec2 pos; + glm::vec3 color; + glm::vec2 texCoord; + + static VkVertexInputBindingDescription getBindingDescription() { + VkVertexInputBindingDescription bindingDescription{}; + bindingDescription.binding = 0; + bindingDescription.stride = sizeof(Vertex); + bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + return bindingDescription; + } + + static std::array getAttributeDescriptions() { + std::array attributeDescriptions{}; + + attributeDescriptions[0].binding = 0; + attributeDescriptions[0].location = 0; + attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; + attributeDescriptions[0].offset = offsetof(Vertex, pos); + + attributeDescriptions[1].binding = 0; + attributeDescriptions[1].location = 1; + attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; + attributeDescriptions[1].offset = offsetof(Vertex, color); + + attributeDescriptions[2].binding = 0; + attributeDescriptions[2].location = 2; + attributeDescriptions[2].format = VK_FORMAT_R32G32_SFLOAT; + attributeDescriptions[2].offset = offsetof(Vertex, texCoord); + + return attributeDescriptions; + } +}; +``` + +Modify the `Vertex` struct to include a `vec2` for texture coordinates. Make +sure to also add a `VkVertexInputAttributeDescription` so that we can use access +texture coordinates as input in the vertex shader. That is necessary to be able +to pass them to the fragment shader for interpolation across the surface of the +square. + +```c++ +const std::vector vertices = { + {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}}, + {{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}}, + {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}}, + {{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}} +}; +``` + +In this tutorial, I will simply fill the square with the texture by using +coordinates from `0, 0` in the top-left corner to `1, 1` in the bottom-right +corner. Feel free to experiment with different coordinates. Try using +coordinates below `0` or above `1` to see the addressing modes in action! + +## Shaders + +The final step is modifying the shaders to sample colors from the texture. We +first need to modify the vertex shader to pass through the texture coordinates +to the fragment shader: + +```glsl +layout(location = 0) in vec2 inPosition; +layout(location = 1) in vec3 inColor; +layout(location = 2) in vec2 inTexCoord; + +layout(location = 0) out vec3 fragColor; +layout(location = 1) out vec2 fragTexCoord; + +void main() { + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0); + fragColor = inColor; + fragTexCoord = inTexCoord; +} +``` + +Just like the per vertex colors, the `fragTexCoord` values will be smoothly +interpolated across the area of the square by the rasterizer. We can visualize +this by having the fragment shader output the texture coordinates as colors: + +```glsl +#version 450 + +layout(location = 0) in vec3 fragColor; +layout(location = 1) in vec2 fragTexCoord; + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(fragTexCoord, 0.0, 1.0); +} +``` + +You should see something like the image below. Don't forget to recompile the +shaders! + +![](/images/texcoord_visualization.png) + +The green channel represents the horizontal coordinates and the red channel the +vertical coordinates. The black and yellow corners confirm that the texture +coordinates are correctly interpolated from `0, 0` to `1, 1` across the square. +Visualizing data using colors is the shader programming equivalent of `printf` +debugging, for lack of a better option! + +A combined image sampler descriptor is represented in GLSL by a sampler uniform. +Add a reference to it in the fragment shader: + +```glsl +layout(binding = 1) uniform sampler2D texSampler; +``` + +There are equivalent `sampler1D` and `sampler3D` types for other types of +images. Make sure to use the correct binding here. + +```glsl +void main() { + outColor = texture(texSampler, fragTexCoord); +} +``` + +Textures are sampled using the built-in `texture` function. It takes a `sampler` +and coordinate as arguments. The sampler automatically takes care of the +filtering and transformations in the background. You should now see the texture +on the square when you run the application: + +![](/images/texture_on_square.png) + +Try experimenting with the addressing modes by scaling the texture coordinates +to values higher than `1`. For example, the following fragment shader produces +the result in the image below when using `VK_SAMPLER_ADDRESS_MODE_REPEAT`: + +```glsl +void main() { + outColor = texture(texSampler, fragTexCoord * 2.0); +} +``` + +![](/images/texture_on_square_repeated.png) + +You can also manipulate the texture colors using the vertex colors: + +```glsl +void main() { + outColor = vec4(fragColor * texture(texSampler, fragTexCoord).rgb, 1.0); +} +``` + +I've separated the RGB and alpha channels here to not scale the alpha channel. + +![](/images/texture_on_square_colorized.png) + +You now know how to access images in shaders! This is a very powerful technique +when combined with images that are also written to in framebuffers. You can use +these images as inputs to implement cool effects like post-processing and camera +displays within the 3D world. + +[C++ code](/code/26_texture_mapping.cpp) / +[Vertex shader](/code/26_shader_textures.vert) / +[Fragment shader](/code/26_shader_textures.frag) diff --git a/ko/07_Depth_buffering.md b/ko/07_Depth_buffering.md new file mode 100644 index 00000000..a896c219 --- /dev/null +++ b/ko/07_Depth_buffering.md @@ -0,0 +1,498 @@ +## 소개 + +지금까지 우리가 다루었던 지오메트리는 3D로 투영되었지만, 실제로는 완전히 평면이었습니다. 이번 장에서는 3D 메시를 준비하기 위해 위치(position)에 Z 좌표를 추가할 것입니다. 이 세 번째 좌표를 사용하여 현재 사각형 위에 또 다른 사각형을 배치함으로써, 지오메트리가 깊이 순으로 정렬되지 않았을 때 발생하는 문제를 직접 확인해 보겠습니다. + +## 3D 지오메트리 + +먼저 `Vertex` 구조체를 변경하여 위치에 3D 벡터를 사용하고, 그에 맞춰 `VkVertexInputAttributeDescription`의 `format`을 업데이트합니다. + +```c++ +struct Vertex { + glm::vec3 pos; + glm::vec3 color; + glm::vec2 texCoord; + + ... + + static std::array getAttributeDescriptions() { + std::array attributeDescriptions{}; + + attributeDescriptions[0].binding = 0; + attributeDescriptions[0].location = 0; + attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT; + attributeDescriptions[0].offset = offsetof(Vertex, pos); + + ... + } +}; +``` + +다음으로, 정점 셰이더가 3D 좌표를 입력으로 받아 변환하도록 수정합니다. 수정 후에는 반드시 셰이더를 다시 컴파일해야 합니다! + +```glsl +layout(location = 0) in vec3 inPosition; + +... + +void main() { + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0); + fragColor = inColor; + fragTexCoord = inTexCoord; +} +``` + +마지막으로, `vertices` 컨테이너를 Z 좌표를 포함하도록 업데이트합니다. + +```c++ +const std::vector vertices = { + {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}}, + {{0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}}, + {{0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}}, + {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}} +}; +``` + +지금 애플리케이션을 실행하면 이전과 완전히 동일한 결과를 볼 수 있습니다. 이제 장면을 더 흥미롭게 만들고 이번 장에서 다룰 문제를 보여주기 위해 지오메트리를 추가할 시간입니다. 현재 사각형 바로 아래에 위치할 사각형을 정의하기 위해 정점들을 복제합니다. + +![](/images/extra_square.svg) + +새 사각형의 Z 좌표는 `-0.5f`로 설정하고, 추가된 사각형에 대한 인덱스도 추가합니다. + +```c++ +const std::vector vertices = { + {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}}, + {{0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}}, + {{0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}}, + {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}}, + + {{-0.5f, -0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}}, + {{0.5f, -0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}}, + {{0.5f, 0.5f, -0.5f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}}, + {{-0.5f, 0.5f, -0.5f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}} +}; + +const std::vector indices = { + 0, 1, 2, 2, 3, 0, + 4, 5, 6, 6, 7, 4 +}; +``` + +이제 프로그램을 실행하면 마치 에셔(Escher)의 그림과 같은 이상한 결과물을 보게 될 것입니다. + +![](/images/depth_issues.png) + +이 문제의 원인은 아래쪽 사각형의 프래그먼트가 단순히 인덱스 배열의 뒤에 온다는 이유만으로 위쪽 사각형의 프래그먼트 위에 그려지기 때문입니다. 이 문제를 해결하는 방법은 두 가지가 있습니다. + +* 모든 그리기 호출(draw call)을 뒤쪽에서 앞쪽 순서로 깊이에 따라 정렬하기 +* 깊이 버퍼(depth buffer)를 이용한 깊이 테스팅(depth testing) 사용하기 + +첫 번째 접근 방식은 보통 투명한 객체를 그릴 때 사용됩니다. 순서에 상관없는 투명도 처리는 해결하기 어려운 문제이기 때문입니다. 하지만 프래그먼트를 깊이 순으로 정렬하는 문제는 보통 **깊이 버퍼**를 사용하여 해결합니다. 깊이 버퍼는 색상 첨부(color attachment)가 모든 위치의 색상을 저장하는 것처럼, 모든 위치의 깊이(depth) 값을 저장하는 추가적인 첨부입니다. 래스터라이저가 프래그먼트를 생성할 때마다 깊이 테스트는 새 프래그먼트가 이전 프래그먼트보다 더 가까운지 확인합니다. 그렇지 않다면 새 프래그먼트는 폐기됩니다. 깊이 테스트를 통과한 프래그먼트는 자신의 깊이 값을 깊이 버퍼에 기록합니다. 프래그먼트 셰이더에서 색상 출력을 조작할 수 있듯이 이 깊이 값도 조작할 수 있습니다. + +```c++ +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include +#include +``` + +GLM이 생성하는 원근 투영 행렬은 기본적으로 OpenGL의 깊이 범위인 `-1.0`에서 `1.0`을 사용합니다. 우리는 `GLM_FORCE_DEPTH_ZERO_TO_ONE` 정의를 사용하여 Vulkan의 깊이 범위인 `0.0`에서 `1.0`을 사용하도록 설정해야 합니다. + +## 깊이 이미지와 이미지 뷰 + +깊이 첨부는 색상 첨부와 마찬가지로 이미지를 기반으로 합니다. 차이점은 스왑 체인이 우리를 위해 깊이 이미지를 자동으로 생성해주지 않는다는 것입니다. 우리는 단 하나의 깊이 이미지만 필요합니다. 한 번에 하나의 그리기 작업만 실행되기 때문입니다. 깊이 이미지는 다시 이미지, 메모리, 이미지 뷰라는 세 가지 리소스가 필요합니다. + +```c++ +VkImage depthImage; +VkDeviceMemory depthImageMemory; +VkImageView depthImageView; +``` + +이러한 리소스들을 설정하기 위해 `createDepthResources`라는 새 함수를 만듭니다. + +```c++ +void initVulkan() { + ... + createCommandPool(); + createDepthResources(); + createTextureImage(); + ... +} + +... + +void createDepthResources() { + +} +``` + +깊이 이미지를 만드는 것은 꽤 간단합니다. 스왑 체인 extent로 정의된 색상 첨부와 동일한 해상도를 가져야 하며, 깊이 첨부에 적합한 이미지 사용법, 최적 타일링(optimal tiling), 그리고 디바이스 로컬 메모리(device local memory)를 사용해야 합니다. 남은 유일한 질문은 "깊이 이미지에 적합한 포맷은 무엇인가?"입니다. 포맷은 깊이 구성 요소(depth component)를 포함해야 하며, 이는 `VK_FORMAT_` 이름에 `_D??_`로 표시됩니다. + +텍스처 이미지와 달리, 우리는 프로그램에서 텍셀에 직접 접근하지 않을 것이므로 특정 포맷이 반드시 필요한 것은 아닙니다. 단지 합리적인 정밀도만 가지면 되며, 실제 애플리케이션에서는 최소 24비트가 일반적입니다. 이 요구 사항을 충족하는 몇 가지 포맷이 있습니다. + +* `VK_FORMAT_D32_SFLOAT`: 깊이를 위한 32비트 부동소수점 +* `VK_FORMAT_D32_SFLOAT_S8_UINT`: 깊이를 위한 32비트 부동소수점과 스텐실(stencil)을 위한 8비트 부호 없는 정수 +* `VK_FORMAT_D24_UNORM_S8_UINT`: 깊이를 위한 24비트 정규화 부동소수점과 스텐실을 위한 8비트 부호 없는 정수 + +스텐실 구성 요소는 [스텐실 테스트](https://en.wikipedia.org/wiki/Stencil_buffer)에 사용되며, 이는 깊이 테스팅과 결합할 수 있는 추가적인 테스트입니다. 이는 이후 튜토리얼에서 다룰 것입니다. + +단순히 `VK_FORMAT_D32_SFLOAT` 포맷을 선택할 수도 있습니다. 이 포맷은 매우 보편적으로 지원되기 때문입니다. 하지만 가능하면 애플리케이션에 유연성을 더하는 것이 좋습니다. 우리는 가장 선호하는 포맷부터 순서대로 후보 목록을 받아 지원되는 첫 번째 포맷을 찾는 `findSupportedFormat` 함수를 작성할 것입니다. + +```c++ +VkFormat findSupportedFormat(const std::vector& candidates, VkImageTiling tiling, VkFormatFeatureFlags features) { + +} +``` + +포맷 지원 여부는 타일링 모드와 사용법에 따라 달라지므로, 이들을 매개변수로 포함해야 합니다. 포맷 지원 여부는 `vkGetPhysicalDeviceFormatProperties` 함수로 질의할 수 있습니다. + +```c++ +for (VkFormat format : candidates) { + VkFormatProperties props; + vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props); +} +``` + +`VkFormatProperties` 구조체는 세 개의 필드를 포함합니다. + +* `linearTilingFeatures`: 선형 타일링에서 지원되는 사용 사례 +* `optimalTilingFeatures`: 최적 타일링에서 지원되는 사용 사례 +* `bufferFeatures`: 버퍼에서 지원되는 사용 사례 + +여기서는 첫 두 필드만 관련이 있으며, 확인해야 할 필드는 함수의 `tiling` 매개변수에 따라 달라집니다. + +```c++ +if (tiling == VK_IMAGE_TILING_LINEAR && (props.linearTilingFeatures & features) == features) { + return format; +} else if (tiling == VK_IMAGE_TILING_OPTIMAL && (props.optimalTilingFeatures & features) == features) { + return format; +} +``` + +만약 후보 포맷 중 어느 것도 원하는 사용법을 지원하지 않는다면, 특별한 값을 반환하거나 예외를 던질 수 있습니다. + +```c++ +VkFormat findSupportedFormat(const std::vector& candidates, VkImageTiling tiling, VkFormatFeatureFlags features) { + for (VkFormat format : candidates) { + VkFormatProperties props; + vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props); + + if (tiling == VK_IMAGE_TILING_LINEAR && (props.linearTilingFeatures & features) == features) { + return format; + } else if (tiling == VK_IMAGE_TILING_OPTIMAL && (props.optimalTilingFeatures & features) == features) { + return format; + } + } + + throw std::runtime_error("failed to find supported format!"); +} +``` + +이제 이 함수를 사용하여 깊이 첨부로 사용 가능한 깊이 구성 요소를 가진 포맷을 선택하는 `findDepthFormat` 헬퍼 함수를 만들 것입니다. + +```c++ +VkFormat findDepthFormat() { + return findSupportedFormat( + {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D24_UNORM_S8_UINT}, + VK_IMAGE_TILING_OPTIMAL, + VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT + ); +} +``` + +이 경우에는 `VK_IMAGE_USAGE_` 플래그 대신 `VK_FORMAT_FEATURE_` 플래그를 사용해야 합니다. 이 후보 포맷들은 모두 깊이 구성 요소를 포함하며, 후자의 두 포맷은 스텐실 구성 요소도 포함합니다. 아직 스텐실을 사용하지는 않겠지만, 이 포맷을 가진 이미지의 레이아웃을 전환할 때는 이를 고려해야 합니다. 선택된 깊이 포맷이 스텐실 구성 요소를 포함하는지 알려주는 간단한 헬퍼 함수를 추가합니다. + +```c++ +bool hasStencilComponent(VkFormat format) { + return format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT; +} +``` + +`createDepthResources` 함수에서 깊이 포맷을 찾기 위해 이 함수를 호출합니다. + +```c++ +VkFormat depthFormat = findDepthFormat(); +``` + +이제 `createImage`와 `createImageView` 헬퍼 함수를 호출하는 데 필요한 모든 정보를 갖추었습니다. + +```c++ +createImage(swapChainExtent.width, swapChainExtent.height, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory); +depthImageView = createImageView(depthImage, depthFormat); +``` + +하지만, `createImageView` 함수는 현재 서브리소스가 항상 `VK_IMAGE_ASPECT_COLOR_BIT`라고 가정하고 있으므로, 해당 필드를 매개변수로 만들어야 합니다. + +```c++ +VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags) { + ... + viewInfo.subresourceRange.aspectMask = aspectFlags; + ... +} +``` + +이 함수를 호출하는 모든 곳을 올바른 aspect를 사용하도록 업데이트합니다. + +```c++ +swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat, VK_IMAGE_ASPECT_COLOR_BIT); +... +depthImageView = createImageView(depthImage, depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT); +... +textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_ASPECT_COLOR_BIT); +``` + +이것으로 깊이 이미지 생성은 끝입니다. 우리는 렌더 패스 시작 시 색상 첨부처럼 깊이 첨부도 소거(clear)할 것이기 때문에, 메모리를 매핑하거나 다른 이미지를 복사할 필요가 없습니다. + +### 깊이 이미지 명시적 전환 + +깊이 첨부로의 이미지 레이아웃 전환은 렌더 패스에서 처리할 것이므로 명시적으로 전환할 필요는 없습니다. 하지만 완전성을 위해 이 섹션에서 그 과정을 설명합니다. 원한다면 이 섹션을 건너뛰어도 좋습니다. + +`createDepthResources` 함수 끝에서 `transitionImageLayout`을 호출합니다. + +```c++ +transitionImageLayout(depthImage, depthFormat, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); +``` + +기존 깊이 이미지 내용이 중요하지 않으므로, `undefined` 레이아웃을 초기 레이아웃으로 사용할 수 있습니다. `transitionImageLayout`의 로직 일부를 수정하여 올바른 서브리소스 aspect를 사용하도록 해야 합니다. + +```c++ +if (newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) { + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; + + if (hasStencilComponent(format)) { + barrier.subresourceRange.aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT; + } +} else { + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +} +``` + +비록 스텐실 구성 요소를 사용하지 않더라도, 깊이 이미지의 레이아웃 전환에는 이를 포함해야 합니다. + +마지막으로, 올바른 접근 마스크(access mask)와 파이프라인 단계를 추가합니다. + +```c++ +if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) { + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + + sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; +} else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) { + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; +} else if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) { + barrier.srcAccessMask = 0; + barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + + sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + destinationStage = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; +} else { + throw std::invalid_argument("unsupported layout transition!"); +} +``` + +깊이 버퍼는 프래그먼트가 보이는지 확인하기 위한 깊이 테스트를 위해 읽히고, 새 프래그먼트가 그려질 때 쓰여집니다. 읽기는 `VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT` 단계에서, 쓰기는 `VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT` 단계에서 발생합니다. 지정된 작업과 일치하는 가장 이른 파이프라인 단계를 선택하여, 깊이 첨부로 사용될 필요가 있을 때 준비되도록 해야 합니다. + +## 렌더 패스 + +이제 `createRenderPass`를 수정하여 깊이 첨부를 포함하도록 하겠습니다. 먼저 `VkAttachmentDescription`을 지정합니다. + +```c++ +VkAttachmentDescription depthAttachment{}; +depthAttachment.format = findDepthFormat(); +depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT; +depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; +depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; +depthAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; +depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; +depthAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; +depthAttachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; +``` + +`format`은 깊이 이미지 자체와 동일해야 합니다. 이번에는 깊이 데이터를 저장하는 데 신경 쓰지 않으므로(`storeOp`), `VK_ATTACHMENT_STORE_OP_DONT_CARE`를 사용합니다. 그리기가 끝난 후에는 사용되지 않을 것이기 때문입니다. 이는 하드웨어가 추가적인 최적화를 수행할 수 있게 해줍니다. 색상 버퍼와 마찬가지로, 이전 깊이 내용에는 신경 쓰지 않으므로 `initialLayout`으로 `VK_IMAGE_LAYOUT_UNDEFINED`를 사용할 수 있습니다. + +```c++ +VkAttachmentReference depthAttachmentRef{}; +depthAttachmentRef.attachment = 1; +depthAttachmentRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; +``` + +첫 번째(이자 유일한) 서브패스를 위해 이 첨부에 대한 참조를 추가합니다. + +```c++ +VkSubpassDescription subpass{}; +subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; +subpass.colorAttachmentCount = 1; +subpass.pColorAttachments = &colorAttachmentRef; +subpass.pDepthStencilAttachment = &depthAttachmentRef; +``` + +색상 첨부와 달리, 서브패스는 단 하나의 깊이(+스텐실) 첨부만 사용할 수 있습니다. 여러 버퍼에 대해 깊이 테스트를 수행하는 것은 의미가 없습니다. + +```c++ +std::array attachments = {colorAttachment, depthAttachment}; +VkRenderPassCreateInfo renderPassInfo{}; +renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; +renderPassInfo.attachmentCount = static_cast(attachments.size()); +renderPassInfo.pAttachments = attachments.data(); +renderPassInfo.subpassCount = 1; +renderPassInfo.pSubpasses = &subpass; +... +``` + +다음으로, 렌더 패스 생성 정보를 업데이트하여 두 첨부를 모두 포함하도록 합니다. `VkRenderPassCreateInfo`를 수정하여 첨부 배열을 가리키도록 합니다. + +```c++ +VkSubpassDependency dependency{}; +dependency.srcSubpass = VK_SUBPASS_EXTERNAL; +dependency.dstSubpass = 0; +dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; +dependency.srcAccessMask = 0; +dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT; +dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + +renderPassInfo.dependencyCount = 1; +renderPassInfo.pDependencies = &dependency; +``` + +이제 서브패스 종속성을 수정하여 렌더 패스가 시작될 때 깊이 버퍼에 쓰기 작업을 수행할 수 있도록 동기화해야 합니다. 깊이 버퍼는 `loadOp`이 `CLEAR`이므로 `early fragment tests` 단계에서 쓰여집니다. 따라서 `dstStageMask`에 `VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT`를 추가하고, `dstAccessMask`에 `VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT`를 추가하여 이 쓰기 작업이 발생할 수 있도록 해야 합니다. 이 종속성은 렌더 패스가 시작되기 전에 이미지 가용성(semaphore)을 기다린 후, 깊이 버퍼 소거 작업이 시작될 수 있도록 보장합니다. + +## 프레임버퍼 + +다음 단계는 프레임버퍼 생성을 수정하여 깊이 이미지를 깊이 첨부에 바인딩하는 것입니다. `createFramebuffers`로 이동하여 깊이 이미지 뷰를 두 번째 첨부로 지정합니다. + +```c++ +std::array attachments = { + swapChainImageViews[i], + depthImageView +}; + +VkFramebufferCreateInfo framebufferInfo{}; +framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; +framebufferInfo.renderPass = renderPass; +framebufferInfo.attachmentCount = static_cast(attachments.size()); +framebufferInfo.pAttachments = attachments.data(); +framebufferInfo.width = swapChainExtent.width; +framebufferInfo.height = swapChainExtent.height; +framebufferInfo.layers = 1; +``` + +색상 첨부는 각 스왑 체인 이미지마다 다르지만, 동일한 깊이 이미지는 모든 프레임버퍼에서 사용될 수 있습니다. 우리의 세마포어 때문에 한 번에 하나의 서브패스만 실행되기 때문입니다. + +또한, 깊이 이미지 뷰가 실제로 생성된 후에 프레임버퍼가 생성되도록 `createFramebuffers` 호출을 이동해야 합니다. + +```c++ +void initVulkan() { + ... + createDepthResources(); + createFramebuffers(); + ... +} +``` + +## 소거 값 (Clear values) + +이제 `VK_ATTACHMENT_LOAD_OP_CLEAR`를 사용하는 여러 첨부가 있으므로, 여러 개의 소거 값(clear value)을 지정해야 합니다. `recordCommandBuffer`로 가서 `VkClearValue` 구조체의 배열을 만듭니다. + +```c++ +std::array clearValues{}; +clearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; +clearValues[1].depthStencil = {1.0f, 0}; + +renderPassInfo.clearValueCount = static_cast(clearValues.size()); +renderPassInfo.pClearValues = clearValues.data(); +``` + +Vulkan에서 깊이 버퍼의 깊이 범위는 `0.0`에서 `1.0`이며, `1.0`은 먼 쪽 뷰 평면(far view plane)에, `0.0`은 가까운 쪽 뷰 평면(near view plane)에 해당합니다. 깊이 버퍼의 각 지점의 초기 값은 가장 먼 깊이인 `1.0`이어야 합니다. + +`clearValues`의 순서는 첨부 파일의 순서와 동일해야 함을 유의하세요. + +## 깊이 및 스텐실 상태 + +깊이 첨부는 이제 사용할 준비가 되었지만, 그래픽 파이프라인에서 깊이 테스팅을 활성화해야 합니다. 이는 `VkPipelineDepthStencilStateCreateInfo` 구조체를 통해 구성됩니다. + +```c++ +VkPipelineDepthStencilStateCreateInfo depthStencil{}; +depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; +depthStencil.depthTestEnable = VK_TRUE; +depthStencil.depthWriteEnable = VK_TRUE; +``` + +`depthTestEnable` 필드는 새 프래그먼트의 깊이를 깊이 버퍼와 비교하여 폐기해야 하는지를 결정합니다. `depthWriteEnable` 필드는 깊이 테스트를 통과한 프래그먼트의 새 깊이가 실제로 깊이 버퍼에 쓰여야 하는지를 지정합니다. + +```c++ +depthStencil.depthCompareOp = VK_COMPARE_OP_LESS; +``` + +`depthCompareOp` 필드는 프래그먼트를 유지하거나 폐기하기 위해 수행되는 비교 연산을 지정합니다. 우리는 더 낮은 깊이 = 더 가까움을 의미하는 관례를 따르므로, 새 프래그먼트의 깊이는 이전 값보다 *작아야* 합니다(`LESS`). + +```c++ +depthStencil.depthBoundsTestEnable = VK_FALSE; +depthStencil.minDepthBounds = 0.0f; // Optional +depthStencil.maxDepthBounds = 1.0f; // Optional +``` + +`depthBoundsTestEnable`, `minDepthBounds`, `maxDepthBounds` 필드는 선택적인 깊이 경계 테스트에 사용됩니다. 기본적으로 지정된 깊이 범위 내에 있는 프래그먼트만 유지할 수 있게 해줍니다. 우리는 이 기능을 사용하지 않을 것입니다. + +```c++ +depthStencil.stencilTestEnable = VK_FALSE; +depthStencil.front = {}; // Optional +depthStencil.back = {}; // Optional +``` + +마지막 세 필드는 스텐실 버퍼 작업을 구성하며, 이 튜토리얼에서는 사용하지 않습니다. 이 작업을 사용하려면 깊이/스텐실 이미지의 포맷이 스텐실 구성 요소를 포함하는지 확인해야 합니다. + +```c++ +pipelineInfo.pDepthStencilState = &depthStencil; +``` + +`VkGraphicsPipelineCreateInfo` 구조체를 업데이트하여 방금 채운 깊이 스텐실 상태를 참조하도록 합니다. 렌더 패스가 깊이 스텐실 첨부를 포함하는 경우, 깊이 스텐실 상태는 항상 지정되어야 합니다. + +이제 프로그램을 실행하면, 지오메트리의 프래그먼트가 올바르게 정렬된 것을 볼 수 있습니다. + +![](/images/depth_correct.png) + +## 창 크기 조절 처리 + +창 크기가 조절될 때, 깊이 버퍼의 해상도도 새 색상 첨부 해상도와 일치하도록 변경되어야 합니다. `recreateSwapChain` 함수를 확장하여 이 경우에 깊이 리소스를 재생성하도록 합니다. + +```c++ +void recreateSwapChain() { + int width = 0, height = 0; + glfwGetFramebufferSize(window, &width, &height); + while (width == 0 || height == 0) { + glfwGetFramebufferSize(window, &width, &height); + glfwWaitEvents(); + } + + vkDeviceWaitIdle(device); + + cleanupSwapChain(); + + createSwapChain(); + createImageViews(); + createDepthResources(); + createFramebuffers(); +} +``` + +정리 작업은 스왑 체인 정리 함수에서 이루어져야 합니다. + +```c++ +void cleanupSwapChain() { + vkDestroyImageView(device, depthImageView, nullptr); + vkDestroyImage(device, depthImage, nullptr); + vkFreeMemory(device, depthImageMemory, nullptr); + + ... +} +``` + +축하합니다, 이제 여러분의 애플리케이션은 임의의 3D 지오메트리를 렌더링하고 올바르게 보이게 할 준비가 되었습니다. 다음 장에서는 텍스처가 입혀진 모델을 그려보며 이를 시험해 보겠습니다! + +[C++ 코드](/code/27_depth_buffering.cpp) / +[정점 셰이더](/code/27_shader_depth.vert) / +[프래그먼트 셰이더](/code/27_shader_depth.frag) \ No newline at end of file diff --git a/ko/08_Loading_models.md b/ko/08_Loading_models.md new file mode 100644 index 00000000..b69255f4 --- /dev/null +++ b/ko/08_Loading_models.md @@ -0,0 +1,246 @@ +## 소개 + +이제 여러분의 프로그램은 텍스처가 입혀진 3D 메시를 렌더링할 준비가 되었습니다. 하지만 현재 `vertices`와 `indices` 배열에 있는 지오메트리는 아직 그다지 흥미롭지 않습니다. 이번 챕터에서는 그래픽 카드가 실제로 어떤 작업을 하도록 만들기 위해, 실제 모델 파일에서 정점과 인덱스를 로드하도록 프로그램을 확장할 것입니다. + +많은 그래픽 API 튜토리얼에서는 이와 같은 챕터에서 독자에게 직접 OBJ 로더를 작성하도록 합니다. 하지만 이 방식의 문제점은, 조금이라도 흥미로운 3D 애플리케이션이라면 곧 골격 애니메이션(skeletal animation)과 같이 OBJ 파일 형식이 지원하지 않는 기능이 필요해진다는 것입니다. 이번 챕터에서 OBJ 모델로부터 메시 데이터를 로드하긴 하겠지만, 파일에서 메시 데이터를 로드하는 세부 사항보다는, 메시 데이터를 프로그램 자체에 통합하는 데 더 중점을 둘 것입니다. + +## 라이브러리 + +정점과 면(face)을 OBJ 파일에서 로드하기 위해 [tinyobjloader](https://github.com/syoyo/tinyobjloader) 라이브러리를 사용할 것입니다. 이 라이브러리는 빠르고, `stb_image`처럼 단일 파일 라이브러리라서 통합하기 쉽습니다. 위 링크의 저장소로 가서 `tiny_obj_loader.h` 파일을 다운로드하여 여러분의 라이브러리 디렉터리 내의 폴더에 넣으세요. + +**Visual Studio** + +`tiny_obj_loader.h`가 있는 디렉터리를 `추가 포함 디렉터리(Additional Include Directories)` 경로에 추가하세요. + +![](/images/include_dirs_tinyobjloader.png) + +**Makefile** + +`tiny_obj_loader.h`가 있는 디렉터리를 GCC의 포함 디렉터리에 추가하세요: + +```text +VULKAN_SDK_PATH = /home/user/VulkanSDK/x.x.x.x/x86_64 +STB_INCLUDE_PATH = /home/user/libraries/stb +TINYOBJ_INCLUDE_PATH = /home/user/libraries/tinyobjloader + +... + +CFLAGS = -std=c++17 -I$(VULKAN_SDK_PATH)/include -I$(STB_INCLUDE_PATH) -I$(TINYOBJ_INCLUDE_PATH) +``` + +## 샘플 메시 + +이번 챕터에서는 아직 조명을 활성화하지 않을 것이므로, 텍스처에 조명이 미리 구워진(baked) 샘플 모델을 사용하는 것이 도움이 됩니다. 이러한 모델을 찾는 쉬운 방법은 [Sketchfab](https://sketchfab.com/)에서 3D 스캔 모델을 찾아보는 것입니다. 해당 사이트의 많은 모델이 허용적인 라이선스와 함께 OBJ 형식으로 제공됩니다. + +이 튜토리얼에서는 [nigelgoh](https://sketchfab.com/nigelgoh)의 [Viking room](https://sketchfab.com/3d-models/viking-room-a49f1b8e4f5c4ecf9e1fe7d81915ad38) 모델([CC BY 4.0](https://web.archive.org/web/20200428202538/https://sketchfab.com/3d-models/viking-room-a49f1b8e4f5c4ecf9e1fe7d81915ad38))을 사용하기로 결정했습니다. 현재 지오메트리를 바로 대체하여 사용할 수 있도록 모델의 크기와 방향을 조정했습니다: + +* [viking_room.obj](/resources/viking_room.obj) +* [viking_room.png](/resources/viking_room.png) + +자신만의 모델을 자유롭게 사용해도 되지만, 해당 모델이 단 하나의 재질(material)로만 구성되어 있고 크기가 약 1.5 x 1.5 x 1.5 단위인지 확인하세요. 이보다 크면 뷰 행렬을 변경해야 합니다. 모델 파일을 `shaders`와 `textures` 옆에 새로운 `models` 디렉터리를 만들어 넣고, 텍스처 이미지는 `textures` 디렉터리에 넣으세요. + +모델과 텍스처 경로를 정의하기 위해 프로그램에 두 개의 새로운 설정 변수를 추가하세요: + +```c++ +const uint32_t WIDTH = 800; +const uint32_t HEIGHT = 600; + +const std::string MODEL_PATH = "models/viking_room.obj"; +const std::string TEXTURE_PATH = "textures/viking_room.png"; +``` + +그리고 `createTextureImage`가 이 경로 변수를 사용하도록 업데이트하세요: + +```c++ +stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); +``` + +## 정점과 인덱스 로드하기 + +이제 모델 파일에서 정점과 인덱스를 로드할 것이므로, 전역 `vertices`와 `indices` 배열을 제거해야 합니다. 이들을 `const`가 아닌 컨테이너 클래스 멤버로 교체하세요: + +```c++ +std::vector vertices; +std::vector indices; +VkBuffer vertexBuffer; +VkDeviceMemory vertexBufferMemory; +``` + +정점의 개수가 65535개를 초과할 것이기 때문에, 인덱스의 타입을 `uint16_t`에서 `uint32_t`로 변경해야 합니다. `vkCmdBindIndexBuffer`의 파라미터도 변경하는 것을 잊지 마세요: + +```c++ +vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT32); +``` + +tinyobjloader 라이브러리는 STB 라이브러리와 같은 방식으로 포함됩니다. `tiny_obj_loader.h` 파일을 포함하고, 링커 오류를 피하기 위해 하나의 소스 파일에 `TINYOBJLOADER_IMPLEMENTATION`을 정의하여 함수 본문을 포함시켜야 합니다: + +```c++ +#define TINYOBJLOADER_IMPLEMENTATION +#include +``` + +이제 이 라이브러리를 사용하여 `vertices`와 `indices` 컨테이너를 메시의 정점 데이터로 채우는 `loadModel` 함수를 작성할 것입니다. 이 함수는 정점 및 인덱스 버퍼가 생성되기 전 어딘가에서 호출되어야 합니다: + +```c++ +void initVulkan() { + ... + loadModel(); + createVertexBuffer(); + createIndexBuffer(); + ... +} + +... + +void loadModel() { + +} +``` + +모델은 `tinyobj::LoadObj` 함수를 호출하여 라이브러리의 데이터 구조로 로드됩니다: + +```c++ +void loadModel() { + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string err; + + if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &err, MODEL_PATH.c_str())) { + throw std::runtime_error(err); + } +} +``` + +OBJ 파일은 위치, 법선(normal), 텍스처 좌표, 그리고 면(face)으로 구성됩니다. 면은 임의의 수의 정점으로 구성되며, 각 정점은 인덱스를 통해 위치, 법선 및/또는 텍스처 좌표를 참조합니다. 이를 통해 전체 정점뿐만 아니라 개별 속성도 재사용할 수 있습니다. + +`attrib` 컨테이너는 `attrib.vertices`, `attrib.normals`, `attrib.texcoords` 벡터에 모든 위치, 법선, 텍스처 좌표를 담고 있습니다. `shapes` 컨테이너는 모든 개별 객체와 그 면들을 포함합니다. 각 면은 정점 배열로 구성되며, 각 정점은 위치, 법선, 텍스처 좌표 속성의 인덱스를 포함합니다. OBJ 모델은 면마다 재질과 텍스처를 정의할 수도 있지만, 우리는 이들을 무시할 것입니다. + +`err` 문자열에는 파일 로딩 중 발생한 오류나, 재질 정의 누락과 같은 경고가 포함됩니다. 로딩은 `LoadObj` 함수가 `false`를 반환할 때만 실패한 것으로 간주합니다. 위에서 언급했듯이, OBJ 파일의 면은 실제로는 임의의 수의 정점을 가질 수 있지만, 우리 애플리케이션은 삼각형만 렌더링할 수 있습니다. 다행히도 `LoadObj`는 이러한 면을 자동으로 삼각형으로 변환하는 선택적 파라미터를 가지고 있으며, 이는 기본적으로 활성화되어 있습니다. + +파일의 모든 면을 단일 모델로 결합할 것이므로, 모든 shape를 순회하기만 하면 됩니다: + +```c++ +for (const auto& shape : shapes) { + +} +``` + +삼각화 기능 덕분에 이미 면당 3개의 정점이 보장되므로, 이제 바로 정점들을 순회하며 `vertices` 벡터에 직접 넣을 수 있습니다: + +```c++ +for (const auto& shape : shapes) { + for (const auto& index : shape.mesh.indices) { + Vertex vertex{}; + + vertices.push_back(vertex); + indices.push_back(indices.size()); + } +} +``` + +단순화를 위해, 지금은 모든 정점이 고유하다고 가정하므로 간단하게 자동 증가하는 인덱스를 사용합니다. `index` 변수는 `tinyobj::index_t` 타입으로, `vertex_index`, `normal_index`, `texcoord_index` 멤버를 포함합니다. 이 인덱스들을 사용하여 `attrib` 배열에서 실제 정점 속성을 찾아야 합니다: + +```c++ +vertex.pos = { + attrib.vertices[3 * index.vertex_index + 0], + attrib.vertices[3 * index.vertex_index + 1], + attrib.vertices[3 * index.vertex_index + 2] +}; + +vertex.texCoord = { + attrib.texcoords[2 * index.texcoord_index + 0], + attrib.texcoords[2 * index.texcoord_index + 1] +}; + +vertex.color = {1.0f, 1.0f, 1.0f}; +``` + +안타깝게도 `attrib.vertices` 배열은 `glm::vec3` 같은 것이 아니라 `float` 값의 배열이므로, 인덱스에 `3`을 곱해야 합니다. 마찬가지로, 항목당 두 개의 텍스처 좌표 성분이 있습니다. 오프셋 `0`, `1`, `2`는 X, Y, Z 성분에 접근하는 데 사용되며, 텍스처 좌표의 경우 U, V 성분에 접근하는 데 사용됩니다. + +이제 최적화를 활성화한 상태로 프로그램을 실행하세요 (예: Visual Studio에서는 `Release` 모드, GCC에서는 `-O3` 컴파일러 플래그 사용). 최적화가 없으면 모델 로딩이 매우 느려지므로 이 과정이 필요합니다. 다음과 같은 화면을 볼 수 있을 것입니다: + +![](/images/inverted_texture_coordinates.png) + +좋습니다, 지오메트리는 올바르게 보이지만 텍스처는 왜 저럴까요? OBJ 형식은 수직 좌표 `0`이 이미지의 하단을 의미하는 좌표계를 가정하지만, 우리는 이미지를 Vulkan에 업로드할 때 `0`이 상단을 의미하는 위에서 아래 방향으로 업로드했습니다. 텍스처 좌표의 수직 성분을 뒤집어서 이 문제를 해결하세요: + +```c++ +vertex.texCoord = { + attrib.texcoords[2 * index.texcoord_index + 0], + 1.0f - attrib.texcoords[2 * index.texcoord_index + 1] +}; +``` + +프로그램을 다시 실행하면 이제 올바른 결과를 볼 수 있을 것입니다: + +![](/images/drawing_model.png) + +모든 노력이 마침내 이런 데모로 결실을 맺기 시작했습니다! + +> 모델이 회전할 때 뒷면(벽의 뒷부분)이 다소 이상하게 보일 수 있습니다. 이는 정상이며, 모델이 원래 그쪽에서 보도록 설계되지 않았기 때문입니다. + +## 정점 중복 제거 + +불행히도 아직 인덱스 버퍼를 제대로 활용하고 있지 않습니다. `vertices` 벡터에는 많은 중복된 정점 데이터가 포함되어 있는데, 이는 많은 정점이 여러 삼각형에 포함되기 때문입니다. 고유한 정점만 유지하고, 이들이 나타날 때마다 인덱스 버퍼를 사용해 재사용해야 합니다. 이를 구현하는 간단한 방법은 `map`이나 `unordered_map`을 사용하여 고유한 정점과 각각의 인덱스를 추적하는 것입니다: + +```c++ +#include + +... + +std::unordered_map uniqueVertices{}; + +for (const auto& shape : shapes) { + for (const auto& index : shape.mesh.indices) { + Vertex vertex{}; + + ... + + if (uniqueVertices.count(vertex) == 0) { + uniqueVertices[vertex] = static_cast(vertices.size()); + vertices.push_back(vertex); + } + + indices.push_back(uniqueVertices[vertex]); + } +} +``` + +OBJ 파일에서 정점을 읽을 때마다, 정확히 동일한 위치와 텍스처 좌표를 가진 정점을 이전에 본 적이 있는지 확인합니다. 만약 본 적이 없다면, `vertices`에 추가하고 그 인덱스를 `uniqueVertices` 컨테이너에 저장합니다. 그 후 새 정점의 인덱스를 `indices`에 추가합니다. 만약 이전에 정확히 동일한 정점을 본 적이 있다면, `uniqueVertices`에서 그 인덱스를 찾아 `indices`에 저장합니다. + +사용자 정의 타입인 `Vertex` 구조체를 해시 테이블의 키로 사용하려면 두 가지 함수, 즉 동등성 검사와 해시 계산을 구현해야 하므로, 지금은 프로그램이 컴파일되지 않을 것입니다. 전자는 `Vertex` 구조체에서 `==` 연산자를 오버라이드하여 쉽게 구현할 수 있습니다: + +```c++ +bool operator==(const Vertex& other) const { + return pos == other.pos && color == other.color && texCoord == other.texCoord; +} +``` + +`Vertex`에 대한 해시 함수는 `std::hash`에 대한 템플릿 특수화를 지정하여 구현합니다. 해시 함수는 복잡한 주제이지만, [cppreference.com은](http://en.cppreference.com/w/cpp/utility/hash) 구조체의 필드들을 결합하여 괜찮은 품질의 해시 함수를 만드는 다음 접근 방식을 권장합니다: + +```c++ +namespace std { + template<> struct hash { + size_t operator()(Vertex const& vertex) const { + return ((hash()(vertex.pos) ^ + (hash()(vertex.color) << 1)) >> 1) ^ + (hash()(vertex.texCoord) << 1); + } + }; +} +``` + +이 코드는 `Vertex` 구조체 외부에 위치해야 합니다. GLM 타입에 대한 해시 함수는 다음 헤더를 사용하여 포함해야 합니다: + +```c++ +#define GLM_ENABLE_EXPERIMENTAL +#include +``` + +해시 함수는 `gtx` 폴더에 정의되어 있는데, 이는 기술적으로는 아직 GLM의 실험적인 확장 기능이라는 것을 의미합니다. 따라서 이를 사용하려면 `GLM_ENABLE_EXPERIMENTAL`을 정의해야 합니다. 이는 향후 GLM의 새 버전에서 API가 변경될 수 있음을 의미하지만, 실제로는 API가 매우 안정적입니다. + +이제 프로그램을 성공적으로 컴파일하고 실행할 수 있을 것입니다. `vertices`의 크기를 확인해보면 1,500,000개에서 265,645개로 줄어든 것을 확인할 수 있습니다! 이는 각 정점이 평균적으로 약 6개의 삼각형에서 재사용된다는 것을 의미합니다. 이로써 확실히 많은 GPU 메모리를 절약할 수 있습니다. + +[C++ 코드](/code/28_model_loading.cpp) / +[정점 셰이더](/code/27_shader_depth.vert) / +[프래그먼트 셰이더](/code/27_shader_depth.frag) \ No newline at end of file diff --git a/ko/09_Generating_Mipmaps.md b/ko/09_Generating_Mipmaps.md new file mode 100644 index 00000000..681db5bf --- /dev/null +++ b/ko/09_Generating_Mipmaps.md @@ -0,0 +1,352 @@ +## 서론 +이제 우리 프로그램은 3D 모델을 로드하고 렌더링할 수 있습니다. 이번 장에서는 밉맵 생성이라는 기능을 하나 더 추가할 것입니다. 밉맵은 게임과 렌더링 소프트웨어에서 널리 사용되며, Vulkan은 밉맵 생성 방법을 완벽하게 제어할 수 있도록 해줍니다. + +밉맵은 미리 계산된, 축소된 버전의 이미지입니다. 각각의 새 이미지는 이전 이미지의 너비와 높이가 절반입니다. 밉맵은 *디테일 수준(Level of Detail, LOD)*의 한 형태로 사용됩니다. 카메라에서 멀리 떨어진 객체는 더 작은 밉 이미지에서 텍스처를 샘플링합니다. 더 작은 이미지를 사용하면 렌더링 속도가 향상되고 [모아레 패턴](https://ko.wikipedia.org/wiki/%EB%AC%B4%EC%95%84%EB%A0%88_%EB%AC%B4%EB%8A%AC)과 같은 아티팩트를 방지할 수 있습니다. 밉맵이 어떻게 생겼는지 보여주는 예시는 다음과 같습니다: + +![](/images/mipmaps_example.jpg) + +## 이미지 생성 + +Vulkan에서 각 밉 이미지는 `VkImage`의 서로 다른 *밉 레벨(mip level)*에 저장됩니다. 밉 레벨 0은 원본 이미지이며, 레벨 0 이후의 밉 레벨들은 흔히 *밉 체인(mip chain)*이라고 불립니다. + +밉 레벨의 수는 `VkImage`를 생성할 때 지정됩니다. 지금까지 우리는 항상 이 값을 1로 설정했습니다. 이제 이미지의 크기로부터 밉 레벨의 수를 계산해야 합니다. 먼저, 이 수를 저장할 클래스 멤버를 추가합니다: + +```c++ +... +uint32_t mipLevels; +VkImage textureImage; +... +``` + +`mipLevels`의 값은 `createTextureImage`에서 텍스처를 로드한 후에 찾을 수 있습니다: + +```c++ +int texWidth, texHeight, texChannels; +stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); +... +mipLevels = static_cast(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1; +``` + +이 코드는 밉 체인의 레벨 수를 계산합니다. `max` 함수는 가장 큰 차원(너비 또는 높이)을 선택합니다. `log2` 함수는 해당 차원을 2로 몇 번 나눌 수 있는지 계산합니다. `floor` 함수는 가장 큰 차원이 2의 거듭제곱이 아닌 경우를 처리합니다. `1`을 더해서 원본 이미지 자체도 밉 레벨을 갖도록 합니다. + +이 값을 사용하려면 `createImage`, `createImageView`, `transitionImageLayout` 함수를 수정하여 밉 레벨 수를 지정할 수 있도록 해야 합니다. 함수들에 `mipLevels` 매개변수를 추가하세요: + +```c++ +void createImage(uint32_t width, uint32_t height, uint32_t mipLevels, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) { + ... + imageInfo.mipLevels = mipLevels; + ... +} +``` +```c++ +VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags, uint32_t mipLevels) { + ... + viewInfo.subresourceRange.levelCount = mipLevels; + ... +} +``` +```c++ +void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout, uint32_t mipLevels) { + ... + barrier.subresourceRange.levelCount = mipLevels; + ... +} +``` + +이 함수들에 대한 모든 호출을 올바른 값을 사용하도록 업데이트합니다: + +```c++ +createImage(swapChainExtent.width, swapChainExtent.height, 1, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory); +... +createImage(texWidth, texHeight, mipLevels, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory); +``` +```c++ +swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat, VK_IMAGE_ASPECT_COLOR_BIT, 1); +... +depthImageView = createImageView(depthImage, depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT, 1); +... +textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_ASPECT_COLOR_BIT, mipLevels); +``` +```c++ +transitionImageLayout(depthImage, depthFormat, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, 1); +... +transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels); +``` + +## 밉맵 생성하기 + +이제 우리의 텍스처 이미지는 여러 밉 레벨을 가지지만, 스테이징 버퍼는 밉 레벨 0을 채우는 데만 사용될 수 있습니다. 다른 레벨들은 여전히 정의되지 않은 상태입니다. 이 레벨들을 채우려면 우리가 가진 단일 레벨로부터 데이터를 생성해야 합니다. 이를 위해 `vkCmdBlitImage` 명령을 사용할 것입니다. 이 명령은 복사, 스케일링, 필터링 연산을 수행합니다. 우리는 이 명령을 여러 번 호출하여 우리 텍스처 이미지의 각 레벨로 데이터를 *블릿(blit)*할 것입니다. + +`vkCmdBlitImage`는 전송 작업으로 간주되므로, 텍스처 이미지를 전송의 소스(source)와 대상(destination)으로 모두 사용할 것임을 Vulkan에 알려야 합니다. `createTextureImage`에서 텍스처 이미지의 사용 플래그에 `VK_IMAGE_USAGE_TRANSFER_SRC_BIT`를 추가합니다: + +```c++ +... +createImage(texWidth, texHeight, mipLevels, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory); +... +``` + +다른 이미지 작업과 마찬가지로, `vkCmdBlitImage`는 작동하는 이미지의 레이아웃에 의존합니다. 전체 이미지를 `VK_IMAGE_LAYOUT_GENERAL`로 전환할 수도 있지만, 이는 매우 느릴 가능성이 높습니다. 최적의 성능을 위해서는 소스 이미지는 `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`에, 대상 이미지는 `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`에 있어야 합니다. Vulkan은 이미지의 각 밉 레벨을 독립적으로 전환할 수 있도록 허용합니다. 각 블릿은 한 번에 두 개의 밉 레벨만 다루므로, 블릿 명령 사이에 각 레벨을 최적의 레이아웃으로 전환할 수 있습니다. + +`transitionImageLayout`은 전체 이미지에 대해서만 레이아웃 전환을 수행하므로, 파이프라인 배리어 명령을 몇 개 더 작성해야 합니다. `createTextureImage`에서 `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`로의 기존 전환을 제거합니다: + +```c++ +... +transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels); + copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast(texHeight)); +// 밉맵을 생성하는 동안 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL로 전환됨 +... +``` + +이렇게 하면 텍스처 이미지의 각 레벨이 `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL` 상태로 남게 됩니다. 각 레벨은 해당 레벨에서 읽는 블릿 명령이 완료된 후 `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`로 전환될 것입니다. + +이제 밉맵을 생성하는 함수를 작성해 보겠습니다: + +```c++ +void generateMipmaps(VkImage image, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) { + VkCommandBuffer commandBuffer = beginSingleTimeCommands(); + + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.image = image; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.subresourceRange.levelCount = 1; + + endSingleTimeCommands(commandBuffer); +} +``` + +여러 번의 전환을 수행할 것이므로 이 `VkImageMemoryBarrier`를 재사용할 것입니다. 위에서 설정된 필드들은 모든 배리어에 대해 동일하게 유지됩니다. `subresourceRange.miplevel`, `oldLayout`, `newLayout`, `srcAccessMask`, `dstAccessMask`는 각 전환마다 변경될 것입니다. + +```c++ +int32_t mipWidth = texWidth; +int32_t mipHeight = texHeight; + +for (uint32_t i = 1; i < mipLevels; i++) { + +} +``` + +이 루프는 각 `VkCmdBlitImage` 명령을 기록합니다. 루프 변수가 0이 아닌 1에서 시작하는 점에 유의하세요. + +```c++ +barrier.subresourceRange.baseMipLevel = i - 1; +barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; +barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; +barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; +barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + +vkCmdPipelineBarrier(commandBuffer, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &barrier); +``` + +먼저, `i - 1` 레벨을 `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`로 전환합니다. 이 전환은 이전 블릿 명령이나 `vkCmdCopyBufferToImage`로부터 `i - 1` 레벨이 채워질 때까지 기다립니다. 현재 블릿 명령은 이 전환을 기다리게 됩니다. + +```c++ +VkImageBlit blit{}; +blit.srcOffsets[0] = { 0, 0, 0 }; +blit.srcOffsets[1] = { mipWidth, mipHeight, 1 }; +blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +blit.srcSubresource.mipLevel = i - 1; +blit.srcSubresource.baseArrayLayer = 0; +blit.srcSubresource.layerCount = 1; +blit.dstOffsets[0] = { 0, 0, 0 }; +blit.dstOffsets[1] = { mipWidth > 1 ? mipWidth / 2 : 1, mipHeight > 1 ? mipHeight / 2 : 1, 1 }; +blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +blit.dstSubresource.mipLevel = i; +blit.dstSubresource.baseArrayLayer = 0; +blit.dstSubresource.layerCount = 1; +``` + +다음으로, 블릿 작업에 사용될 영역을 지정합니다. 소스 밉 레벨은 `i - 1`이고 대상 밉 레벨은 `i`입니다. `srcOffsets` 배열의 두 요소는 데이터가 블릿될 3D 영역을 결정합니다. `dstOffsets`는 데이터가 블릿될 영역을 결정합니다. 각 밉 레벨은 이전 레벨 크기의 절반이므로 `dstOffsets[1]`의 X와 Y 차원은 2로 나눕니다. 2D 이미지는 깊이가 1이므로 `srcOffsets[1]`과 `dstOffsets[1]`의 Z 차원은 1이어야 합니다. + +```c++ +vkCmdBlitImage(commandBuffer, + image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, &blit, + VK_FILTER_LINEAR); +``` + +이제 블릿 명령을 기록합니다. `srcImage`와 `dstImage` 매개변수 모두에 `textureImage`가 사용되는 점에 유의하세요. 이는 동일한 이미지의 서로 다른 레벨 간에 블리팅을 수행하기 때문입니다. 소스 밉 레벨은 방금 `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`로 전환되었고, 대상 레벨은 `createTextureImage`에서부터 `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL` 상태로 남아있습니다. + +([정점 버퍼](!kr/Vertex_buffers/Staging_buffer)에서 제안된 것처럼) 전용 전송 큐를 사용하고 있다면 주의하세요: `vkCmdBlitImage`는 그래픽스 기능이 있는 큐에 제출되어야 합니다. + +마지막 매개변수는 블릿에 사용할 `VkFilter`를 지정할 수 있게 해줍니다. 여기서는 `VkSampler`를 만들 때와 동일한 필터링 옵션을 가집니다. 우리는 보간을 활성화하기 위해 `VK_FILTER_LINEAR`를 사용합니다. + +```c++ +barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; +barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; +barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; +barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + +vkCmdPipelineBarrier(commandBuffer, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &barrier); +``` + +이 배리어는 밉 레벨 `i - 1`을 `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`로 전환합니다. 이 전환은 현재 블릿 명령이 완료되기를 기다립니다. 모든 샘플링 작업은 이 전환이 완료되기를 기다릴 것입니다. + +```c++ + ... + if (mipWidth > 1) mipWidth /= 2; + if (mipHeight > 1) mipHeight /= 2; +} +``` + +루프의 끝에서 현재 밉 차원을 2로 나눕니다. 각 차원이 0이 되지 않도록 나누기 전에 확인합니다. 이는 이미지가 정사각형이 아닐 경우를 처리하는데, 한쪽 밉 차원이 다른 쪽보다 먼저 1에 도달하기 때문입니다. 이런 경우, 해당 차원은 나머지 모든 레벨에 대해 1로 유지되어야 합니다. + +```c++ + barrier.subresourceRange.baseMipLevel = mipLevels - 1; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + vkCmdPipelineBarrier(commandBuffer, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &barrier); + + endSingleTimeCommands(commandBuffer); +} +``` + +커맨드 버퍼를 종료하기 전에, 파이프라인 배리어를 하나 더 삽입합니다. 이 배리어는 마지막 밉 레벨을 `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`에서 `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`로 전환합니다. 마지막 밉 레벨은 블릿의 소스로 사용되지 않기 때문에 루프에서 처리되지 않았습니다. + +마지막으로, `createTextureImage`에서 `generateMipmaps`를 호출합니다: + +```c++ +transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels); + copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast(texHeight)); +// 밉맵을 생성하는 동안 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL로 전환됨 +... +generateMipmaps(textureImage, texWidth, texHeight, mipLevels); +``` + +이제 우리 텍스처 이미지의 밉맵이 완전히 채워졌습니다. + +## 선형 필터링 지원 + +`vkCmdBlitImage`와 같은 내장 함수를 사용하여 모든 밉 레벨을 생성하는 것은 매우 편리하지만, 불행히도 모든 플랫폼에서 지원된다고 보장되지는 않습니다. 이를 위해서는 우리가 사용하는 텍스처 이미지 형식이 선형 필터링을 지원해야 하며, 이는 `vkGetPhysicalDeviceFormatProperties` 함수로 확인할 수 있습니다. 이를 위해 `generateMipmaps` 함수에 확인 코드를 추가할 것입니다. + +먼저 이미지 형식을 지정하는 추가 매개변수를 추가합니다: + +```c++ +void createTextureImage() { + ... + + generateMipmaps(textureImage, VK_FORMAT_R8G8B8A8_SRGB, texWidth, texHeight, mipLevels); +} + +void generateMipmaps(VkImage image, VkFormat imageFormat, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) { + + ... +} +``` + +`generateMipmaps` 함수에서 `vkGetPhysicalDeviceFormatProperties`를 사용하여 텍스처 이미지 형식의 속성을 요청합니다: + +```c++ +void generateMipmaps(VkImage image, VkFormat imageFormat, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) { + + // 이미지 형식이 선형 블리팅을 지원하는지 확인 + VkFormatProperties formatProperties; + vkGetPhysicalDeviceFormatProperties(physicalDevice, imageFormat, &formatProperties); + + ... +``` + +`VkFormatProperties` 구조체에는 `linearTilingFeatures`, `optimalTilingFeatures`, `bufferFeatures`라는 세 개의 필드가 있으며, 각각 형식이 사용되는 방식에 따라 어떻게 사용될 수 있는지를 설명합니다. 우리는 최적(optimal) 타일링 형식으로 텍스처 이미지를 생성하므로, `optimalTilingFeatures`를 확인해야 합니다. 선형 필터링 기능 지원은 `VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT`로 확인할 수 있습니다: + +```c++ +if (!(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT)) { + throw std::runtime_error("texture image format does not support linear blitting!"); +} +``` + +이 경우 두 가지 대안이 있습니다. 선형 블리팅을 *지원하는* 일반적인 텍스처 이미지 형식을 검색하는 함수를 구현하거나, [stb_image_resize](https://github.com/nothings/stb/blob/master/stb_image_resize.h)와 같은 라이브러리를 사용하여 소프트웨어에서 밉맵 생성을 구현할 수 있습니다. 그런 다음 각 밉 레벨을 원본 이미지를 로드했던 것과 같은 방식으로 이미지에 로드할 수 있습니다. + +실무에서는 런타임에 밉맵 레벨을 생성하는 것이 일반적이지 않다는 점을 알아두는 것이 좋습니다. 보통은 로딩 속도를 향상시키기 위해 미리 생성하여 기본 레벨과 함께 텍스처 파일에 저장합니다. 소프트웨어에서 리사이징을 구현하고 파일에서 여러 레벨을 로드하는 것은 독자의 연습 과제로 남겨두겠습니다. + +## 샘플러 + +`VkImage`가 밉맵 데이터를 보유하는 동안, `VkSampler`는 렌더링 중에 해당 데이터를 읽는 방법을 제어합니다. Vulkan은 `minLod`, `maxLod`, `mipLodBias`, `mipmapMode`를 지정할 수 있게 해줍니다("Lod"는 "Level of Detail"을 의미합니다). 텍스처가 샘플링될 때, 샘플러는 다음 의사 코드에 따라 밉 레벨을 선택합니다: + +```c++ +lod = getLodLevelFromScreenSize(); // 객체가 가까우면 작아지고, 음수일 수 있음 +lod = clamp(lod + mipLodBias, minLod, maxLod); + +level = clamp(floor(lod), 0, texture.mipLevels - 1); // 텍스처의 밉 레벨 수로 클램핑됨 + +if (mipmapMode == VK_SAMPLER_MIPMAP_MODE_NEAREST) { + color = sample(level); +} else { + color = blend(sample(level), sample(level + 1)); +} +``` + +`samplerInfo.mipmapMode`가 `VK_SAMPLER_MIPMAP_MODE_NEAREST`이면 `lod`는 샘플링할 밉 레벨을 선택합니다. 밉맵 모드가 `VK_SAMPLER_MIPMAP_MODE_LINEAR`이면, `lod`는 샘플링할 두 개의 밉 레벨을 선택하는 데 사용됩니다. 해당 레벨들이 샘플링되고 결과는 선형으로 블렌딩됩니다. + +샘플 작업은 `lod`의 영향도 받습니다: + +```c++ +if (lod <= 0) { + color = readTexture(uv, magFilter); +} else { + color = readTexture(uv, minFilter); +} +``` + +객체가 카메라에 가까우면 `magFilter`가 필터로 사용됩니다. 객체가 카메라에서 더 멀리 있으면 `minFilter`가 사용됩니다. 보통 `lod`는 음수가 아니며, 카메라에 가까울 때만 0입니다. `mipLodBias`를 사용하면 Vulkan이 평소보다 낮은 `lod`와 `level`을 사용하도록 강제할 수 있습니다. + +이 장의 결과를 보려면 `textureSampler`에 대한 값을 선택해야 합니다. 우리는 이미 `minFilter`와 `magFilter`를 `VK_FILTER_LINEAR`를 사용하도록 설정했습니다. 이제 `minLod`, `maxLod`, `mipLodBias`, `mipmapMode`에 대한 값을 선택하기만 하면 됩니다. + +```c++ +void createTextureSampler() { + ... + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + samplerInfo.minLod = 0.0f; // 선택 사항 + samplerInfo.maxLod = VK_LOD_CLAMP_NONE; + samplerInfo.mipLodBias = 0.0f; // 선택 사항 + ... +} +``` + +전체 범위의 밉 레벨을 사용할 수 있도록 `minLod`를 0.0f로 설정하고, `maxLod`는 `VK_LOD_CLAMP_NONE`으로 설정합니다. 이 상수는 `1000.0f`와 같으며, 이는 텍스처에서 사용 가능한 모든 밉맵 레벨을 샘플링한다는 것을 의미합니다. `lod` 값을 변경할 이유가 없으므로 `mipLodBias`를 0.0f로 설정합니다. + +이제 프로그램을 실행하면 다음과 같은 화면을 볼 수 있습니다: + +![](/images/mipmaps.png) + +장면이 매우 단순하기 때문에 극적인 차이는 없습니다. 자세히 보면 미묘한 차이가 있습니다. + +![](/images/mipmaps_comparison.png) + +가장 눈에 띄는 차이점은 종이에 적힌 글씨입니다. 밉맵을 사용하면 글씨가 부드럽게 처리되었습니다. 밉맵이 없으면 글씨는 모아레 아티팩트로 인해 거친 가장자리와 끊김이 있습니다. + +샘플러 설정을 바꿔보면서 밉매핑에 어떤 영향을 미치는지 시험해 볼 수 있습니다. 예를 들어 `minLod`를 변경하여 샘플러가 가장 낮은 밉 레벨을 사용하지 않도록 강제할 수 있습니다: + +```c++ +samplerInfo.minLod = static_cast(mipLevels / 2); +``` + +이 설정은 다음과 같은 이미지를 생성합니다: + + +![](/images/highmipmaps.png) + +이것이 객체가 카메라에서 더 멀리 있을 때 더 높은 밉 레벨이 사용되는 방식입니다. + +[C++ 코드](/code/29_mipmapping.cpp) / +[정점 셰이더](/code/27_shader_depth.vert) / +[프래그먼트 셰이더](/code/27_shader_depth.frag) \ No newline at end of file diff --git a/ko/10_Multisampling.md b/ko/10_Multisampling.md new file mode 100644 index 00000000..1b94c5e4 --- /dev/null +++ b/ko/10_Multisampling.md @@ -0,0 +1,292 @@ +## 소개 + +이제 우리 프로그램은 텍스처에 대해 여러 디테일 수준(Level of Detail, LOD)을 로드할 수 있게 되어, 뷰어로부터 멀리 떨어진 객체를 렌더링할 때 발생하던 아티팩트(artifact)를 수정합니다. 이미지는 이제 훨씬 부드러워졌지만, 자세히 살펴보면 그려진 기하학적 모양의 가장자리를 따라 들쭉날쭉한 톱니 모양의 패턴을 발견할 수 있습니다. 이는 초기에 사각형 하나를 렌더링했던 프로그램에서 특히 두드러지게 나타납니다. + +![](/images/texcoord_visualization.png) + +이러한 바람직하지 않은 효과를 "앨리어싱(aliasing)"이라고 하며, 이는 렌더링에 사용할 수 있는 픽셀 수가 제한적이기 때문에 발생하는 결과입니다. 무한한 해상도를 가진 디스플레이는 없으므로, 이 현상은 어느 정도 항상 보일 수밖에 없습니다. 이를 해결하는 여러 방법이 있으며, 이 장에서는 가장 널리 사용되는 방법 중 하나인 [멀티샘플 안티-앨리어싱(Multisample anti-aliasing, MSAA)](https://en.wikipedia.org/wiki/Multisample_anti-aliasing)에 초점을 맞출 것입니다. + +일반적인 렌더링에서 픽셀 색상은 단일 샘플 포인트(대부분 화면의 대상 픽셀 중앙)를 기준으로 결정됩니다. 만약 그려진 선의 일부가 특정 픽셀을 통과하지만 샘플 포인트를 덮지 않으면, 그 픽셀은 비어 있게 되어 들쭉날쭉한 "계단 현상"이 발생합니다. + +![](/images/aliasing.png) + +MSAA는 픽셀당 여러 개의 샘플 포인트(이름에서 알 수 있듯이)를 사용하여 최종 색상을 결정합니다. 예상할 수 있듯이, 샘플 수가 많을수록 결과는 좋아지지만, 연산 비용도 더 많이 듭니다. + +![](/images/antialiasing.png) + +우리의 구현에서는 사용 가능한 최대 샘플 수를 사용하는 데 중점을 둘 것입니다. 여러분의 애플리케이션에 따라 이것이 항상 최선의 접근 방식은 아닐 수 있으며, 최종 결과가 품질 요구 사항을 충족한다면 더 높은 성능을 위해 더 적은 샘플을 사용하는 것이 더 나을 수도 있습니다. + +## 사용 가능한 샘플 수 얻기 + +먼저 우리 하드웨어가 사용할 수 있는 샘플 수를 결정하는 것부터 시작하겠습니다. 대부분의 최신 GPU는 최소 8개의 샘플을 지원하지만, 이 숫자가 모든 곳에서 동일하다고 보장할 수는 없습니다. 새로운 클래스 멤버를 추가하여 이 값을 추적하겠습니다. + +```c++ +... +VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT; +... +``` + +기본적으로 픽셀당 하나의 샘플만 사용할 것이며, 이는 멀티샘플링을 사용하지 않는 것과 같습니다. 이 경우 최종 이미지는 변경되지 않습니다. 정확한 최대 샘플 수는 선택된 물리 디바이스와 연관된 `VkPhysicalDeviceProperties`에서 추출할 수 있습니다. 우리는 깊이 버퍼를 사용하므로, 컬러와 깊이 버퍼 모두에 대한 샘플 수를 고려해야 합니다. 두 버퍼 모두에서 지원되는(&) 가장 높은 샘플 수가 우리가 지원할 수 있는 최대치가 됩니다. 이 정보를 가져올 함수를 추가합시다. + +```c++ +VkSampleCountFlagBits getMaxUsableSampleCount() { + VkPhysicalDeviceProperties physicalDeviceProperties; + vkGetPhysicalDeviceProperties(physicalDevice, &physicalDeviceProperties); + + VkSampleCountFlags counts = physicalDeviceProperties.limits.framebufferColorSampleCounts & physicalDeviceProperties.limits.framebufferDepthSampleCounts; + if (counts & VK_SAMPLE_COUNT_64_BIT) { return VK_SAMPLE_COUNT_64_BIT; } + if (counts & VK_SAMPLE_COUNT_32_BIT) { return VK_SAMPLE_COUNT_32_BIT; } + if (counts & VK_SAMPLE_COUNT_16_BIT) { return VK_SAMPLE_COUNT_16_BIT; } + if (counts & VK_SAMPLE_COUNT_8_BIT) { return VK_SAMPLE_COUNT_8_BIT; } + if (counts & VK_SAMPLE_COUNT_4_BIT) { return VK_SAMPLE_COUNT_4_BIT; } + if (counts & VK_SAMPLE_COUNT_2_BIT) { return VK_SAMPLE_COUNT_2_BIT; } + + return VK_SAMPLE_COUNT_1_BIT; +} +``` + +이제 이 함수를 사용하여 물리 디바이스 선택 과정에서 `msaaSamples` 변수를 설정할 것입니다. 이를 위해 `pickPhysicalDevice` 함수를 약간 수정해야 합니다. + +```c++ +void pickPhysicalDevice() { + ... + for (const auto& device : devices) { + if (isDeviceSuitable(device)) { + physicalDevice = device; + msaaSamples = getMaxUsableSampleCount(); + break; + } + } + ... +} +``` + +## 렌더 타겟 설정하기 + +MSAA에서는 각 픽셀이 오프스크린 버퍼에 샘플링된 후 화면에 렌더링됩니다. 이 새로운 버퍼는 우리가 지금까지 렌더링해왔던 일반 이미지와는 약간 다릅니다. 픽셀당 하나 이상의 샘플을 저장할 수 있어야 합니다. 멀티샘플링된 버퍼가 생성되면, 기본 프레임버퍼(픽셀당 단일 샘플만 저장)로 리졸브(resolve)되어야 합니다. 이 때문에 추가적인 렌더 타겟을 생성하고 현재의 그리기 프로세스를 수정해야 합니다. 깊이 버퍼와 마찬가지로 한 번에 하나의 그리기 작업만 활성화되므로 렌더 타겟은 하나만 필요합니다. 다음 클래스 멤버를 추가합시다. + +```c++ +... +VkImage colorImage; +VkDeviceMemory colorImageMemory; +VkImageView colorImageView; +... +``` + +이 새로운 이미지는 픽셀당 원하는 수의 샘플을 저장해야 하므로, 이미지 생성 과정에서 `VkImageCreateInfo`에 이 숫자를 전달해야 합니다. `createImage` 함수에 `numSamples` 매개변수를 추가하여 수정합시다. + +```c++ +void createImage(uint32_t width, uint32_t height, uint32_t mipLevels, VkSampleCountFlagBits numSamples, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) { + ... + imageInfo.samples = numSamples; + ... +} +``` + +이제 구현을 진행하면서 적절한 값으로 대체할 것이므로, 지금은 이 함수에 대한 모든 호출을 `VK_SAMPLE_COUNT_1_BIT`를 사용하여 업데이트합니다. + +```c++ +createImage(swapChainExtent.width, swapChainExtent.height, 1, VK_SAMPLE_COUNT_1_BIT, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory); +... +createImage(texWidth, texHeight, mipLevels, VK_SAMPLE_COUNT_1_BIT, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory); +``` + +이제 멀티샘플링된 컬러 버퍼를 생성하겠습니다. `createColorResources` 함수를 추가하고, 여기서 `msaaSamples`를 `createImage` 함수의 매개변수로 사용하고 있음을 주목하세요. 밉 레벨은 하나만 사용하는데, 이는 픽셀당 샘플이 하나 이상인 이미지의 경우 벌칸 명세에 의해 강제되기 때문입니다. 또한, 이 컬러 버퍼는 텍스처로 사용되지 않을 것이므로 밉맵이 필요 없습니다. + +```c++ +void createColorResources() { + VkFormat colorFormat = swapChainImageFormat; + + createImage(swapChainExtent.width, swapChainExtent.height, 1, msaaSamples, colorFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, colorImage, colorImageMemory); + colorImageView = createImageView(colorImage, colorFormat, VK_IMAGE_ASPECT_COLOR_BIT, 1); +} +``` + +일관성을 위해, 이 함수를 `createDepthResources` 바로 앞에서 호출합니다. + +```c++ +void initVulkan() { + ... + createColorResources(); + createDepthResources(); + ... +} +``` + +이제 멀티샘플링된 컬러 버퍼가 준비되었으니, 깊이 버퍼를 처리할 차례입니다. `createDepthResources`를 수정하고 깊이 버퍼에서 사용하는 샘플 수를 업데이트하세요. + +```c++ +void createDepthResources() { + ... + createImage(swapChainExtent.width, swapChainExtent.height, 1, msaaSamples, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory); + ... +} +``` + +이제 몇 가지 새로운 벌칸 리소스를 생성했으므로, 필요할 때 이를 해제하는 것을 잊지 말아야 합니다. + +```c++ +void cleanupSwapChain() { + vkDestroyImageView(device, colorImageView, nullptr); + vkDestroyImage(device, colorImage, nullptr); + vkFreeMemory(device, colorImageMemory, nullptr); + ... +} +``` + +그리고 `recreateSwapChain`을 업데이트하여 창 크기가 조절될 때 새로운 컬러 이미지가 올바른 해상도로 다시 생성될 수 있도록 합니다. + +```c++ +void recreateSwapChain() { + ... + createImageViews(); + createColorResources(); + createDepthResources(); + ... +} +``` + +초기 MSAA 설정을 마쳤습니다. 이제 이 새로운 리소스를 그래픽 파이프라인, 프레임버퍼, 렌더 패스에서 사용하고 결과를 확인해야 합니다! + +## 새로운 어태치먼트 추가하기 + +먼저 렌더 패스부터 처리합시다. `createRenderPass`를 수정하여 컬러 및 깊이 어태치먼트 생성 정보 구조체를 업데이트하세요. + +```c++ +void createRenderPass() { + ... + colorAttachment.samples = msaaSamples; + colorAttachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + ... + depthAttachment.samples = msaaSamples; + ... +} +``` + +`finalLayout`을 `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`에서 `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`로 변경한 것을 눈치채셨을 겁니다. 이는 멀티샘플링된 이미지는 직접 화면에 표시(present)할 수 없기 때문입니다. 먼저 일반 이미지로 리졸브해야 합니다. 이 요구사항은 깊이 버퍼에는 적용되지 않는데, 깊이 버퍼는 어떤 시점에도 화면에 표시되지 않기 때문입니다. 따라서 우리는 소위 리졸브 어태치먼트(resolve attachment)라고 불리는, 컬러를 위한 새로운 어태치먼트 하나만 추가하면 됩니다. + +```c++ + ... + VkAttachmentDescription colorAttachmentResolve{}; + colorAttachmentResolve.format = swapChainImageFormat; + colorAttachmentResolve.samples = VK_SAMPLE_COUNT_1_BIT; + colorAttachmentResolve.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + colorAttachmentResolve.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + colorAttachmentResolve.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + colorAttachmentResolve.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + colorAttachmentResolve.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + colorAttachmentResolve.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + ... +``` + +이제 렌더 패스는 멀티샘플링된 컬러 이미지를 일반 어태치먼트로 리졸브하도록 지시받아야 합니다. 리졸브 타겟이 될 컬러 버퍼를 가리킬 새로운 어태치먼트 참조를 생성합니다. + +```c++ + ... + VkAttachmentReference colorAttachmentResolveRef{}; + colorAttachmentResolveRef.attachment = 2; + colorAttachmentResolveRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + ... +``` + +서브패스 구조체의 `pResolveAttachments` 멤버가 새로 생성된 어태치먼트 참조를 가리키도록 설정합니다. 이것만으로도 렌더 패스가 멀티샘플 리졸브 작업을 정의하게 되어, 이미지를 화면에 렌더링할 수 있게 됩니다. + +``` + ... + subpass.pResolveAttachments = &colorAttachmentResolveRef; + ... +``` + +멀티샘플링된 컬러 이미지를 재사용하므로, `VkSubpassDependency`의 `srcAccessMask`를 업데이트해야 합니다. 이 업데이트는 컬러 어태치먼트에 대한 쓰기 작업이 후속 작업 시작 전에 완료되도록 보장하여, 불안정한 렌더링 결과를 초래할 수 있는 쓰기 후 쓰기(write-after-write) 위험을 방지합니다. + +```c++ + ... + dependency.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + ... +``` + +이제 렌더 패스 정보 구조체를 새로운 컬러 어태치먼트로 업데이트합니다. + +```c++ + ... + std::array attachments = {colorAttachment, depthAttachment, colorAttachmentResolve}; + ... +``` + +렌더 패스가 준비되었으니, `createFramebuffers`를 수정하고 새로운 이미지 뷰를 목록에 추가합니다. + +```c++ +void createFramebuffers() { + ... + std::array attachments = { + colorImageView, + depthImageView, + swapChainImageViews[i] + }; + ... +} +``` + +마지막으로, `createGraphicsPipeline`을 수정하여 새로 생성된 파이프라인이 하나 이상의 샘플을 사용하도록 지시합니다. + +```c++ +void createGraphicsPipeline() { + ... + multisampling.rasterizationSamples = msaaSamples; + ... +} +``` + +이제 프로그램을 실행하면 다음과 같은 화면을 볼 수 있습니다. + +![](/images/multisampling.png) + +밉매핑과 마찬가지로, 차이가 즉시 눈에 띄지 않을 수 있습니다. 자세히 살펴보면 가장자리가 예전만큼 들쭉날쭉하지 않고 전체 이미지가 원본에 비해 약간 더 부드러워진 것을 알 수 있습니다. + +![](/images/multisampling_comparison.png) + +가장자리 중 하나를 가까이에서 보면 차이가 더 두드러집니다. + +![](/images/multisampling_comparison2.png) + +## 품질 개선 + +현재 MSAA 구현에는 몇 가지 한계가 있어 더 디테일한 장면에서 출력 이미지의 품질에 영향을 미칠 수 있습니다. 예를 들어, 현재 우리는 셰이더 앨리어싱으로 인해 발생할 수 있는 잠재적인 문제를 해결하고 있지 않습니다. 즉, MSAA는 지오메트리의 가장자리만 부드럽게 처리할 뿐 내부 채우기는 처리하지 않습니다. 이로 인해 화면에 부드러운 폴리곤이 렌더링되더라도, 적용된 텍스처에 대비가 강한 색상이 포함되어 있다면 여전히 앨리어싱이 발생한 것처럼 보일 수 있습니다. 이 문제를 해결하는 한 가지 방법은 [샘플 셰이딩(Sample Shading)](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap27.html#primsrast-sampleshading)을 활성화하는 것입니다. 이는 추가적인 성능 비용을 수반하지만 이미지 품질을 더욱 향상시킬 수 있습니다. + +```c++ +void createLogicalDevice() { + ... + deviceFeatures.sampleRateShading = VK_TRUE; // 디바이스에 샘플 셰이딩 기능 활성화 + ... +} + +void createGraphicsPipeline() { + ... + multisampling.sampleShadingEnable = VK_TRUE; // 파이프라인에서 샘플 셰이딩 활성화 + multisampling.minSampleShading = .2f; // 샘플 셰이딩을 위한 최소 비율; 1에 가까울수록 부드러워짐 + ... +} +``` + +이 예제에서는 샘플 셰이딩을 비활성화 상태로 두겠지만, 특정 시나리오에서는 품질 향상이 눈에 띄게 나타날 수 있습니다. + +![](/images/sample_shading.png) + +## 결론 + +여기까지 오기까지 많은 노력이 필요했지만, 이제 여러분은 마침내 훌륭한 벌칸 프로그램의 기반을 갖추게 되었습니다. 여러분이 지금 가진 벌칸의 기본 원리에 대한 지식은 다음과 같은 더 많은 기능을 탐색하기에 충분할 것입니다. + +* 푸시 상수(Push constants) +* 인스턴스 렌더링(Instanced rendering) +* 동적 유니폼(Dynamic uniforms) +* 이미지와 샘플러 디스크립터 분리 +* 파이프라인 캐시 +* 다중 스레드 커맨드 버퍼 생성 +* 다중 서브패스 +* 컴퓨트 셰이더 + +현재 프로그램은 블린-퐁(Blinn-Phong) 조명, 후처리 효과, 그림자 매핑 등을 추가하는 등 다양한 방식으로 확장될 수 있습니다. 벌칸의 명시적인 특성에도 불구하고 많은 개념이 여전히 동일하게 작동하기 때문에, 다른 API의 튜토리얼을 통해 이러한 효과들이 어떻게 작동하는지 배울 수 있을 것입니다. + +[C++ 코드](/code/30_multisampling.cpp) / +[정점 셰이더](/code/27_shader_depth.vert) / +[프래그먼트 셰이더](/code/27_shader_depth.frag) \ No newline at end of file diff --git a/ko/11_Compute_Shader.md b/ko/11_Compute_Shader.md new file mode 100644 index 00000000..5ae5e311 --- /dev/null +++ b/ko/11_Compute_Shader.md @@ -0,0 +1,651 @@ +## 소개 + +이 보너스 챕터에서는 컴퓨트 셰이더(compute shader)에 대해 살펴보겠습니다. 지금까지의 모든 챕터는 Vulkan 파이프라인의 전통적인 그래픽스 부분을 다루었습니다. 하지만 OpenGL과 같은 오래된 API와 달리, Vulkan에서 컴퓨트 셰이더 지원은 필수입니다. 이는 고사양 데스크톱 GPU든 저전력 임베디드 장치든, 사용 가능한 모든 Vulkan 구현에서 컴퓨트 셰이더를 사용할 수 있다는 의미입니다. + +이는 여러분의 애플리케이션이 어디서 실행되든 상관없이 GPU(그래픽 처리 장치)를 이용한 범용 컴퓨팅(GPGPU, general purpose computing on graphics processor units)의 세계를 열어줍니다. GPGPU는 전통적으로 CPU의 영역이었던 일반적인 계산을 GPU에서 수행할 수 있음을 의미합니다. GPU가 점점 더 강력해지고 유연해짐에 따라, CPU의 범용적인 능력이 필요했던 많은 작업들을 이제 GPU에서 실시간으로 처리할 수 있게 되었습니다. + +GPU의 컴퓨팅 능력이 사용될 수 있는 몇 가지 예로는 이미지 처리, 가시성 테스트, 후처리(post processing), 고급 조명 계산, 애니메이션, 물리(예: 파티클 시스템) 등이 있으며, 이 외에도 훨씬 더 많습니다. 심지어 수치 연산이나 AI 관련 작업처럼 그래픽 출력이 전혀 필요 없는 비시각적 계산 전용 작업에도 컴퓨트를 사용할 수 있습니다. 이를 "헤드리스 컴퓨트(headless compute)"라고 합니다. + +## 장점 + +계산 비용이 많이 드는 작업을 GPU에서 수행하면 몇 가지 장점이 있습니다. 가장 명백한 것은 CPU의 작업을 덜어내는 것입니다. 또 다른 장점은 CPU의 주 메모리와 GPU 메모리 간에 데이터를 옮길 필요가 없다는 점입니다. 모든 데이터는 주 메모리로부터의 느린 전송을 기다릴 필요 없이 GPU에 머무를 수 있습니다. + +이 외에도 GPU는 수만 개의 작은 연산 유닛으로 고도로 병렬화되어 있습니다. 이 때문에 몇 개의 큰 연산 유닛을 가진 CPU보다 고도로 병렬화된 워크플로우에 더 적합한 경우가 많습니다. + +## Vulkan 파이프라인 + +컴퓨트는 파이프라인의 그래픽스 부분과 완전히 분리되어 있다는 점을 알아두는 것이 중요합니다. 이는 공식 명세서의 다음 Vulkan 파이프라인 블록 다이어그램에서 확인할 수 있습니다. + +![](/images/vulkan_pipeline_block_diagram.png) + +이 다이어그램의 왼쪽에는 전통적인 그래픽스 파이프라인 부분이 있고, 오른쪽에는 컴퓨트 셰이더 단계를 포함하여 이 그래픽스 파이프라인에 속하지 않는 여러 단계들이 있습니다. 컴퓨트 셰이더 단계가 그래픽스 파이프라인에서 분리되어 있으므로, 우리는 필요하다고 생각되는 어느 곳에서든 이를 사용할 수 있습니다. 이는 항상 정점 셰이더의 변환된 출력에 적용되는 프래그먼트 셰이더와는 매우 다릅니다. + +다이어그램 중앙은 디스크립터 셋(descriptor set)과 같은 요소들이 컴퓨트에서도 사용된다는 것을 보여주므로, 우리가 디스크립터 레이아웃, 디스크립터 셋, 디스크립터에 대해 배운 모든 것이 여기에도 적용됩니다. + +## 예제 + +이 챕터에서 구현할 이해하기 쉬운 예제는 GPU 기반 파티클 시스템입니다. 이러한 시스템은 많은 게임에서 사용되며, 종종 상호작용 가능한 프레임 속도로 업데이트되어야 하는 수천 개의 파티클로 구성됩니다. 이러한 시스템을 렌더링하려면 두 가지 주요 구성 요소가 필요합니다: 정점 버퍼로 전달되는 정점들과, 어떤 방정식에 기반하여 이들을 업데이트하는 방법입니다. + +"전통적인" CPU 기반 파티클 시스템은 파티클 데이터를 시스템의 주 메모리에 저장한 다음 CPU를 사용하여 업데이트합니다. 업데이트 후에는 다음 프레임에서 업데이트된 파티클을 표시할 수 있도록 정점 데이터를 다시 GPU 메모리로 전송해야 합니다. 가장 간단한 방법은 매 프레임마다 새로운 데이터로 정점 버퍼를 다시 생성하는 것입니다. 이는 명백히 비용이 많이 듭니다. 구현에 따라, CPU가 쓸 수 있도록 GPU 메모리를 매핑하거나("데스크톱 시스템에서는 resizable BAR", 통합 GPU에서는 통합 메모리라고 함), 호스트 로컬 버퍼를 사용하는(PCI-E 대역폭 때문에 가장 느린 방법) 등의 다른 옵션이 있습니다. 하지만 어떤 버퍼 업데이트 방법을 선택하든, 파티클을 업데이트하기 위해 항상 CPU를 거쳐야 하는 과정이 필요합니다. + +GPU 기반 파티클 시스템을 사용하면 이러한 과정이 더 이상 필요하지 않습니다. 정점 데이터는 처음에만 GPU에 업로드되며, 모든 업데이트는 컴퓨트 셰이더를 사용하여 GPU 메모리 내에서 이루어집니다. 이것이 더 빠른 주된 이유 중 하나는 GPU와 로컬 메모리 간의 훨씬 높은 대역폭 때문입니다. CPU 기반 시나리오에서는 주 메모리와 PCI-Express 대역폭에 의해 제한되는데, 이는 종종 GPU 메모리 대역폭의 일부에 불과합니다. + +전용 컴퓨트 큐가 있는 GPU에서 이 작업을 수행하면 그래픽스 파이프라인의 렌더링 부분과 병렬로 파티클을 업데이트할 수 있습니다. 이를 "비동기 컴퓨트(async compute)"라고 하며, 이 튜토리얼에서는 다루지 않는 고급 주제입니다. + +다음은 이 챕터 코드의 스크린샷입니다. 여기에 보이는 파티클은 CPU 상호작용 없이 GPU에서 직접 컴퓨트 셰이더에 의해 업데이트됩니다. + +![](/images/compute_shader_particles.png) + +## 데이터 조작 + +이 튜토리얼에서 우리는 프리미티브를 전달하기 위한 정점 및 인덱스 버퍼, 셰이더에 데이터를 전달하기 위한 유니폼 버퍼와 같은 다양한 버퍼 유형에 대해 이미 배웠습니다. 그리고 텍스처 매핑을 위해 이미지를 사용하기도 했습니다. 하지만 지금까지는 항상 CPU를 사용하여 데이터를 쓰고 GPU에서는 읽기만 했습니다. + +컴퓨트 셰이더와 함께 도입된 중요한 개념은 버퍼에 **읽고 쓰는 것**을 자유롭게 할 수 있다는 점입니다. 이를 위해 Vulkan은 두 가지 전용 저장소 유형을 제공합니다. + +### 셰이더 저장 버퍼 객체 (SSBO) + +셰이더 저장 버퍼(SSBO, Shader Storage Buffer Object)는 셰이더가 버퍼에서 읽고 쓸 수 있게 해줍니다. 이를 사용하는 것은 유니폼 버퍼 객체를 사용하는 것과 유사합니다. 가장 큰 차이점은 다른 버퍼 유형을 SSBO로 사용할 수 있으며, 크기에 제한이 없다는 것입니다. + +GPU 기반 파티클 시스템으로 돌아가서, 컴퓨트 셰이더에 의해 업데이트(쓰기)되고 정점 셰이더에 의해 읽히는(그리기) 정점을 어떻게 처리해야 할지 궁금할 수 있습니다. 두 사용 사례가 서로 다른 버퍼 유형을 필요로 하는 것처럼 보이기 때문입니다. + +하지만 그렇지 않습니다. Vulkan에서는 버퍼와 이미지에 대해 여러 사용 용도를 지정할 수 있습니다. 따라서 파티클 정점 버퍼를 정점 버퍼(그래픽스 패스에서)와 저장 버퍼(컴퓨트 패스에서)로 사용하려면, 해당 두 사용 플래그로 버퍼를 생성하기만 하면 됩니다. + +```c++ +VkBufferCreateInfo bufferInfo{}; +... +bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; +... + +if (vkCreateBuffer(device, &bufferInfo, nullptr, &shaderStorageBuffers[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create vertex buffer!"); +} +``` +`bufferInfo.usage`에 설정된 `VK_BUFFER_USAGE_VERTEX_BUFFER_BIT`와 `VK_BUFFER_USAGE_STORAGE_BUFFER_BIT` 두 플래그는 구현에 이 버퍼를 두 가지 다른 시나리오, 즉 정점 셰이더의 정점 버퍼와 저장 버퍼로 사용하고 싶다는 것을 알립니다. 또한 호스트에서 GPU로 데이터를 전송할 수 있도록 `VK_BUFFER_USAGE_TRANSFER_DST_BIT` 플래그도 추가했습니다. 셰이더 저장 버퍼를 GPU 메모리에만 유지하기를 원하므로(`VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT`), 호스트에서 이 버퍼로 데이터를 전송해야 하기 때문에 이는 매우 중요합니다. + +다음은 `createBuffer` 헬퍼 함수를 사용한 동일한 코드입니다. + +```c++ +createBuffer(bufferSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, shaderStorageBuffers[i], shaderStorageBuffersMemory[i]); +``` + +이러한 버퍼에 접근하기 위한 GLSL 셰이더 선언은 다음과 같습니다. + +```glsl +struct Particle { + vec2 position; + vec2 velocity; + vec4 color; +}; + +layout(std140, binding = 1) readonly buffer ParticleSSBOIn { + Particle particlesIn[ ]; +}; + +layout(std140, binding = 2) buffer ParticleSSBOOut { + Particle particlesOut[ ]; +}; +``` + +이 예제에는 각 파티클이 위치와 속도 값을 가지는 타입이 지정된 SSBO가 있습니다(`Particle` 구조체 참조). 그리고 SSBO는 `[]`로 표시된 것처럼 바인딩되지 않은 수의 파티클을 포함합니다. SSBO에서 요소의 수를 지정할 필요가 없다는 것은 유니폼 버퍼 등에 비해 장점 중 하나입니다. `std140`은 셰이더 저장 버퍼의 멤버 요소가 메모리에서 어떻게 정렬되는지를 결정하는 메모리 레이아웃 한정자입니다. 이는 호스트와 GPU 간에 버퍼를 매핑하는 데 필요한 특정 보장을 제공합니다. + +컴퓨트 셰이더에서 이러한 저장 버퍼 객체에 쓰는 것은 간단하며, C++ 측에서 버퍼에 쓰는 방식과 유사합니다. + +```glsl +particlesOut[index].position = particlesIn[index].position + particlesIn[index].velocity.xy * ubo.deltaTime; +``` + +### 저장 이미지 + +*이 챕터에서는 이미지 조작을 다루지 않습니다. 이 단락은 독자들에게 컴퓨트 셰이더가 이미지 조작에도 사용될 수 있음을 알리기 위해 존재합니다.* + +저장 이미지(storage image)는 이미지에서 읽고 쓸 수 있게 해줍니다. 일반적인 사용 사례는 텍스처에 이미지 효과를 적용하거나, 후처리를 하거나(매우 유사함), 밉맵을 생성하는 것입니다. + +이미지의 경우도 비슷합니다. + +```c++ +VkImageCreateInfo imageInfo {}; +... +imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT; +... + +if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) { + throw std::runtime_error("failed to create image!"); +} +``` + +`imageInfo.usage`에 설정된 `VK_IMAGE_USAGE_SAMPLED_BIT`와 `VK_IMAGE_USAGE_STORAGE_BIT` 두 플래그는 이 이미지를 두 가지 다른 시나리오, 즉 프래그먼트 셰이더에서 샘플링되는 이미지와 컴퓨트 셰이더의 저장 이미지로 사용하고 싶다는 것을 구현에 알립니다. + +저장 이미지에 대한 GLSL 셰이더 선언은 프래그먼트 셰이더 등에서 사용되는 샘플링된 이미지와 유사합니다. + +```glsl +layout (binding = 0, rgba8) uniform readonly image2D inputImage; +layout (binding = 1, rgba8) uniform writeonly image2D outputImage; +``` + +여기서 몇 가지 차이점은 이미지의 형식을 위한 `rgba8`과 같은 추가 속성, 입력 이미지에서는 읽기만 하고 출력 이미지에는 쓰기만 할 것임을 구현에 알리는 `readonly` 및 `writeonly` 한정자입니다. 그리고 마지막으로 저장 이미지를 선언하기 위해 `image2D` 타입을 사용해야 합니다. + +컴퓨트 셰이더에서 저장 이미지에 읽고 쓰는 것은 `imageLoad`와 `imageStore`를 사용하여 수행됩니다. + +```glsl +vec3 pixel = imageLoad(inputImage, ivec2(gl_GlobalInvocationID.xy)).rgb; +imageStore(outputImage, ivec2(gl_GlobalInvocationID.xy), pixel); +``` + +## 컴퓨트 큐 패밀리 + +[물리 장치 및 큐 패밀리 챕터](03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md#page_Queue-families)에서 우리는 이미 큐 패밀리와 그래픽스 큐 패밀리를 선택하는 방법에 대해 배웠습니다. 컴퓨트는 큐 패밀리 속성 플래그 비트 `VK_QUEUE_COMPUTE_BIT`를 사용합니다. 따라서 컴퓨트 작업을 하려면 컴퓨트를 지원하는 큐 패밀리에서 큐를 가져와야 합니다. + +Vulkan은 그래픽스 작업을 지원하는 구현이 그래픽스와 컴퓨트 작업을 모두 지원하는 큐 패밀리를 최소 하나 이상 가지도록 요구하지만, 구현이 전용 컴퓨트 큐를 제공할 수도 있다는 점에 유의해야 합니다. 이 전용 컴퓨트 큐(그래픽스 비트가 없는)는 비동기 컴퓨트 큐를 암시합니다. 하지만 이 튜토리얼은 초심자에게 친숙하도록 그래픽스와 컴퓨트 작업을 모두 할 수 있는 큐를 사용할 것입니다. 이는 또한 여러 고급 동기화 메커니즘을 다루는 것을 피하게 해줍니다. + +컴퓨트 샘플을 위해 장치 생성 코드를 약간 변경해야 합니다. + +```c++ +uint32_t queueFamilyCount = 0; +vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr); + +std::vector queueFamilies(queueFamilyCount); +vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data()); + +int i = 0; +for (const auto& queueFamily : queueFamilies) { + if ((queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) && (queueFamily.queueFlags & VK_QUEUE_COMPUTE_BIT)) { + indices.graphicsAndComputeFamily = i; + } + + i++; +} +``` + +변경된 큐 패밀리 인덱스 선택 코드는 이제 그래픽스와 컴퓨트를 모두 지원하는 큐 패밀리를 찾으려고 시도할 것입니다. + +그런 다음 `createLogicalDevice`에서 이 큐 패밀리로부터 컴퓨트 큐를 가져올 수 있습니다. + +```c++ +vkGetDeviceQueue(device, indices.graphicsAndComputeFamily.value(), 0, &computeQueue); +``` + +## 컴퓨트 셰이더 단계 + +그래픽스 샘플에서는 셰이더를 로드하고 디스크립터에 접근하기 위해 다른 파이프라인 단계를 사용했습니다. 컴퓨트 셰이더는 `VK_SHADER_STAGE_COMPUTE_BIT` 파이프라인을 사용하여 비슷한 방식으로 접근됩니다. 따라서 컴퓨트 셰이더를 로드하는 것은 정점 셰이더를 로드하는 것과 동일하지만 셰이더 단계가 다릅니다. 이에 대해서는 다음 단락에서 자세히 다룰 것입니다. 컴퓨트는 또한 나중에 사용해야 할 `VK_PIPELINE_BIND_POINT_COMPUTE`라는 새로운 디스크립터 및 파이프라인 바인딩 포인트 유형을 도입합니다. + +## 컴퓨트 셰이더 로드하기 + +애플리케이션에서 컴퓨트 셰이더를 로드하는 것은 다른 셰이더를 로드하는 것과 동일합니다. 유일한 실제 차이점은 위에서 언급한 `VK_SHADER_STAGE_COMPUTE_BIT`를 사용해야 한다는 것입니다. + +```c++ +auto computeShaderCode = readFile("shaders/compute.spv"); + +VkShaderModule computeShaderModule = createShaderModule(computeShaderCode); + +VkPipelineShaderStageCreateInfo computeShaderStageInfo{}; +computeShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; +computeShaderStageInfo.stage = VK_SHADER_STAGE_COMPUTE_BIT; +computeShaderStageInfo.module = computeShaderModule; +computeShaderStageInfo.pName = "main"; +... +``` + +## 셰이더 저장 버퍼 준비하기 + +앞서 우리는 임의의 데이터를 컴퓨트 셰이더에 전달하기 위해 셰이더 저장 버퍼를 사용할 수 있다고 배웠습니다. 이 예제에서는 파티클 배열을 GPU에 업로드하여 GPU 메모리에서 직접 조작할 수 있도록 할 것입니다. + +[Frames in flight](03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md) 챕터에서 우리는 CPU와 GPU를 계속 바쁘게 유지하기 위해 프레임별로 리소스를 복제하는 것에 대해 이야기했습니다. 먼저 버퍼 객체와 이를 지원하는 장치 메모리를 위한 벡터를 선언합니다. + +```c++ +std::vector shaderStorageBuffers; +std::vector shaderStorageBuffersMemory; +``` + +`createShaderStorageBuffers`에서 이 벡터들의 크기를 최대 프레임 수에 맞게 조정합니다. + +```c++ +shaderStorageBuffers.resize(MAX_FRAMES_IN_FLIGHT); +shaderStorageBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT); +``` + +이 설정이 완료되면 초기 파티클 정보를 GPU로 옮기기 시작할 수 있습니다. 먼저 호스트 측에서 파티클 벡터를 초기화합니다. + +```c++ + // 파티클 초기화 + std::default_random_engine rndEngine((unsigned)time(nullptr)); + std::uniform_real_distribution rndDist(0.0f, 1.0f); + + // 원 위에 초기 파티클 위치 지정 + std::vector particles(PARTICLE_COUNT); + for (auto& particle : particles) { + float r = 0.25f * sqrt(rndDist(rndEngine)); + float theta = rndDist(rndEngine) * 2 * 3.14159265358979323846; + float x = r * cos(theta) * HEIGHT / WIDTH; + float y = r * sin(theta); + particle.position = glm::vec2(x, y); + particle.velocity = glm::normalize(glm::vec2(x,y)) * 0.00025f; + particle.color = glm::vec4(rndDist(rndEngine), rndDist(rndEngine), rndDist(rndEngine), 1.0f); + } + +``` + +그런 다음 초기 파티클 속성을 담을 호스트 메모리에 [스테이징 버퍼](04_Vertex_buffers/02_Staging_buffer.md)를 생성합니다. + +```c++ + VkDeviceSize bufferSize = sizeof(Particle) * PARTICLE_COUNT; + + VkBuffer stagingBuffer; + VkDeviceMemory stagingBufferMemory; + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); + + void* data; + vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data); + memcpy(data, particles.data(), (size_t)bufferSize); + vkUnmapMemory(device, stagingBufferMemory); +``` + +이 스테이징 버퍼를 소스로 사용하여 프레임별 셰이더 저장 버퍼를 생성하고 파티클 속성을 스테이징 버퍼에서 각각으로 복사합니다. + +```c++ + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + createBuffer(bufferSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, shaderStorageBuffers[i], shaderStorageBuffersMemory[i]); + // 스테이징 버퍼(호스트)에서 셰이더 저장 버퍼(GPU)로 데이터 복사 + copyBuffer(stagingBuffer, shaderStorageBuffers[i], bufferSize); + } +} +``` + +## 디스크립터 + +컴퓨트를 위한 디스크립터 설정은 그래픽스와 거의 동일합니다. 유일한 차이점은 디스크립터가 컴퓨트 단계에서 접근할 수 있도록 `VK_SHADER_STAGE_COMPUTE_BIT`가 설정되어야 한다는 것입니다. + +```c++ +std::array layoutBindings{}; +layoutBindings[0].binding = 0; +layoutBindings[0].descriptorCount = 1; +layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +layoutBindings[0].pImmutableSamplers = nullptr; +layoutBindings[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; +... +``` + +여기서 셰이더 단계를 결합할 수 있습니다. 예를 들어, 정점 및 컴퓨트 단계에서 접근 가능한 디스크립터(예: 이들 간에 공유되는 매개변수가 있는 유니폼 버퍼)를 원한다면, 두 단계에 대한 비트를 모두 설정하면 됩니다. + +```c++ +layoutBindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_COMPUTE_BIT; +``` + +다음은 우리 샘플의 디스크립터 설정입니다. 레이아웃은 다음과 같습니다. + +```c++ +std::array layoutBindings{}; +layoutBindings[0].binding = 0; +layoutBindings[0].descriptorCount = 1; +layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +layoutBindings[0].pImmutableSamplers = nullptr; +layoutBindings[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + +layoutBindings[1].binding = 1; +layoutBindings[1].descriptorCount = 1; +layoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; +layoutBindings[1].pImmutableSamplers = nullptr; +layoutBindings[1].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + +layoutBindings[2].binding = 2; +layoutBindings[2].descriptorCount = 1; +layoutBindings[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; +layoutBindings[2].pImmutableSamplers = nullptr; +layoutBindings[2].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + +VkDescriptorSetLayoutCreateInfo layoutInfo{}; +layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; +layoutInfo.bindingCount = 3; +layoutInfo.pBindings = layoutBindings.data(); + +if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &computeDescriptorSetLayout) != VK_SUCCESS) { + throw std::runtime_error("failed to create compute descriptor set layout!"); +} +``` + +이 설정을 보면, 단일 파티클 시스템만 렌더링하는데도 왜 셰이더 저장 버퍼 객체에 대한 레이아웃 바인딩이 두 개나 있는지 궁금할 수 있습니다. 이는 파티클 위치가 델타 타임에 따라 프레임별로 업데이트되기 때문입니다. 즉, 각 프레임은 이전 프레임의 파티클 위치를 알아야 새로운 델타 타임으로 업데이트하고 자신의 SSBO에 기록할 수 있습니다. + +![](/images/compute_ssbo_read_write.svg) + +이를 위해 컴퓨트 셰이더는 이전 프레임과 현재 프레임의 SSBO에 접근해야 합니다. 이는 디스크립터 설정에서 두 SSBO를 모두 컴퓨트 셰이더에 전달함으로써 이루어집니다. `storageBufferInfoLastFrame`과 `storageBufferInfoCurrentFrame`을 확인하세요. + +```c++ +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + VkDescriptorBufferInfo uniformBufferInfo{}; + uniformBufferInfo.buffer = uniformBuffers[i]; + uniformBufferInfo.offset = 0; + uniformBufferInfo.range = sizeof(UniformBufferObject); + + std::array descriptorWrites{}; + ... + + VkDescriptorBufferInfo storageBufferInfoLastFrame{}; + // (i - 1) % MAX_FRAMES_IN_FLIGHT를 통해 이전 프레임의 버퍼에 접근 + storageBufferInfoLastFrame.buffer = shaderStorageBuffers[(i - 1 + MAX_FRAMES_IN_FLIGHT) % MAX_FRAMES_IN_FLIGHT]; + storageBufferInfoLastFrame.offset = 0; + storageBufferInfoLastFrame.range = sizeof(Particle) * PARTICLE_COUNT; + + descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[1].dstSet = computeDescriptorSets[i]; + descriptorWrites[1].dstBinding = 1; + descriptorWrites[1].dstArrayElement = 0; + descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[1].descriptorCount = 1; + descriptorWrites[1].pBufferInfo = &storageBufferInfoLastFrame; + + VkDescriptorBufferInfo storageBufferInfoCurrentFrame{}; + storageBufferInfoCurrentFrame.buffer = shaderStorageBuffers[i]; + storageBufferInfoCurrentFrame.offset = 0; + storageBufferInfoCurrentFrame.range = sizeof(Particle) * PARTICLE_COUNT; + + descriptorWrites[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[2].dstSet = computeDescriptorSets[i]; + descriptorWrites[2].dstBinding = 2; + descriptorWrites[2].dstArrayElement = 0; + descriptorWrites[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[2].descriptorCount = 1; + descriptorWrites[2].pBufferInfo = &storageBufferInfoCurrentFrame; + + vkUpdateDescriptorSets(device, 3, descriptorWrites.data(), 0, nullptr); +} +``` + +우리의 디스크립터 풀에서 SSBO에 대한 디스크립터 유형을 요청해야 한다는 것을 기억하세요. + +```c++ +std::array poolSizes{}; +... + +poolSizes[1].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; +poolSizes[1].descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT) * 2; +``` + +우리의 셋이 이전 프레임과 현재 프레임의 SSBO를 참조하기 때문에 풀에서 요청하는 `VK_DESCRIPTOR_TYPE_STORAGE_BUFFER` 유형의 수를 두 배로 늘려야 합니다. + +## 컴퓨트 파이프라인 + +컴퓨트는 그래픽스 파이프라인의 일부가 아니므로 `vkCreateGraphicsPipelines`를 사용할 수 없습니다. 대신, 컴퓨트 명령을 실행하기 위해 `vkCreateComputePipelines`로 전용 컴퓨트 파이프라인을 생성해야 합니다. 컴퓨트 파이프라인은 래스터화 상태를 건드리지 않기 때문에 그래픽스 파이프라인보다 상태가 훨씬 적습니다. + +```c++ +VkComputePipelineCreateInfo pipelineInfo{}; +pipelineInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; +pipelineInfo.layout = computePipelineLayout; +pipelineInfo.stage = computeShaderStageInfo; + +if (vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &computePipeline) != VK_SUCCESS) { + throw std::runtime_error("failed to create compute pipeline!"); +} +``` + +하나의 셰이더 단계와 파이프라인 레이아웃만 필요하므로 설정이 훨씬 간단합니다. 파이프라인 레이아웃은 그래픽스 파이프라인과 동일하게 작동합니다. + +```c++ +VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; +pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; +pipelineLayoutInfo.setLayoutCount = 1; +pipelineLayoutInfo.pSetLayouts = &computeDescriptorSetLayout; + +if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &computePipelineLayout) != VK_SUCCESS) { + throw std::runtime_error("failed to create compute pipeline layout!"); +} +``` + +## 컴퓨트 공간 + +컴퓨트 셰이더가 어떻게 작동하고 GPU에 컴퓨트 워크로드를 제출하는지 알아보기 전에, 두 가지 중요한 컴퓨트 개념인 **워크 그룹(work groups)**과 **인보케이션(invocations)**에 대해 이야기해야 합니다. 이들은 컴퓨트 워크로드가 GPU의 컴퓨트 하드웨어에 의해 3차원(x, y, z)으로 어떻게 처리되는지에 대한 추상적인 실행 모델을 정의합니다. + +**워크 그룹**은 컴퓨트 워크로드가 GPU의 컴퓨트 하드웨어에 의해 어떻게 형성되고 처리되는지를 정의합니다. GPU가 처리해야 할 작업 항목이라고 생각할 수 있습니다. 워크 그룹의 차원은 애플리케이션에서 커맨드 버퍼 기록 시 디스패치 명령을 사용하여 설정됩니다. + +그리고 각 워크 그룹은 동일한 컴퓨트 셰이더를 실행하는 **인보케이션**의 모음입니다. 인보케이션은 잠재적으로 병렬로 실행될 수 있으며, 그 차원은 컴퓨트 셰이더에서 설정됩니다. 단일 워크 그룹 내의 인보케이션들은 공유 메모리에 접근할 수 있습니다. + +이 이미지는 이 둘의 관계를 3차원으로 보여줍니다. + +![](/images/compute_space.svg) + +워크 그룹(`vkCmdDispatch`로 정의)과 인보케이션(컴퓨트 셰이더의 로컬 크기로 정의)의 차원 수는 입력 데이터가 어떻게 구조화되어 있는지에 따라 달라집니다. 예를 들어 이 챕터에서처럼 1차원 배열 작업을 하는 경우, 둘 다에 대해 x 차원만 지정하면 됩니다. + +예를 들어, 워크 그룹 수를 [64, 1, 1]로 디스패치하고 컴퓨트 셰이더의 로컬 크기를 [32, 32, 1]로 설정하면, 컴퓨트 셰이더는 64 x 32 x 32 = 65,536번 호출됩니다. + +워크 그룹과 로컬 크기의 최대 개수는 구현마다 다르므로, 항상 `VkPhysicalDeviceLimits`의 컴퓨트 관련 `maxComputeWorkGroupCount`, `maxComputeWorkGroupInvocations`, `maxComputeWorkGroupSize` 제한을 확인해야 합니다. + +## 컴퓨트 셰이더 + +이제 컴퓨트 셰이더 파이프라인을 설정하는 데 필요한 모든 부분을 배웠으니, 컴퓨트 셰이더 자체를 살펴볼 차례입니다. 정점 및 프래그먼트 셰이더와 같이 GLSL 셰이더를 사용하는 것에 대해 배운 모든 것이 컴퓨트 셰이더에도 적용됩니다. 문법은 동일하며, 애플리케이션과 셰이더 간의 데이터 전달과 같은 많은 개념도 동일합니다. 하지만 몇 가지 중요한 차이점이 있습니다. + +선형 파티클 배열을 업데이트하기 위한 아주 기본적인 컴퓨트 셰이더는 다음과 같습니다. + +```glsl +#version 450 + +layout (binding = 0) uniform ParameterUBO { + float deltaTime; +} ubo; + +struct Particle { + vec2 position; + vec2 velocity; + vec4 color; +}; + +layout(std140, binding = 1) readonly buffer ParticleSSBOIn { + Particle particlesIn[ ]; +}; + +layout(std140, binding = 2) buffer ParticleSSBOOut { + Particle particlesOut[ ]; +}; + +layout (local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +void main() +{ + uint index = gl_GlobalInvocationID.x; + + Particle particleIn = particlesIn[index]; + + particlesOut[index].position = particleIn.position + particleIn.velocity.xy * ubo.deltaTime; + particlesOut[index].velocity = particleIn.velocity; + ... +} +``` + +셰이더의 상단 부분은 셰이더 입력을 위한 선언을 포함합니다. 첫 번째는 바인딩 0에 있는 유니폼 버퍼 객체로, 이 튜토리얼에서 이미 배운 것입니다. 그 아래에는 C++ 코드의 선언과 일치하는 `Particle` 구조체를 선언합니다. 바인딩 1은 이전 프레임의 파티클 데이터가 있는 셰이더 저장 버퍼 객체(디스크립터 설정 참조)를, 바인딩 2는 현재 프레임의 SSBO를 가리키며, 이 셰이더로 업데이트할 대상입니다. + +흥미로운 점은 컴퓨트 공간과 관련된 이 컴퓨트 전용 선언입니다. + +```glsl +layout (local_size_x = 256, local_size_y = 1, local_size_z = 1) in; +``` +이는 현재 워크 그룹에서 이 컴퓨트 셰이더의 인보케이션 수를 정의합니다. 앞서 언급했듯이, 이것은 컴퓨트 공간의 로컬 부분입니다. 그래서 접두사로 `local_`이 붙습니다. 우리는 선형 1D 파티클 배열에서 작업하므로, `local_size_x`에 x 차원에 대한 숫자만 지정하면 됩니다. + +`main` 함수는 이전 프레임의 SSBO에서 읽고 업데이트된 파티클 위치를 현재 프레임의 SSBO에 씁니다. 다른 셰이더 유형과 마찬가지로 컴퓨트 셰이더는 고유한 내장 입력 변수 집합을 가집니다. 내장 변수는 항상 `gl_` 접두사가 붙습니다. 그러한 내장 변수 중 하나가 `gl_GlobalInvocationID`이며, 이는 현재 디스패치 내에서 현재 컴퓨트 셰이더 인보케이션을 고유하게 식별하는 변수입니다. 우리는 이것을 사용하여 파티클 배열에 인덱싱합니다. + +## 컴퓨트 명령 실행하기 + +### 디스패치 + +이제 GPU에 실제로 컴퓨트 작업을 하도록 지시할 차례입니다. 이는 커맨드 버퍼 내에서 `vkCmdDispatch`를 호출하여 수행됩니다. 완벽하게 맞지는 않지만, 디스패치는 컴퓨트에서 그래픽스의 `vkCmdDraw`와 같은 드로우 콜과 유사한 역할을 합니다. 이는 최대 3차원의 주어진 수의 컴퓨트 작업 항목을 디스패치합니다. + +```c++ +VkCommandBufferBeginInfo beginInfo{}; +beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + +if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) { + throw std::runtime_error("failed to begin recording command buffer!"); +} + +... + +vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipeline); +vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 0, 1, &computeDescriptorSets[i], 0, 0); + +vkCmdDispatch(computeCommandBuffer, PARTICLE_COUNT / 256, 1, 1); + +... + +if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to record command buffer!"); +} +``` + +`vkCmdDispatch`는 x 차원에서 `PARTICLE_COUNT / 256` 개의 로컬 워크 그룹을 디스패치합니다. 우리 파티클 배열은 선형이므로 다른 두 차원은 1로 남겨두어 1차원 디스패치가 됩니다. 그런데 왜 파티클 수(우리 배열의)를 256으로 나누는 걸까요? 이전 단락에서 워크 그룹의 모든 컴퓨트 셰이더가 256번의 인보케이션을 수행한다고 정의했기 때문입니다. 따라서 4096개의 파티클이 있다면 16개의 워크 그룹을 디스패치하고, 각 워크 그룹은 256개의 컴퓨트 셰이더 인보케이션을 실행합니다. 두 숫자를 올바르게 맞추는 것은 일반적으로 워크로드와 실행 중인 하드웨어에 따라 약간의 조정과 프로파일링이 필요합니다. 만약 파티클 크기가 동적이고 항상 256으로 나누어지지 않는다면, 컴퓨트 셰이더 시작 부분에서 `gl_GlobalInvocationID`를 사용하여 전역 인보케이션 인덱스가 파티클 수보다 크면 반환할 수 있습니다. + +그리고 컴퓨트 파이프라인의 경우와 마찬가지로, 컴퓨트 커맨드 버퍼는 그래픽스 커맨드 버퍼보다 훨씬 적은 상태를 포함합니다. 렌더 패스를 시작하거나 뷰포트를 설정할 필요가 없습니다. + +### 작업 제출하기 + +우리 샘플은 컴퓨트와 그래픽스 작업을 모두 수행하므로, 프레임당 그래픽스와 컴퓨트 큐에 두 번의 제출을 할 것입니다(`drawFrame` 함수 참조). + +```c++ +... +if (vkQueueSubmit(computeQueue, 1, &submitInfo, nullptr) != VK_SUCCESS) { + throw std::runtime_error("failed to submit compute command buffer!"); +}; +... +if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) { + throw std::runtime_error("failed to submit draw command buffer!"); +} +``` + +컴퓨트 큐에 대한 첫 번째 제출은 컴퓨트 셰이더를 사용하여 파티클 위치를 업데이트하고, 두 번째 제출은 그 업데이트된 데이터를 사용하여 파티클 시스템을 그릴 것입니다. + +### 그래픽스와 컴퓨트 동기화하기 + +동기화는 Vulkan의 중요한 부분이며, 그래픽스와 함께 컴퓨트를 할 때는 더욱 그렇습니다. 잘못되거나 부족한 동기화는 컴퓨트 셰이더가 파티클 업데이트(쓰기)를 마치기 전에 정점 단계가 파티클 그리기(읽기)를 시작하거나(읽기 후 쓰기(read-after-write) 위험), 정점 파이프라인 부분에서 아직 사용 중인 파티클을 컴퓨트 셰이더가 업데이트하기 시작하는(쓰기 후 읽기(write-after-read) 위험) 결과를 초래할 수 있습니다. + +따라서 그래픽스와 컴퓨트 부하를 적절하게 동기화하여 이러한 경우가 발생하지 않도록 해야 합니다. 컴퓨트 워크로드를 제출하는 방식에 따라 여러 가지 방법이 있지만, 우리 경우처럼 두 개의 별도 제출을 하는 경우에는 [세마포어](03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md#page_Semaphores)와 [펜스](03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md#page_Fences)를 사용하여 정점 셰이더가 컴퓨트 셰이더의 업데이트가 끝날 때까지 정점 가져오기를 시작하지 않도록 보장할 것입니다. + +두 제출이 차례로 순서대로 이루어지더라도 GPU에서 이 순서대로 실행된다는 보장이 없기 때문에 이는 필수적입니다. 대기 및 신호 세마포어를 추가하면 이 실행 순서가 보장됩니다. + +먼저 `createSyncObjects`에서 컴퓨트 작업을 위한 새로운 동기화 프리미티브 세트를 추가합니다. 컴퓨트 펜스는 그래픽스 펜스와 마찬가지로 신호(signaled) 상태로 생성됩니다. 그렇지 않으면 첫 번째 드로우가 펜스가 신호되기를 기다리다 타임아웃될 것이기 때문입니다. ([이전 프레임 기다리기](03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md#page_Waiting-for-the-previous-frame)에서 자세히 설명). + +```c++ +std::vector computeInFlightFences; +std::vector computeFinishedSemaphores; +... +computeInFlightFences.resize(MAX_FRAMES_IN_FLIGHT); +computeFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT); + +VkSemaphoreCreateInfo semaphoreInfo{}; +semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + +VkFenceCreateInfo fenceInfo{}; +fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; +fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + ... + if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &computeFinishedSemaphores[i]) != VK_SUCCESS || + vkCreateFence(device, &fenceInfo, nullptr, &computeInFlightFences[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create compute synchronization objects for a frame!"); + } +} +``` +그런 다음 이것들을 사용하여 컴퓨트 버퍼 제출을 그래픽스 제출과 동기화합니다. + +```c++ +// 컴퓨트 제출 +vkWaitForFences(device, 1, &computeInFlightFences[currentFrame], VK_TRUE, UINT64_MAX); + +updateUniformBuffer(currentFrame); + +vkResetFences(device, 1, &computeInFlightFences[currentFrame]); + +vkResetCommandBuffer(computeCommandBuffers[currentFrame], /*VkCommandBufferResetFlagBits*/ 0); +recordComputeCommandBuffer(computeCommandBuffers[currentFrame]); + +submitInfo.commandBufferCount = 1; +submitInfo.pCommandBuffers = &computeCommandBuffers[currentFrame]; +submitInfo.signalSemaphoreCount = 1; +submitInfo.pSignalSemaphores = &computeFinishedSemaphores[currentFrame]; + +if (vkQueueSubmit(computeQueue, 1, &submitInfo, computeInFlightFences[currentFrame]) != VK_SUCCESS) { + throw std::runtime_error("failed to submit compute command buffer!"); +}; + +// 그래픽스 제출 +vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); + +... + +vkResetFences(device, 1, &inFlightFences[currentFrame]); + +vkResetCommandBuffer(commandBuffers[currentFrame], /*VkCommandBufferResetFlagBits*/ 0); +recordCommandBuffer(commandBuffers[currentFrame], imageIndex); + +VkSemaphore waitSemaphores[] = { computeFinishedSemaphores[currentFrame], imageAvailableSemaphores[currentFrame] }; +VkPipelineStageFlags waitStages[] = { VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT }; +submitInfo = {}; +submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + +submitInfo.waitSemaphoreCount = 2; +submitInfo.pWaitSemaphores = waitSemaphores; +submitInfo.pWaitDstStageMask = waitStages; +submitInfo.commandBufferCount = 1; +submitInfo.pCommandBuffers = &commandBuffers[currentFrame]; +submitInfo.signalSemaphoreCount = 1; +submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentFrame]; + +if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) { + throw std::runtime_error("failed to submit draw command buffer!"); +} +``` + +[세마포어 챕터](03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md#page_Semaphores)의 샘플과 유사하게, 이 설정은 대기 세마포어를 지정하지 않았기 때문에 컴퓨트 셰이더를 즉시 실행합니다. `vkWaitForFences` 명령으로 현재 프레임의 컴퓨트 커맨드 버퍼가 실행을 마칠 때까지 기다리기 때문에 이는 괜찮습니다. + +반면에 그래픽스 제출은 컴퓨트 작업이 끝나기를 기다려야 컴퓨트 버퍼가 아직 업데이트 중일 때 정점을 가져오기 시작하지 않습니다. 따라서 현재 프레임의 `computeFinishedSemaphores`를 기다리고, 그래픽스 제출이 정점이 소비되는 `VK_PIPELINE_STAGE_VERTEX_INPUT_BIT` 단계에서 기다리도록 합니다. + +하지만 프래그먼트 셰이더가 이미지가 제시될 때까지 컬러 어태치먼트에 출력하지 않도록 프레젠테이션도 기다려야 합니다. 따라서 `VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT` 단계에서 현재 프레임의 `imageAvailableSemaphores`도 기다립니다. + +## 파티클 시스템 그리기 + +앞서 우리는 Vulkan의 버퍼가 여러 사용 사례를 가질 수 있다는 것을 배웠고, 그래서 우리는 파티클을 포함하는 셰이더 저장 버퍼를 셰이더 저장 버퍼 비트와 정점 버퍼 비트를 모두 사용하여 생성했습니다. 이는 이전 챕터에서 "순수" 정점 버퍼를 사용했던 것처럼 드로잉을 위해 셰이더 저장 버퍼를 사용할 수 있다는 것을 의미합니다. + +먼저 우리 파티클 구조체와 일치하도록 정점 입력 상태를 설정합니다. + +```c++ +struct Particle { + ... + + static std::array getAttributeDescriptions() { + std::array attributeDescriptions{}; + + attributeDescriptions[0].binding = 0; + attributeDescriptions[0].location = 0; + attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; + attributeDescriptions[0].offset = offsetof(Particle, position); + + attributeDescriptions[1].binding = 0; + attributeDescriptions[1].location = 1; + attributeDescriptions[1].format = VK_FORMAT_R32G32B32A32_SFLOAT; + attributeDescriptions[1].offset = offsetof(Particle, color); + + return attributeDescriptions; + } +}; +``` + +`velocity`는 컴퓨트 셰이더에서만 사용되므로 정점 입력 속성에 추가하지 않는다는 점에 유의하세요. + +그런 다음 다른 정점 버퍼처럼 바인딩하고 그립니다. + +```c++ +vkCmdBindVertexBuffers(commandBuffer, 0, 1, &shaderStorageBuffers[currentFrame], offsets); + +vkCmdDraw(commandBuffer, PARTICLE_COUNT, 1, 0, 0); +``` + +## 결론 + +이 챕터에서는 CPU의 작업을 GPU로 오프로드하기 위해 컴퓨트 셰이더를 사용하는 방법을 배웠습니다. 컴퓨트 셰이더가 없다면 현대 게임과 애플리케이션의 많은 효과들이 불가능하거나 훨씬 느리게 실행될 것입니다. 하지만 그래픽스보다 더 많은 분야에서 컴퓨트는 많은 사용 사례를 가지고 있으며, 이 챕터는 가능한 것의 일부만을 보여줍니다. 이제 컴퓨트 셰이더를 사용하는 방법을 알았으니, 다음과 같은 고급 컴퓨트 주제들을 살펴보는 것도 좋습니다. + +- 공유 메모리 +- [비동기 컴퓨트 (Asynchronous compute)](https://github.com/KhronosGroup/Vulkan-Samples/tree/master/samples/performance/async_compute) +- 원자적 연산 (Atomic operations) +- [서브그룹 (Subgroups)](https://www.khronos.org/blog/vulkan-subgroup-tutorial) + +[공식 Khronos Vulkan 샘플 저장소](https://github.com/KhronosGroup/Vulkan-Samples/tree/master/samples/api)에서 몇 가지 고급 컴퓨트 샘플을 찾을 수 있습니다. + +[C++ 코드](/code/31_compute_shader.cpp) / +[정점 셰이더](/code/31_shader_compute.vert) / +[프래그먼트 셰이더](/code/31_shader_compute.frag) / +[컴퓨트 셰이더](/code/31_shader_compute.comp) \ No newline at end of file diff --git a/ko/90_FAQ.md b/ko/90_FAQ.md new file mode 100644 index 00000000..ee9233b4 --- /dev/null +++ b/ko/90_FAQ.md @@ -0,0 +1,51 @@ +이 페이지는 Vulkan 애플리케이션을 개발하면서 마주칠 수 있는 일반적인 문제들에 대한 해결책을 다룹니다. + +## 코어 검증 레이어에서 접근 위반(access violation) 오류가 발생합니다 + +MSI Afterburner / RivaTuner Statistics Server가 Vulkan과 몇 가지 호환성 문제가 있으므로, 해당 프로그램이 실행 중이지 않은지 확인하십시오. + +## 검증 레이어에서 아무런 메시지도 표시되지 않거나, 검증 레이어를 사용할 수 없습니다 + +먼저, 프로그램이 종료된 후에도 터미널 창을 열어 두어 검증 레이어가 오류를 출력할 시간을 주어야 합니다. Visual Studio에서는 F5 대신 **Ctrl+F5**로 프로그램을 실행하고, Linux에서는 터미널 창에서 직접 프로그램을 실행하여 이를 수행할 수 있습니다. + +그래도 메시지가 표시되지 않고 검증 레이어가 켜져 있는 것이 확실하다면, [이 페이지](https://vulkan.lunarg.com/doc/view/1.2.135.0/windows/getting_started.html)의 '설치 확인(Verify the Installation)' 안내에 따라 Vulkan SDK가 올바르게 설치되었는지 확인해야 합니다. 또한 `VK_LAYER_KHRONOS_validation` 레이어를 지원하려면 SDK 버전이 최소 **1.1.106.0** 이상인지 확인하십시오. + +## vkCreateSwapchainKHR 함수 호출 시 SteamOverlayVulkanLayer64.dll에서 오류가 발생합니다 + +이것은 스팀(Steam) 클라이언트 베타의 호환성 문제로 보입니다. 다음과 같은 몇 가지 해결 방법이 있습니다: +* 스팀 베타 프로그램 참여를 중단합니다. +* `DISABLE_VK_LAYER_VALVE_steam_overlay_1` 환경 변수를 `1`로 설정합니다. +* `HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ImplicitLayers` 경로 아래 레지스트리에서 스팀 오버레이 Vulkan 레이어 항목을 삭제합니다. + +예시: + +![](/images/steam_layers_env.png) + +## vkCreateInstance 호출이 VK_ERROR_INCOMPATIBLE_DRIVER 오류와 함께 실패합니다 + +최신 MoltenVK SDK와 함께 macOS를 사용하는 경우, `vkCreateInstance`가 `VK_ERROR_INCOMPATIBLE_DRIVER` 오류를 반환할 수 있습니다. 이는 [Vulkan SDK 버전 1.3.216 이상](https://vulkan.lunarg.com/doc/sdk/1.3.216.0/mac/getting_started.html)부터 MoltenVK가 아직 완벽하게 호환되지 않기 때문에, 이를 사용하려면 `VK_KHR_PORTABILITY_subset` 확장을 활성화해야 하기 때문입니다. + +`VkInstanceCreateInfo`에 `VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR` 플래그를 추가하고, 인스턴스 확장 목록에 `VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME`을 추가해야 합니다. + +코드 예시: + +```c++ +... + +std::vector requiredExtensions; + +for(uint32_t i = 0; i < glfwExtensionCount; i++) { + requiredExtensions.emplace_back(glfwExtensions[i]); +} + +requiredExtensions.emplace_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + +createInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + +createInfo.enabledExtensionCount = (uint32_t) requiredExtensions.size(); +createInfo.ppEnabledExtensionNames = requiredExtensions.data(); + +if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); +} +``` \ No newline at end of file diff --git a/ko/95_Privacy_policy.md b/ko/95_Privacy_policy.md new file mode 100644 index 00000000..0d07e219 --- /dev/null +++ b/ko/95_Privacy_policy.md @@ -0,0 +1,21 @@ +## 일반 + +본 개인정보처리방침은 귀하가 vulkan-tutorial.com 또는 그 하위 도메인을 이용할 때 수집되는 정보에 적용됩니다. 본 문서는 웹사이트 소유자인 Alexander Overvoorde가 귀하에 대한 정보를 수집, 이용 및 공유하는 방법을 설명합니다. + +## 분석 + +이 웹사이트는 자체 호스팅하는 Matomo(이전 Piwik, [https://matomo.org/](https://matomo.org/)) 인스턴스를 사용하여 방문자에 대한 분석 데이터를 수집합니다. 이 분석 데이터에는 귀하가 방문하는 페이지, 사용하는 기기 및 브라우저 유형, 특정 페이지를 본 시간, 유입 경로(어떤 웹사이트를 통해 방문했는지)가 기록됩니다. 이 정보는 IP 주소의 앞 두 바이트(예: `123.123.xxx.xxx`)만 기록하여 익명으로 처리됩니다. 이렇게 익명화된 로그는 영구적으로 저장됩니다. + +수집된 분석 데이터는 웹사이트 콘텐츠 소비 방식, 전체 방문자 수, 이 웹사이트로 연결되는 다른 웹사이트들을 추적하는 데 사용됩니다. 이를 통해 커뮤니티와 더 원활하게 소통하고, 예를 들어 모바일 가독성 향상에 더 많은 시간을 투자해야 하는지 등 웹사이트의 어떤 부분을 개선해야 할지 결정하는 데 도움이 됩니다. + +이 데이터는 제3자와 공유되지 않습니다. + +## 광고 + +이 웹사이트는 제3자 광고 서버를 사용하며, 이 서버는 광고 참여도를 측정하기 위해 쿠키를 사용하여 웹사이트 내 활동을 추적할 수 있습니다. + +## 댓글 + +각 챕터 끝에는 제3자 서비스인 Disqus가 제공하는 댓글 섹션이 있습니다. Disqus 서비스는 댓글을 읽고 작성하는 것을 용이하게 하기 위해 신원 데이터를 수집하며, 서비스 개선을 위해 집계된 이용 정보를 수집합니다. + +이 제3자 서비스의 전체 개인정보처리방침은 다음 링크에서 확인하실 수 있습니다: [https://help.disqus.com/terms-and-policies/disqus-privacy-policy](https://help.disqus.com/terms-and-policies/disqus-privacy-policy) \ No newline at end of file From f461954bf367302b2a5a5390ae6becd99f30abe7 Mon Sep 17 00:00:00 2001 From: erenengine Date: Sat, 21 Jun 2025 12:32:08 +0900 Subject: [PATCH 2/4] Implement frame rendering optimization and swap chain recreation handling - Added support for multiple frames in flight to improve rendering efficiency. - Introduced a new function `recreateSwapChain` to handle swap chain recreation when the window surface changes. - Implemented cleanup functions for swap chain resources to ensure proper resource management. - Enhanced `drawFrame` function to manage frame synchronization and handle out-of-date swap chains. - Added explicit handling for window resizing and minimization scenarios to maintain application stability. --- .../00_Setup/00_Base_code.md | 284 +++---- .../00_Setup/01_Instance.md | 274 +++--- .../00_Setup/02_Validation_layers.md | 553 ++++--------- .../03_Physical_devices_and_queue_families.md | 412 ++++----- .../00_Setup/04_Logical_device_and_queues.md | 236 +++--- .../01_Presentation/00_Window_surface.md | 328 ++++---- .../01_Presentation/01_Swap_chain.md | 783 ++++++------------ .../01_Presentation/02_Image_views.md | 223 ++--- .../00_Introduction.md | 139 ++-- .../01_Shader_modules.md | 476 +++-------- .../02_Fixed_functions.md | 544 ++++-------- .../03_Render_passes.md | 274 +++--- .../04_Conclusion.md | 169 ++-- .../03_Drawing/00_Framebuffers.md | 177 ++-- .../03_Drawing/01_Command_buffers.md | 466 +++++------ .../02_Rendering_and_presentation.md | 775 ++++++----------- .../03_Drawing/03_Frames_in_flight.md | 266 +++--- .../04_Swap_chain_recreation.md | 416 +++++----- .../00_Setup/00_Base_code.md | 126 +-- .../00_Setup/01_Instance.md | 121 +-- .../00_Setup/02_Validation_layers.md | 266 ++---- .../03_Physical_devices_and_queue_families.md | 141 +--- .../00_Setup/04_Logical_device_and_queues.md | 93 +-- .../01_Presentation/00_Window_surface.md | 130 +-- .../01_Presentation/01_Swap_chain.md | 347 +++----- .../01_Presentation/02_Image_views.md | 53 +- .../00_Introduction.md | 103 +-- .../01_Shader_modules.md | 280 ++----- .../02_Fixed_functions.md | 270 ++---- .../03_Render_passes.md | 155 ++-- .../04_Conclusion.md | 90 +- .../03_Drawing/00_Framebuffers.md | 50 +- .../03_Drawing/01_Command_buffers.md | 199 ++--- .../02_Rendering_and_presentation.md | 405 +++------ .../03_Drawing/03_Frames_in_flight.md | 70 +- .../04_Swap_chain_recreation.md | 119 +-- 36 files changed, 3467 insertions(+), 6346 deletions(-) diff --git a/ko-rust/03_Drawing_a_triangle/00_Setup/00_Base_code.md b/ko-rust/03_Drawing_a_triangle/00_Setup/00_Base_code.md index df26c6ac..7d904248 100644 --- a/ko-rust/03_Drawing_a_triangle/00_Setup/00_Base_code.md +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/00_Base_code.md @@ -1,217 +1,171 @@ -## General structure +## 일반적인 구조 -In the previous chapter you've created a Vulkan project with all of the proper -configuration and tested it with the sample code. In this chapter we're starting -from scratch with the following code: +이전 장에서 여러분은 모든 설정을 마친 Vulkan 프로젝트를 만들고 예제 코드로 테스트했습니다. 이번 장에서는 다음 코드를 가지고 처음부터 시작합니다. -```c++ -#include +```rust +use ash::vk; -#include -#include -#include +use std::error::Error; -class HelloTriangleApplication { -public: - void run() { - initVulkan(); - mainLoop(); - cleanup(); - } +struct HelloTriangleApplication { -private: - void initVulkan() { +} +impl HelloTriangleApplication { + pub fn new() -> Self { + HelloTriangleApplication {} } - void mainLoop() { + pub fn run(&mut self) -> Result<(), Box> { + self.init_vulkan()?; + self.main_loop(); + self.cleanup(); + Ok(()) + } + fn init_vulkan(&mut self) -> Result<(), Box> { + Ok(()) } - void cleanup() { + fn main_loop(&mut self) { } -}; -int main() { - HelloTriangleApplication app; + fn cleanup(&mut self) { - try { - app.run(); - } catch (const std::exception& e) { - std::cerr << e.what() << std::endl; - return EXIT_FAILURE; } +} - return EXIT_SUCCESS; +fn main() { + let mut app = HelloTriangleApplication::new(); + + if let Err(e) = app.run() { + eprintln!("오류 발생: {}", e); + std::process::exit(1); + } } ``` -We first include the Vulkan header from the LunarG SDK, which provides the -functions, structures and enumerations. The `stdexcept` and `iostream` headers -are included for reporting and propagating errors. The `cstdlib` -header provides the `EXIT_SUCCESS` and `EXIT_FAILURE` macros. - -The program itself is wrapped into a class where we'll store the Vulkan objects -as private class members and add functions to initiate each of them, which will -be called from the `initVulkan` function. Once everything has been prepared, we -enter the main loop to start rendering frames. We'll fill in the `mainLoop` -function to include a loop that iterates until the window is closed in a moment. -Once the window is closed and `mainLoop` returns, we'll make sure to deallocate -the resources we've used in the `cleanup` function. - -If any kind of fatal error occurs during execution then we'll throw a -`std::runtime_error` exception with a descriptive message, which will propagate -back to the `main` function and be printed to the command prompt. To handle -a variety of standard exception types as well, we catch the more general `std::exception`. One example of an error that we will deal with soon is finding -out that a certain required extension is not supported. - -Roughly every chapter that follows after this one will add one new function that -will be called from `initVulkan` and one or more new Vulkan objects to the -private class members that need to be freed at the end in `cleanup`. - -## Resource management - -Just like each chunk of memory allocated with `malloc` requires a call to -`free`, every Vulkan object that we create needs to be explicitly destroyed when -we no longer need it. In C++ it is possible to perform automatic resource -management using [RAII](https://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization) -or smart pointers provided in the `` header. However, I've chosen to be -explicit about allocation and deallocation of Vulkan objects in this tutorial. -After all, Vulkan's niche is to be explicit about every operation to avoid -mistakes, so it's good to be explicit about the lifetime of objects to learn how -the API works. - -After following this tutorial, you could implement automatic resource management -by writing C++ classes that acquire Vulkan objects in their constructor and -release them in their destructor, or by providing a custom deleter to either -`std::unique_ptr` or `std::shared_ptr`, depending on your ownership requirements. -RAII is the recommended model for larger Vulkan programs, but -for learning purposes it's always good to know what's going on behind the -scenes. - -Vulkan objects are either created directly with functions like `vkCreateXXX`, or -allocated through another object with functions like `vkAllocateXXX`. After -making sure that an object is no longer used anywhere, you need to destroy it -with the counterparts `vkDestroyXXX` and `vkFreeXXX`. The parameters for these -functions generally vary for different types of objects, but there is one -parameter that they all share: `pAllocator`. This is an optional parameter that -allows you to specify callbacks for a custom memory allocator. We will ignore -this parameter in the tutorial and always pass `nullptr` as argument. - -## Integrating GLFW - -Vulkan works perfectly fine without creating a window if you want to use it for -off-screen rendering, but it's a lot more exciting to actually show something! -First replace the `#include ` line with - -```c++ -#define GLFW_INCLUDE_VULKAN -#include -``` +먼저 `ash` 크레이트에서 Vulkan 타입, 함수, 열거형을 가져옵니다. `std::error::Error` 트레이트는 Rust의 관용적인 오류 처리 방식에 사용됩니다. -That way GLFW will include its own definitions and automatically load the Vulkan -header with it. Add a `initWindow` function and add a call to it from the `run` -function before the other calls. We'll use that function to initialize GLFW and -create a window. - -```c++ -void run() { - initWindow(); - initVulkan(); - mainLoop(); - cleanup(); -} +프로그램 자체는 `struct`로 감싸져 있습니다. Vulkan 객체들을 구조체 필드로 저장하고, 각 객체를 초기화하는 메서드들을 추가하여 `run` 메서드에서 순서대로 호출할 것입니다. 모든 준비가 끝나면 메인 루프에 진입하여 프레임 렌더링을 시작합니다. 잠시 후에 창이 닫힐 때까지 이벤트를 처리하는 `main_loop`를 채워 넣을 것입니다. 루프가 종료되면, `cleanup` 메서드에서 사용했던 리소스들을 반드시 해제할 것입니다. -private: - void initWindow() { +실행 중 치명적인 오류가 발생하면, `Result` 열거형을 반환합니다. 이 예제에서는 `Box`를 사용하여 다양한 타입의 오류를 처리합니다. 오류가 발생하면 `main` 함수로 전파되어 터미널에 출력됩니다. 곧 다룰 오류의 한 예는 특정 필수 익스텐션이 지원되지 않는다는 것을 발견하는 경우입니다. - } -``` +이 장 이후의 거의 모든 장에서는 `init_vulkan`에서 호출될 새로운 초기화 로직과, `cleanup`에서 마지막에 해제해야 할 하나 이상의 새로운 Vulkan 객체를 구조체 필드에 추가할 것입니다. -The very first call in `initWindow` should be `glfwInit()`, which initializes -the GLFW library. Because GLFW was originally designed to create an OpenGL -context, we need to tell it to not create an OpenGL context with a subsequent -call: +## 리소스 관리 -```c++ -glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); -``` +C/C++에서 `malloc`으로 할당된 모든 메모리에 `free` 호출이 필요한 것처럼, 우리가 생성하는 모든 Vulkan 객체는 더 이상 필요하지 않을 때 명시적으로 파괴되어야 합니다. 하지만 Rust는 RAII(Resource Acquisition Is Initialization) 패턴을 언어 차원에서 강력하게 지원합니다. 객체의 수명은 소유권(ownership) 시스템에 의해 관리되며, 객체가 스코프를 벗어날 때 `Drop` 트레이트가 자동으로 호출됩니다. -Because handling resized windows takes special care that we'll look into later, -disable it for now with another window hint call: +Vulkan 핸들을 감싸는 구조체를 만들고 그 구조체에 `Drop`을 구현하면 리소스 관리를 자동화할 수 있습니다. 이는 더 큰 Rust 프로그램에서 권장되는 모델입니다. -```c++ -glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); -``` +하지만 이 튜토리얼에서는 C++ 원본의 교육적 목표를 따라, Vulkan 객체의 할당과 해제를 명시적으로 다루겠습니다. `cleanup` 메서드에서 `ash`가 제공하는 `destroy_...` 함수들을 직접 호출할 것입니다. 이를 통해 Vulkan API가 내부적으로 어떻게 동작하는지 명확하게 배울 수 있습니다. -All that's left now is creating the actual window. Add a `GLFWwindow* window;` -private class member to store a reference to it and initialize the window with: +Vulkan 객체는 `create_...` 함수로 직접 생성되거나, `allocate_...` 함수를 통해 다른 객체에서 할당됩니다. 사용이 끝난 객체는 그에 상응하는 `destroy_...` 및 `free_...` 함수로 파괴해야 합니다. 이 함수들의 매개변수는 일반적으로 객체 유형에 따라 다르지만, 마지막 매개변수로 `pAllocator`에 해당하는 인자를 받는 경우가 많습니다. 이는 사용자 정의 메모리 할당자를 위한 콜백을 지정하는 선택적 매개변수입니다. 이 튜토리얼에서는 이 매개변수를 무시하고 항상 `None`을 전달할 것입니다. -```c++ -window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr); -``` +## Winit 통합 -The first three parameters specify the width, height and title of the window. -The fourth parameter allows you to optionally specify a monitor to open the -window on and the last parameter is only relevant to OpenGL. +Vulkan은 오프스크린 렌더링을 위해 창 없이도 완벽하게 작동하지만, 화면에 무언가를 보여주는 것이 훨씬 더 흥미롭습니다! Rust 생태계에서는 창 생성을 위해 `winit` 라이브러리를 주로 사용합니다. -It's a good idea to use constants instead of hardcoded width and height numbers -because we'll be referring to these values a couple of times in the future. I've -added the following lines above the `HelloTriangleApplication` class definition: +먼저 `Cargo.toml` 파일에 `ash`와 `winit` 의존성을 추가해야 합니다. -```c++ -const uint32_t WIDTH = 800; -const uint32_t HEIGHT = 600; +```toml +[dependencies] +ash = "0.37" +winit = "0.28" ``` -and replaced the window creation call with +C++ 버전과 달리, Rust에서는 `run` 메서드가 창과 이벤트 루프의 생명주기를 직접 관리하는 것이 더 관용적입니다. `HelloTriangleApplication` 구조체는 Vulkan 관련 객체들을 소유하고, 창(window)은 Vulkan 초기화에 필요한 정보를 제공하는 역할을 합니다. -```c++ -window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); -``` +다음과 같이 `run` 메서드를 수정하고 `main_loop`의 시그니처를 변경합니다. `init_window` 함수는 필요하지 않으며, `run` 메서드 내에서 직접 창을 생성합니다. -You should now have a `initWindow` function that looks like this: +```rust +// use 문들을 파일 상단에 추가하세요. +use winit::event::{Event, WindowEvent}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::window::{Window, WindowBuilder}; -```c++ -void initWindow() { - glfwInit(); +// ... HelloTriangleApplication 구조체 정의 ... - glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); - glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); +impl HelloTriangleApplication { + pub fn run(&mut self) -> Result<(), Box> { + // 1. Winit으로 이벤트 루프와 창 생성 + let event_loop = EventLoop::new(); + let window = WindowBuilder::new() + .with_title("Vulkan") + .with_inner_size(winit::dpi::LogicalSize::new(WIDTH, HEIGHT)) + .with_resizable(false) // 크기 조절 비활성화 + .build(&event_loop)?; - window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); -} -``` + // 2. Vulkan 초기화 + self.init_vulkan(&window)?; + + // 3. 메인 루프 실행 + self.main_loop(event_loop, window); + + // 4. 리소스 정리 + self.cleanup(); -To keep the application running until either an error occurs or the window is -closed, we need to add an event loop to the `mainLoop` function as follows: + Ok(()) + } + + fn init_vulkan(&mut self, window: &Window) -> Result<(), Box> { + // 이 메서드는 나중에 채웁니다. + // window 파라미터는 표면(surface) 생성에 필요합니다. + Ok(()) + } -```c++ -void mainLoop() { - while (!glfwWindowShouldClose(window)) { - glfwPollEvents(); + fn main_loop(&mut self, event_loop: EventLoop<()>, window: Window) { + // ... } + // ... } ``` -This code should be fairly self-explanatory. It loops and checks for events like -pressing the X button until the window has been closed by the user. This is also -the loop where we'll later call a function to render a single frame. +`glfwInit()`을 호출하는 대신 `EventLoop::new()`를 호출하여 이벤트 루프를 만듭니다. `glfwWindowHint`를 설정하는 대신, `WindowBuilder`를 사용하여 창의 속성을 설정합니다. `with_resizable(false)`는 창 크기 조절을 비활성화합니다. -Once the window is closed, we need to clean up resources by destroying it and -terminating GLFW itself. This will be our first `cleanup` code: +창의 크기를 위해 상수를 사용하는 것이 좋습니다. 구조체 정의 위에 상수를 추가합시다. + +```rust +const WIDTH: u32 = 800; +const HEIGHT: u32 = 600; + +struct HelloTriangleApplication { + // ... +} +``` + +오류가 발생하거나 창이 닫힐 때까지 애플리케이션을 실행하려면 `main_loop`를 이벤트 처리 로직으로 채워야 합니다. Winit의 이벤트 루프는 `while` 루프 대신 클로저 기반으로 동작합니다. + +```rust +fn main_loop(&mut self, event_loop: EventLoop<()>, window: Window) { + event_loop.run(move |event, _, control_flow| { + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => { + *control_flow = ControlFlow::Exit; + } + Event::MainEventsCleared => { + // 여기서 프레임을 그리는 로직을 호출합니다. + } + _ => (), + } + }); +} +``` +이 코드는 `event_loop.run` 메서드를 호출하여 이벤트 처리를 시작합니다. 이 메서드는 애플리케이션이 종료될 때까지 반환되지 않습니다. 클로저 내부에서 발생하는 다양한 이벤트 (`Event`)를 `match` 문으로 처리합니다. 사용자가 창의 닫기 버튼을 누르면 `WindowEvent::CloseRequested` 이벤트가 발생하며, 이때 `control_flow`를 `ControlFlow::Exit`로 설정하여 루프를 종료시킵니다. 나중에는 `Event::MainEventsCleared` 분기에서 매 프레임을 그리는 함수를 호출하게 될 것입니다. -```c++ -void cleanup() { - glfwDestroyWindow(window); +Winit에서는 창과 관련된 리소스가 `window` 객체의 스코프가 끝날 때 자동으로 정리됩니다. `event_loop.run`이 종료되면 `run` 메서드도 종료되고 `window`가 소멸(drop)되므로, `glfwDestroyWindow`나 `glfwTerminate`에 해당하는 명시적인 호출이 필요 없습니다. `cleanup` 메서드는 순수하게 Vulkan 리소스를 정리하는 데 사용될 것입니다. 지금은 비워둡니다. - glfwTerminate(); +```rust +fn cleanup(&mut self) { + // 이 메서드는 나중에 Vulkan 객체들을 파괴하는 데 사용됩니다. } ``` -When you run the program now you should see a window titled `Vulkan` show up -until the application is terminated by closing the window. Now that we have the -skeleton for the Vulkan application, let's [create the first Vulkan object](!en/Drawing_a_triangle/Setup/Instance)! +이제 프로그램을 실행하면 "Vulkan"이라는 제목의 창이 나타나고, 창을 닫아 애플리케이션이 종료될 때까지 유지됩니다. 이제 Vulkan 애플리케이션의 골격을 갖추었으니, [첫 번째 Vulkan 객체 생성하기](!ko/Drawing_a_triangle/Setup/Instance)로 넘어갑시다! -[C++ code](/code/00_base_code.cpp) +[Rust 코드](/code/rust/00_base_code.rs) \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/00_Setup/01_Instance.md b/ko-rust/03_Drawing_a_triangle/00_Setup/01_Instance.md index d9744a1c..e8de0a03 100644 --- a/ko-rust/03_Drawing_a_triangle/00_Setup/01_Instance.md +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/01_Instance.md @@ -1,221 +1,169 @@ -## Creating an instance +## 인스턴스 생성 (Rust/Ash) -The very first thing you need to do is initialize the Vulkan library by creating -an *instance*. The instance is the connection between your application and the -Vulkan library and creating it involves specifying some details about your -application to the driver. +가장 먼저 해야 할 일은 *인스턴스(instance)*를 생성하여 Vulkan 라이브러리를 초기화하는 것입니다. 인스턴스는 애플리케이션과 Vulkan 라이브러리 간의 연결고리이며, 이 과정에서 애플리케이션에 대한 몇 가지 세부 정보를 드라이버에 지정해야 합니다. -Start by adding a `createInstance` function and invoking it in the -`initVulkan` function. +먼저 애플리케이션 구조체에 `create_instance` 함수를 추가하고, 나중에 만들 `init_vulkan` 함수에서 호출하도록 구성합니다. -```c++ -void initVulkan() { - createInstance(); +```rust +fn init_vulkan(&mut self) -> Result<(), Box> { + self.create_instance()?; + Ok(()) } ``` -Additionally add a data member to hold the handle to the instance: +또한 인스턴스 핸들을 저장할 필드를 구조체에 추가합니다. `ash::Instance` 타입은 Vulkan 인스턴스를 나타냅니다. -```c++ -private: -VkInstance instance; -``` - -Now, to create an instance we'll first have to fill in a struct with some -information about our application. This data is technically optional, but it may -provide some useful information to the driver in order to optimize our specific -application (e.g. because it uses a well-known graphics engine with -certain special behavior). This struct is called `VkApplicationInfo`: - -```c++ -void createInstance() { - VkApplicationInfo appInfo{}; - appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; - appInfo.pApplicationName = "Hello Triangle"; - appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); - appInfo.pEngineName = "No Engine"; - appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); - appInfo.apiVersion = VK_API_VERSION_1_0; +```rust +struct HelloTriangleApplication { + // ... 다른 필드들 + instance: ash::Instance, } ``` -As mentioned before, many structs in Vulkan require you to explicitly specify -the type in the `sType` member. This is also one of the many structs with a -`pNext` member that can point to extension information in the future. We're -using value initialization here to leave it as `nullptr`. - -A lot of information in Vulkan is passed through structs instead of function -parameters and we'll have to fill in one more struct to provide sufficient -information for creating an instance. This next struct is not optional and tells -the Vulkan driver which global extensions and validation layers we want to use. -Global here means that they apply to the entire program and not a specific -device, which will become clear in the next few chapters. - -```c++ -VkInstanceCreateInfo createInfo{}; -createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; -createInfo.pApplicationInfo = &appInfo; -``` +이제 인스턴스를 생성하기 위해, 먼저 애플리케이션에 대한 정보가 담긴 구조체를 채워야 합니다. 이 데이터는 기술적으로 선택 사항이지만, 드라이버가 우리의 특정 애플리케이션을 최적화하는 데 유용한 정보를 제공할 수 있습니다(예: 특정 특수 동작을 하는 잘 알려진 그래픽 엔진을 사용하는 경우). 이 구조체는 `vk::ApplicationInfo`입니다. -The first two parameters are straightforward. The next two layers specify the -desired global extensions. As mentioned in the overview chapter, Vulkan is a -platform agnostic API, which means that you need an extension to interface with -the window system. GLFW has a handy built-in function that returns the -extension(s) it needs to do that which we can pass to the struct: +Ash에서는 빌더(builder) 패턴을 사용하여 구조체를 안전하고 편리하게 생성합니다. -```c++ -uint32_t glfwExtensionCount = 0; -const char** glfwExtensions; +```rust +// create_instance 함수 내부 +use std::ffi::CStr; +use ash::vk; -glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); - -createInfo.enabledExtensionCount = glfwExtensionCount; -createInfo.ppEnabledExtensionNames = glfwExtensions; -``` +// ... -The last two members of the struct determine the global validation layers to -enable. We'll talk about these more in-depth in the next chapter, so just leave -these empty for now. +let app_name = CStr::from_bytes_with_nul(b"Hello Triangle\0").unwrap(); +let engine_name = CStr::from_bytes_with_nul(b"No Engine\0").unwrap(); -```c++ -createInfo.enabledLayerCount = 0; +let app_info = vk::ApplicationInfo::builder() + .application_name(app_name) + .application_version(vk::make_api_version(0, 1, 0, 0)) + .engine_name(engine_name) + .engine_version(vk::make_api_version(0, 1, 0, 0)) + .api_version(vk::API_VERSION_1_0); ``` -We've now specified everything Vulkan needs to create an instance and we can -finally issue the `vkCreateInstance` call: +Vulkan의 많은 구조체와 마찬가지로, `sType` 멤버는 빌더가 자동으로 설정해 줍니다. Ash의 빌더는 `pNext` 멤버를 다루는 확장 기능도 지원하지만, 여기서는 기본값인 null 포인터로 둡니다. -```c++ -VkResult result = vkCreateInstance(&createInfo, nullptr, &instance); -``` +다음으로, 인스턴스 생성을 위한 더 중요한 정보를 담은 구조체를 채워야 합니다. 이 구조체는 필수이며, 우리가 사용할 전역 확장(global extensions)과 유효성 검사 레이어(validation layers)를 Vulkan 드라이버에 알려줍니다. -As you'll see, the general pattern that object creation function parameters in -Vulkan follow is: +```rust +let create_info = vk::InstanceCreateInfo::builder() + .application_info(&app_info); +``` -* Pointer to struct with creation info -* Pointer to custom allocator callbacks, always `nullptr` in this tutorial -* Pointer to the variable that stores the handle to the new object +이제 원하는 전역 확장을 지정해야 합니다. Vulkan은 플랫폼에 구애받지 않는 API이므로, 창 시스템과 상호작용하려면 확장이 필요합니다. C++의 GLFW와 마찬가지로, Rust 생태계에서는 `winit` 창 라이브러리와 `ash-window` 크레이트를 함께 사용하여 필요한 확장 목록을 쉽게 얻을 수 있습니다. -If everything went well then the handle to the instance was stored in the -`VkInstance` class member. Nearly all Vulkan functions return a value of type -`VkResult` that is either `VK_SUCCESS` or an error code. To check if the -instance was created successfully, we don't need to store the result and can -just use a check for the success value instead: +```rust +// winit::window::Window 객체가 있다고 가정합니다. +// let window: winit::window::Window = ...; -```c++ -if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { - throw std::runtime_error("failed to create instance!"); -} +let required_extensions = ash_window::enumerate_required_extensions(window.display_handle().unwrap().as_raw()) + .unwrap() + .to_vec(); ``` -Now run the program to make sure that the instance is created successfully. +이제 이 확장 목록을 `InstanceCreateInfo` 빌더에 추가합니다. -## Encountered VK_ERROR_INCOMPATIBLE_DRIVER: -If using MacOS with the latest MoltenVK sdk, you may get `VK_ERROR_INCOMPATIBLE_DRIVER` -returned from `vkCreateInstance`. According to the [Getting Start Notes](https://vulkan.lunarg.com/doc/sdk/1.3.216.0/mac/getting_started.html). Beginning with the 1.3.216 Vulkan SDK, the `VK_KHR_PORTABILITY_subset` -extension is mandatory. +```rust +let mut create_info = vk::InstanceCreateInfo::builder() + .application_info(&app_info) + .enabled_extension_names(&required_extensions); +``` -To get over this error, first add the `VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR` bit -to `VkInstanceCreateInfo` struct's flags, then add `VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME` -to instance enabled extension list. +구조체의 마지막 부분은 활성화할 전역 유효성 검사 레이어를 결정합니다. 다음 장에서 자세히 다룰 것이므로 지금은 비워둡니다. -Typically the code could be like this: -```c++ -... +```rust +// create_info 빌더 체인에 추가 +// .enabled_layer_count(0) +// .pp_enabled_layer_names(std::ptr::null()) +``` -std::vector requiredExtensions; +이제 Vulkan이 인스턴스를 생성하는 데 필요한 모든 것을 지정했으므로, 마침내 `create_instance`를 호출할 수 있습니다. Ash에서는 Vulkan 라이브러리 로딩을 담당하는 `ash::Entry` 객체를 통해 이 함수를 호출합니다. -for(uint32_t i = 0; i < glfwExtensionCount; i++) { - requiredExtensions.emplace_back(glfwExtensions[i]); -} +```rust +// entry: &ash::Entry 는 함수 인자로 전달받았다고 가정 +let instance = unsafe { + entry + .create_instance(&create_info, None) + .expect("Failed to create instance!") +}; +self.instance = instance; +``` -requiredExtensions.emplace_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); +Rust/Ash에서 객체 생성 함수의 패턴은 다음과 같습니다. -createInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; +1. 생성 정보가 담긴 구조체의 빌더를 사용합니다. +2. `ash::Entry` (전역 함수용) 또는 다른 Vulkan 객체(자식 객체용)의 메서드를 호출합니다. +3. 첫 번째 인자로 생성 정보 구조체에 대한 참조를 전달합니다. +4. 두 번째 인자는 사용자 정의 할당자 콜백으로, 이 튜토리얼에서는 `None`을 사용합니다. +5. 이 함수는 `Result`를 반환하므로, Rust의 오류 처리 메커니즘(`?` 연산자, `match`, `expect` 등)을 사용하여 결과를 처리합니다. -createInfo.enabledExtensionCount = (uint32_t) requiredExtensions.size(); -createInfo.ppEnabledExtensionNames = requiredExtensions.data(); +`create_instance` 호출은 `unsafe` 블록 안에 있습니다. 이는 Ash가 우리가 제공한 포인터(예: 확장 이름)가 유효하고 올바른 생명주기를 가졌는지 보장할 수 없기 때문입니다. 우리는 이 조건들을 충족함을 보장해야 합니다. -if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { - throw std::runtime_error("failed to create instance!"); -} -``` +### VK_ERROR_INCOMPATIBLE_DRIVER 오류 발생 시 (macOS) +최신 MoltenVK SDK를 사용하는 macOS에서는 `VK_KHR_portability_subset` 확장이 필수로 요구될 수 있습니다. 이로 인해 `create_instance`가 실패할 수 있습니다. -## Checking for extension support +이 문제를 해결하려면, `VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR` 플래그를 추가하고, `VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME` 확장을 활성화해야 합니다. -If you look at the `vkCreateInstance` documentation then you'll see that one of -the possible error codes is `VK_ERROR_EXTENSION_NOT_PRESENT`. We could simply -specify the extensions we require and terminate if that error code comes back. -That makes sense for essential extensions like the window system interface, but -what if we want to check for optional functionality? +Rust에서는 `cfg` 속성을 사용하여 플랫폼별 코드를 작성할 수 있습니다. -To retrieve a list of supported extensions before creating an instance, there's -the `vkEnumerateInstanceExtensionProperties` function. It takes a pointer to a -variable that stores the number of extensions and an array of -`VkExtensionProperties` to store details of the extensions. It also takes an -optional first parameter that allows us to filter extensions by a specific -validation layer, which we'll ignore for now. +```rust +let mut required_extensions = ash_window::enumerate_required_extensions(window.display_handle().unwrap().as_raw()) + .unwrap() + .to_vec(); -To allocate an array to hold the extension details we first need to know how -many there are. You can request just the number of extensions by leaving the -latter parameter empty: +let mut create_info = vk::InstanceCreateInfo::builder() + .application_info(&app_info); -```c++ -uint32_t extensionCount = 0; -vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); -``` +if cfg!(target_os = "macos") { + required_extensions.push(ash::extensions::khr::PortabilityEnumeration::name().as_ptr()); + create_info = create_info.flags(vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR); +} -Now allocate an array to hold the extension details (`include `): +create_info = create_info.enabled_extension_names(&required_extensions); -```c++ -std::vector extensions(extensionCount); +// ... create_instance 호출 ``` -Finally we can query the extension details: +### 확장 지원 여부 확인 -```c++ -vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data()); -``` +선택적 기능에 대한 지원 여부를 확인하고 싶다면, 인스턴스를 생성하기 전에 `enumerate_instance_extension_properties`를 사용하여 지원되는 확장 목록을 가져올 수 있습니다. Ash는 이 과정을 매우 간단하게 만들어 줍니다. -Each `VkExtensionProperties` struct contains the name and version of an -extension. We can list them with a simple for loop (`\t` is a tab for -indentation): +```rust +// entry: &ash::Entry -```c++ -std::cout << "available extensions:\n"; +let available_extensions = entry + .enumerate_instance_extension_properties(None) + .expect("Failed to enumerate instance extensions"); -for (const auto& extension : extensions) { - std::cout << '\t' << extension.extensionName << '\n'; +println!("Available extensions:"); +for extension in available_extensions.iter() { + let extension_name = unsafe { CStr::from_ptr(extension.extension_name.as_ptr()) }; + println!("\t{}", extension_name.to_str().unwrap()); } ``` -You can add this code to the `createInstance` function if you'd like to provide -some details about the Vulkan support. As a challenge, try to create a function -that checks if all of the extensions returned by -`glfwGetRequiredInstanceExtensions` are included in the supported extensions -list. - -## Cleaning up +Ash는 C++처럼 두 번 호출할 필요 없이 지원되는 모든 확장을 `Vec`로 편리하게 반환해 줍니다. -The `VkInstance` should be only destroyed right before the program exits. It can -be destroyed in `cleanup` with the `vkDestroyInstance` function: +도전 과제로, `ash-window`가 요구하는 모든 확장이 `available_extensions` 목록에 포함되어 있는지 확인하는 코드를 작성해 보세요. Rust의 `HashSet`과 이터레이터를 사용하면 효율적으로 구현할 수 있습니다. -```c++ -void cleanup() { - vkDestroyInstance(instance, nullptr); +### 정리 (Cleaning up) - glfwDestroyWindow(window); +`ash::Instance`는 프로그램이 종료되기 직전에만 파괴되어야 합니다. Rust에서는 RAII(Resource Acquisition Is Initialization) 패턴을 따르는 것이 가장 일반적입니다. 애플리케이션 구조체에 대해 `Drop` 트레이트를 구현하여 리소스가 범위를 벗어날 때 자동으로 정리되도록 합니다. - glfwTerminate(); +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + // 다른 모든 Vulkan 리소스가 파괴된 후에 인스턴스를 파괴해야 합니다. + self.instance.destroy_instance(None); + } + } } ``` -The parameters for the `vkDestroyInstance` function are straightforward. As -mentioned in the previous chapter, the allocation and deallocation functions -in Vulkan have an optional allocator callback that we'll ignore by passing -`nullptr` to it. All of the other Vulkan resources that we'll create in the -following chapters should be cleaned up before the instance is destroyed. +`destroy_instance` 호출은 `unsafe`입니다. 왜냐하면 이 인스턴스로부터 생성된 다른 모든 Vulkan 리소스(디바이스, 버퍼 등)가 이미 파괴되었음을 프로그래머가 보장해야 하기 때문입니다. `drop` 메서드 내에서 필드의 소멸 순서를 올바르게 지정하면 이 요구사항을 충족할 수 있습니다. -Before continuing with the more complex steps after instance creation, it's time -to evaluate our debugging options by checking out [validation layers](!en/Drawing_a_triangle/Setup/Validation_layers). +인스턴스 생성 후의 더 복잡한 단계로 넘어가기 전에, [유효성 검사 레이어](!ko/Drawing_a_triangle/Setup/Validation_layers)를 살펴봄으로써 디버깅 옵션을 평가해 볼 시간입니다. -[C++ code](/code/01_instance_creation.cpp) +[Rust 코드](/code/rust/01_instance_creation.rs) \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md b/ko-rust/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md index 569a0178..b031f835 100644 --- a/ko-rust/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md @@ -1,458 +1,247 @@ -## What are validation layers? - -The Vulkan API is designed around the idea of minimal driver overhead and one of -the manifestations of that goal is that there is very limited error checking in -the API by default. Even mistakes as simple as setting enumerations to incorrect -values or passing null pointers to required parameters are generally not -explicitly handled and will simply result in crashes or undefined behavior. -Because Vulkan requires you to be very explicit about everything you're doing, -it's easy to make many small mistakes like using a new GPU feature and -forgetting to request it at logical device creation time. - -However, that doesn't mean that these checks can't be added to the API. Vulkan -introduces an elegant system for this known as *validation layers*. Validation -layers are optional components that hook into Vulkan function calls to apply -additional operations. Common operations in validation layers are: - -* Checking the values of parameters against the specification to detect misuse -* Tracking creation and destruction of objects to find resource leaks -* Checking thread safety by tracking the threads that calls originate from -* Logging every call and its parameters to the standard output -* Tracing Vulkan calls for profiling and replaying - -Here's an example of what the implementation of a function in a diagnostics -validation layer could look like: - -```c++ -VkResult vkCreateInstance( - const VkInstanceCreateInfo* pCreateInfo, - const VkAllocationCallbacks* pAllocator, - VkInstance* instance) { - - if (pCreateInfo == nullptr || instance == nullptr) { - log("Null pointer passed to required parameter!"); - return VK_ERROR_INITIALIZATION_FAILED; - } +## 밸리데이션 레이어란 무엇인가? - return real_vkCreateInstance(pCreateInfo, pAllocator, instance); -} -``` +Vulkan API는 최소한의 드라이버 오버헤드를 목표로 설계되었으며, 이 목표가 드러나는 부분 중 하나는 API에 기본적으로 내장된 오류 검사가 매우 제한적이라는 점입니다. 열거형(enum) 값을 잘못 설정하거나 필수 파라미터에 null 포인터를 전달하는 것과 같은 간단한 실수조차도 일반적으로 명시적으로 처리되지 않으며, 크래시나 정의되지 않은 동작(undefined behavior)으로 이어질 뿐입니다. Vulkan은 개발자가 수행하는 모든 작업을 매우 명시적으로 지정해야 하므로, 논리 장치(logical device)를 생성할 때 새로운 GPU 기능을 사용하면서 해당 기능 사용을 요청하는 것을 잊는 등 많은 사소한 실수를 하기 쉽습니다. -These validation layers can be freely stacked to include all the debugging -functionality that you're interested in. You can simply enable validation layers -for debug builds and completely disable them for release builds, which gives you -the best of both worlds! - -Vulkan does not come with any validation layers built-in, but the LunarG Vulkan -SDK provides a nice set of layers that check for common errors. They're also -completely [open source](https://github.com/KhronosGroup/Vulkan-ValidationLayers), -so you can check which kind of mistakes they check for and contribute. Using the -validation layers is the best way to avoid your application breaking on -different drivers by accidentally relying on undefined behavior. - -Validation layers can only be used if they have been installed onto the system. -For example, the LunarG validation layers are only available on PCs with the -Vulkan SDK installed. - -There were formerly two different types of validation layers in Vulkan: instance -and device specific. The idea was that instance layers would only check -calls related to global Vulkan objects like instances, and device specific layers -would only check calls related to a specific GPU. Device specific layers have now been -deprecated, which means that instance validation layers apply to all Vulkan -calls. The specification document still recommends that you enable validation -layers at device level as well for compatibility, which is required by some -implementations. We'll simply specify the same layers as the instance at logical -device level, which we'll see [later on](!en/Drawing_a_triangle/Setup/Logical_device_and_queues). - -## Using validation layers - -In this section we'll see how to enable the standard diagnostics layers provided -by the Vulkan SDK. Just like extensions, validation layers need to be enabled by -specifying their name. All of the useful standard validation is bundled into a layer included in the SDK that is known as `VK_LAYER_KHRONOS_validation`. - -Let's first add two configuration variables to the program to specify the layers -to enable and whether to enable them or not. I've chosen to base that value on -whether the program is being compiled in debug mode or not. The `NDEBUG` macro -is part of the C++ standard and means "not debug". - -```c++ -const uint32_t WIDTH = 800; -const uint32_t HEIGHT = 600; - -const std::vector validationLayers = { - "VK_LAYER_KHRONOS_validation" -}; - -#ifdef NDEBUG - const bool enableValidationLayers = false; -#else - const bool enableValidationLayers = true; -#endif -``` +하지만 이러한 검사를 API에 추가할 수 없다는 의미는 아닙니다. Vulkan은 이를 위해 *밸리데이션 레이어*라는 멋진 시스템을 도입했습니다. 밸리데이션 레이어는 Vulkan 함수 호출에 끼어들어(hook into) 추가적인 작업을 적용하는 선택적 컴포넌트입니다. 밸리데이션 레이어의 일반적인 작업은 다음과 같습니다. -We'll add a new function `checkValidationLayerSupport` that checks if all of -the requested layers are available. First list all of the available layers -using the `vkEnumerateInstanceLayerProperties` function. Its usage is identical -to that of `vkEnumerateInstanceExtensionProperties` which was discussed in the -instance creation chapter. +* 사양에 명시된 값과 파라미터 값을 비교하여 오용을 감지 +* 객체의 생성 및 소멸을 추적하여 리소스 누수(resource leak)를 발견 +* 호출이 발생한 스레드를 추적하여 스레드 안전성(thread safety)을 검사 +* 모든 호출과 그 파라미터를 표준 출력으로 로깅 +* 프로파일링 및 재현(replaying)을 위해 Vulkan 호출을 추적 -```c++ -bool checkValidationLayerSupport() { - uint32_t layerCount; - vkEnumerateInstanceLayerProperties(&layerCount, nullptr); +이러한 밸리데이션 레이어들은 원하는 모든 디버깅 기능을 포함하도록 자유롭게 쌓아서(stacked) 사용할 수 있습니다. 디버그 빌드에서는 밸리데이션 레이어를 활성화하고 릴리즈 빌드에서는 완전히 비활성화하면, 두 가지 장점을 모두 누릴 수 있습니다! - std::vector availableLayers(layerCount); - vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data()); +Vulkan은 내장된 밸리데이션 레이어를 제공하지 않지만, LunarG Vulkan SDK는 일반적인 오류를 검사하는 훌륭한 레이어 세트를 제공합니다. 이 레이어들은 완전히 [오픈 소스](https://github.com/KhronosGroup/Vulkan-ValidationLayers)이므로, 어떤 종류의 실수를 검사하는지 확인하고 기여할 수도 있습니다. 밸리데이션 레이어를 사용하는 것은 실수로 정의되지 않은 동작에 의존하여 애플리케이션이 다른 드라이버에서 깨지는 것을 방지하는 가장 좋은 방법입니다. - return false; -} -``` +밸리데이션 레이어는 시스템에 설치된 경우에만 사용할 수 있습니다. 예를 들어, LunarG 밸리데이션 레이어는 Vulkan SDK가 설치된 PC에서만 사용할 수 있습니다. -Next, check if all of the layers in `validationLayers` exist in the -`availableLayers` list. You may need to include `` for `strcmp`. +## 밸리데이션 레이어 사용하기 (Rust & Ash) -```c++ -for (const char* layerName : validationLayers) { - bool layerFound = false; +이 섹션에서는 Vulkan SDK가 제공하는 표준 진단 레이어를 활성화하는 방법을 살펴보겠습니다. 확장(extension)과 마찬가지로, 밸리데이션 레이어도 이름을 지정하여 활성화해야 합니다. 모든 유용한 표준 밸리데이션은 SDK에 포함된 `VK_LAYER_KHRONOS_validation`이라는 레이어에 번들로 제공됩니다. - for (const auto& layerProperties : availableLayers) { - if (strcmp(layerName, layerProperties.layerName) == 0) { - layerFound = true; - break; - } - } +먼저 활성화할 레이어와 그 활성화 여부를 결정하는 상수를 정의합시다. Rust에서는 C++의 `#ifdef NDEBUG`와 유사한 역할을 하는 조건부 컴파일 속성 `#[cfg(debug_assertions)]`를 사용합니다. `debug_assertions`는 디버그 빌드에서 활성화됩니다. - if (!layerFound) { - return false; - } -} +Vulkan C API는 C 스타일 문자열(null로 끝나는 바이트 배열)을 요구하므로, `std::ffi::CString`을 사용해 Rust 문자열을 변환해야 합니다. -return true; -``` +```rust +use std::ffi::{c_char, CStr, CString}; -We can now use this function in `createInstance`: +const WIDTH: u32 = 800; +const HEIGHT: u32 = 600; -```c++ -void createInstance() { - if (enableValidationLayers && !checkValidationLayerSupport()) { - throw std::runtime_error("validation layers requested, but not available!"); - } +// Vulkan C API와 통신하기 위해 C 문자열 포인터 목록을 만듭니다. +const VALIDATION_LAYERS: [*const c_char; 1] = + [b"VK_LAYER_KHRONOS_validation\0".as_ptr() as *const c_char]; - ... -} +// cfg! 매크로를 사용하여 컴파일 타임에 값을 결정합니다. +const ENABLE_VALIDATION_LAYERS: bool = cfg!(debug_assertions); ``` +**참고:** `b"..."` 구문은 바이트 슬라이스를 만듭니다. 여기에 `\0`을 추가하여 C 문자열 형식에 맞춥니다. -Now run the program in debug mode and ensure that the error does not occur. If -it does, then have a look at the FAQ. - -Finally, modify the `VkInstanceCreateInfo` struct instantiation to include the -validation layer names if they are enabled: - -```c++ -if (enableValidationLayers) { - createInfo.enabledLayerCount = static_cast(validationLayers.size()); - createInfo.ppEnabledLayerNames = validationLayers.data(); -} else { - createInfo.enabledLayerCount = 0; -} -``` - -If the check was successful then `vkCreateInstance` should not ever return a -`VK_ERROR_LAYER_NOT_PRESENT` error, but you should run the program to make sure. - -## Message callback +다음으로, 요청된 모든 레이어가 사용 가능한지 확인하는 `check_validation_layer_support` 함수를 추가합니다. `ash`의 `Entry::enumerate_instance_layer_properties` 함수를 사용합니다. -The validation layers will print debug messages to the standard output by default, but we can also handle them ourselves by providing an explicit callback in our program. This will also allow you to decide which kind of messages you would like to see, because not all are necessarily (fatal) errors. If you don't want to do that right now then you may skip to the last section in this chapter. +```rust +// 이 함수는 C 문자열 포인터를 다루므로 unsafe 블록이 필요합니다. +unsafe fn check_validation_layer_support(entry: &ash::Entry) -> bool { + let available_layers = entry + .enumerate_instance_layer_properties() + .expect("인스턴스 레이어 속성을 가져오지 못했습니다."); -To set up a callback in the program to handle messages and the associated details, we have to set up a debug messenger with a callback using the `VK_EXT_debug_utils` extension. + for &layer_name_ptr in VALIDATION_LAYERS.iter() { + let layer_name = CStr::from_ptr(layer_name_ptr); + let mut layer_found = false; -We'll first create a `getRequiredExtensions` function that will return the -required list of extensions based on whether validation layers are enabled or -not: - -```c++ -std::vector getRequiredExtensions() { - uint32_t glfwExtensionCount = 0; - const char** glfwExtensions; - glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); - - std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); + for layer_properties in available_layers.iter() { + let available_layer_name = CStr::from_ptr(layer_properties.layer_name.as_ptr()); + if available_layer_name == layer_name { + layer_found = true; + break; + } + } - if (enableValidationLayers) { - extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + if !layer_found { + return false; + } } - return extensions; + true } ``` -The extensions specified by GLFW are always required, but the debug messenger -extension is conditionally added. Note that I've used the -`VK_EXT_DEBUG_UTILS_EXTENSION_NAME` macro here which is equal to the literal -string "VK_EXT_debug_utils". Using this macro lets you avoid typos. - -We can now use this function in `createInstance`: - -```c++ -auto extensions = getRequiredExtensions(); -createInfo.enabledExtensionCount = static_cast(extensions.size()); -createInfo.ppEnabledExtensionNames = extensions.data(); -``` - -Run the program to make sure you don't receive a -`VK_ERROR_EXTENSION_NOT_PRESENT` error. We don't really need to check for the -existence of this extension, because it should be implied by the availability of -the validation layers. - -Now let's see what a debug callback function looks like. Add a new static member -function called `debugCallback` with the `PFN_vkDebugUtilsMessengerCallbackEXT` -prototype. The `VKAPI_ATTR` and `VKAPI_CALL` ensure that the function has the -right signature for Vulkan to call it. +이제 이 함수를 인스턴스 생성 로직에 통합합니다. -```c++ -static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( - VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, - VkDebugUtilsMessageTypeFlagsEXT messageType, - const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, - void* pUserData) { - - std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl; - - return VK_FALSE; +```rust +// in create_instance() +if ENABLE_VALIDATION_LAYERS && !unsafe { check_validation_layer_support(&self.entry) } { + panic!("요청한 밸리데이션 레이어를 사용할 수 없습니다!"); } ``` -The first parameter specifies the severity of the message, which is one of the following flags: - -* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT`: Diagnostic message -* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT`: Informational message like the creation of a resource -* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT`: Message about behavior that is not necessarily an error, but very likely a bug in your application -* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT`: Message about behavior that is invalid and may cause crashes +마지막으로 `ash`의 빌더 패턴을 사용하여 `InstanceCreateInfo` 구조체를 수정하고, 밸리데이션 레이어를 활성화합니다. -The values of this enumeration are set up in such a way that you can use a comparison operation to check if a message is equal or worse compared to some level of severity, for example: +```rust +// in create_instance() +let mut create_info = vk::InstanceCreateInfo::builder() + .application_info(&app_info) + .enabled_extension_names(&extensions); -```c++ -if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { - // Message is important enough to show +if ENABLE_VALIDATION_LAYERS { + create_info = create_info.enabled_layer_names(&VALIDATION_LAYERS); } +// ... ``` +`ash`의 빌더는 슬라이스(`&VALIDATION_LAYERS`)를 받아 자동으로 `enabledLayerCount`와 `ppEnabledLayerNames`를 설정해 주므로 매우 편리합니다. -The `messageType` parameter can have the following values: - -* `VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT`: Some event has happened that is unrelated to the specification or performance -* `VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT`: Something has happened that violates the specification or indicates a possible mistake -* `VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT`: Potential non-optimal use of Vulkan - -The `pCallbackData` parameter refers to a `VkDebugUtilsMessengerCallbackDataEXT` struct containing the details of the message itself, with the most important members being: - -* `pMessage`: The debug message as a null-terminated string -* `pObjects`: Array of Vulkan object handles related to the message -* `objectCount`: Number of objects in array - -Finally, the `pUserData` parameter contains a pointer that was specified during the setup of the callback and allows you to pass your own data to it. - -The callback returns a boolean that indicates if the Vulkan call that triggered -the validation layer message should be aborted. If the callback returns true, -then the call is aborted with the `VK_ERROR_VALIDATION_FAILED_EXT` error. This -is normally only used to test the validation layers themselves, so you should -always return `VK_FALSE`. - -All that remains now is telling Vulkan about the callback function. Perhaps -somewhat surprisingly, even the debug callback in Vulkan is managed with a -handle that needs to be explicitly created and destroyed. Such a callback is part of a *debug messenger* and you can have as many of them as you want. Add a class member for -this handle right under `instance`: +## 메시지 콜백 (Message Callback) -```c++ -VkDebugUtilsMessengerEXT debugMessenger; -``` +밸리데이션 레이어는 기본적으로 디버그 메시지를 표준 출력으로 인쇄하지만, Rust에서 직접 콜백을 제공하여 처리할 수 있습니다. 이를 위해 `VK_EXT_debug_utils` 확장이 필요합니다. -Now add a function `setupDebugMessenger` to be called from `initVulkan` right -after `createInstance`: +먼저 필요한 확장 목록을 반환하는 함수를 수정합니다. `ash`는 `ash::extensions::ext::DebugUtils::name()`을 통해 확장 이름을 `&'static CStr`로 제공하여 편리하게 사용할 수 있습니다. -```c++ -void initVulkan() { - createInstance(); - setupDebugMessenger(); -} +```rust +fn get_required_extensions(window: &winit::window::Window) -> Vec<*const c_char> { + let mut extensions = ash_window::enumerate_required_extensions(window) + .expect("필요한 확장 목록을 가져오지 못했습니다.") + .to_vec(); -void setupDebugMessenger() { - if (!enableValidationLayers) return; + if ENABLE_VALIDATION_LAYERS { + extensions.push(ash::extensions::ext::DebugUtils::name().as_ptr()); + } + extensions } ``` -We'll need to fill in a structure with details about the messenger and its callback: - -```c++ -VkDebugUtilsMessengerCreateInfoEXT createInfo{}; -createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; -createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; -createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; -createInfo.pfnUserCallback = debugCallback; -createInfo.pUserData = nullptr; // Optional -``` - -The `messageSeverity` field allows you to specify all the types of severities you would like your callback to be called for. I've specified all types except for `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT` here to receive notifications about possible problems while leaving out verbose general debug info. - -Similarly the `messageType` field lets you filter which types of messages your callback is notified about. I've simply enabled all types here. You can always disable some if they're not useful to you. - -Finally, the `pfnUserCallback` field specifies the pointer to the callback function. You can optionally pass a pointer to the `pUserData` field which will be passed along to the callback function via the `pUserData` parameter. You could use this to pass a pointer to the `HelloTriangleApplication` class, for example. +이제 디버그 콜백 함수를 정의합니다. 이 함수는 C ABI를 따라야 하므로 `extern "system"`으로 선언합니다. `ash`는 `PFN_vkDebugUtilsMessengerCallbackEXT` 타입 별칭을 제공합니다. -Note that there are many more ways to configure validation layer messages and debug callbacks, but this is a good setup to get started with for this tutorial. See the [extension specification](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap50.html#VK_EXT_debug_utils) for more info about the possibilities. +```rust +use ash::vk; // vk 네임스페이스를 가져옵니다. +use std::os::raw::c_void; -This struct should be passed to the `vkCreateDebugUtilsMessengerEXT` function to -create the `VkDebugUtilsMessengerEXT` object. Unfortunately, because this -function is an extension function, it is not automatically loaded. We have to -look up its address ourselves using `vkGetInstanceProcAddr`. We're going to -create our own proxy function that handles this in the background. I've added it -right above the `HelloTriangleApplication` class definition. +// Vulkan이 호출할 콜백 함수 +unsafe extern "system" fn vulkan_debug_callback( + message_severity: vk::DebugUtilsMessageSeverityFlagsEXT, + _message_type: vk::DebugUtilsMessageTypeFlagsEXT, + p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT, + _p_user_data: *mut c_void, +) -> vk::Bool32 { + let message = CStr::from_ptr((*p_callback_data).p_message); + let severity = format!("{:?}", message_severity).to_lowercase(); + println!("[Vulkan Validation] [{}]: {:?}", severity, message); -```c++ -VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) { - auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT"); - if (func != nullptr) { - return func(instance, pCreateInfo, pAllocator, pDebugMessenger); - } else { - return VK_ERROR_EXTENSION_NOT_PRESENT; - } + vk::FALSE } ``` +**안전성(Safety):** 이 함수는 `unsafe`입니다. C 코드로부터 호출되며, `p_callback_data` 같은 원시 포인터를 역참조하기 때문입니다. `CStr::from_ptr`을 사용하여 메모리를 안전하게 읽습니다. -The `vkGetInstanceProcAddr` function will return `nullptr` if the function -couldn't be loaded. We can now call this function to create the extension -object if it's available: +이제 이 콜백을 등록하는 `setup_debug_messenger` 함수를 만듭니다. `ash`에서는 확장 기능 로드가 매우 간단합니다. `DebugUtils` 구조체를 생성하기만 하면 됩니다. 이 구조체와 메신저 핸들은 애플리케이션이 살아있는 동안 유지되어야 하므로, 주 애플리케이션 구조체에 멤버로 저장합니다. -```c++ -if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { - throw std::runtime_error("failed to set up debug messenger!"); +```rust +// 애플리케이션 구조체에 추가 +struct HelloTriangleApplication { + // ... + debug_utils: Option, + debug_messenger: Option, } -``` -The second to last parameter is again the optional allocator callback that we -set to `nullptr`, other than that the parameters are fairly straightforward. -Since the debug messenger is specific to our Vulkan instance and its layers, it -needs to be explicitly specified as first argument. You will also see this -pattern with other *child* objects later on. - -The `VkDebugUtilsMessengerEXT` object also needs to be cleaned up with a call to -`vkDestroyDebugUtilsMessengerEXT`. Similarly to `vkCreateDebugUtilsMessengerEXT` -the function needs to be explicitly loaded. - -Create another proxy function right below `CreateDebugUtilsMessengerEXT`: +impl HelloTriangleApplication { + fn setup_debug_messenger(&mut self) { + if !ENABLE_VALIDATION_LAYERS { + return; + } -```c++ -void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) { - auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT"); - if (func != nullptr) { - func(instance, debugMessenger, pAllocator); + let debug_utils = ash::extensions::ext::DebugUtils::new(&self.entry, &self.instance); + + let create_info = vk::DebugUtilsMessengerCreateInfoEXT::builder() + .message_severity( + vk::DebugUtilsMessageSeverityFlagsEXT::ERROR + | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING + // | vk::DebugUtilsMessageSeverityFlagsEXT::INFO + // | vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE, + ) + .message_type( + vk::DebugUtilsMessageTypeFlagsEXT::GENERAL + | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION + | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE, + ) + .pfn_user_callback(Some(vulkan_debug_callback)) + .build(); + + let debug_messenger = unsafe { + debug_utils + .create_debug_utils_messenger(&create_info, None) + .expect("디버그 메신저 설정에 실패했습니다!") + }; + + self.debug_utils = Some(debug_utils); + self.debug_messenger = Some(debug_messenger); } } ``` +`ash`의 `DebugUtils::new`는 필요한 함수 포인터(`vkCreateDebugUtilsMessengerEXT`, `vkDestroyDebugUtilsMessengerEXT` 등)를 자동으로 로드합니다. 수동으로 `vkGetInstanceProcAddr`를 호출할 필요가 없습니다. -Make sure that this function is either a static class function or a function -outside the class. We can then call it in the `cleanup` function: +메신저는 리소스이므로, 애플리케이션이 종료될 때 반드시 정리해야 합니다. Rust에서는 `Drop` 트레잇을 구현하여 이를 자동화하는 것이 가장 이상적입니다(RAII 패턴). -```c++ -void cleanup() { - if (enableValidationLayers) { - DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr); +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + if let (Some(debug_utils), Some(debug_messenger)) = (self.debug_utils.as_ref(), self.debug_messenger) { + debug_utils.destroy_debug_utils_messenger(*debug_messenger, None); + } + // ... 다른 리소스 정리 ... + self.instance.destroy_instance(None); + } } - - vkDestroyInstance(instance, nullptr); - - glfwDestroyWindow(window); - - glfwTerminate(); } ``` -## Debugging instance creation and destruction +## 인스턴스 생성 및 소멸 디버깅하기 -Although we've now added debugging with validation layers to the program we're not covering everything quite yet. The `vkCreateDebugUtilsMessengerEXT` call requires a valid instance to have been created and `vkDestroyDebugUtilsMessengerEXT` must be called before the instance is destroyed. This currently leaves us unable to debug any issues in the `vkCreateInstance` and `vkDestroyInstance` calls. +`vkCreateInstance`와 `vkDestroyInstance` 호출 자체의 오류를 디버깅하려면, 인스턴스 생성 정보에 디버그 메신저 생성 정보를 연결해야 합니다. `ash` 빌더의 `.p_next()` 메서드를 사용합니다. -However, if you closely read the [extension documentation](https://github.com/KhronosGroup/Vulkan-Docs/blob/main/appendices/VK_EXT_debug_utils.adoc#examples), you'll see that there is a way to create a separate debug utils messenger specifically for those two function calls. It requires you to simply pass a pointer to a `VkDebugUtilsMessengerCreateInfoEXT` struct in the `pNext` extension field of `VkInstanceCreateInfo`. First extract population of the messenger create info into a separate function: - -```c++ -void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) { - createInfo = {}; - createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; - createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; - createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; - createInfo.pfnUserCallback = debugCallback; -} +```rust +// in create_instance() +let mut debug_create_info = vk::DebugUtilsMessengerCreateInfoEXT::builder() + .message_severity( + vk::DebugUtilsMessageSeverityFlagsEXT::ERROR + | vk::DebugUtilsMessageSeverityFlagsEXT::WARNING, + ) + .message_type( + vk::DebugUtilsMessageTypeFlagsEXT::GENERAL + | vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION + | vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE, + ) + .pfn_user_callback(Some(vulkan_debug_callback)); -... +let mut create_info = vk::InstanceCreateInfo::builder() + .application_info(&app_info) + .enabled_extension_names(&extensions); -void setupDebugMessenger() { - if (!enableValidationLayers) return; - - VkDebugUtilsMessengerCreateInfoEXT createInfo; - populateDebugMessengerCreateInfo(createInfo); - - if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { - throw std::runtime_error("failed to set up debug messenger!"); - } +if ENABLE_VALIDATION_LAYERS { + create_info = create_info + .enabled_layer_names(&VALIDATION_LAYERS) + .p_next(&mut debug_create_info as *mut _ as *const c_void); } ``` +**안전성(Safety):** `.p_next()`는 원시 포인터를 받기 때문에 `unsafe` 코드 블록이 필요합니다. `&mut debug_create_info as *mut _ as *const c_void` 캐스팅은 Rust의 가변 참조를 C API가 요구하는 `const void*` 타입으로 변환합니다. `debug_create_info` 변수는 `vkCreateInstance` 호출 이후까지 살아있어야 하므로, `create_info`보다 먼저 선언되어야 합니다. -We can now re-use this in the `createInstance` function: - -```c++ -void createInstance() { - ... - - VkInstanceCreateInfo createInfo{}; - createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; - createInfo.pApplicationInfo = &appInfo; +이 메신저는 인스턴스 생성 및 소멸 시에만 사용되고 자동으로 정리되므로, 별도로 `destroy`를 호출할 필요가 없습니다. - ... +## 테스트하기 - VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; - if (enableValidationLayers) { - createInfo.enabledLayerCount = static_cast(validationLayers.size()); - createInfo.ppEnabledLayerNames = validationLayers.data(); +의도적으로 실수를 만들어 밸리데이션 레이어가 작동하는지 확인해 봅시다. `Drop` 구현에서 `destroy_debug_utils_messenger` 호출을 일시적으로 주석 처리하고 디버그 모드로 프로그램을 실행하세요. 프로그램이 종료될 때 다음과 유사한 오류 메시지가 터미널에 출력될 것입니다. - populateDebugMessengerCreateInfo(debugCreateInfo); - createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*) &debugCreateInfo; - } else { - createInfo.enabledLayerCount = 0; - - createInfo.pNext = nullptr; - } - - if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { - throw std::runtime_error("failed to create instance!"); - } -} ``` +[Vulkan Validation] [error]: Validation Error: [ UNASSIGNED-ObjectTracker-ObjectLeak ] Object 0x... (type: DEBUG_UTILS_MESSENGER_EXT) was not destroyed. +``` +이 메시지는 디버그 메신저가 제대로 정리되지 않았음을 명확히 알려줍니다. -The `debugCreateInfo` variable is placed outside the if statement to ensure that it is not destroyed before the `vkCreateInstance` call. By creating an additional debug messenger this way it will automatically be used during `vkCreateInstance` and `vkDestroyInstance` and cleaned up after that. - -## Testing - -Now let's intentionally make a mistake to see the validation layers in action. Temporarily remove the call to `DestroyDebugUtilsMessengerEXT` in the `cleanup` function and run your program. Once it exits you should see something like this: - -![](/images/validation_layer_test.png) - ->If you don't see any messages then [check your installation](https://vulkan.lunarg.com/doc/view/1.2.131.1/windows/getting_started.html#user-content-verify-the-installation). - -If you want to see which call triggered a message, you can add a breakpoint to the message callback and look at the stack trace. - -## Configuration - -There are a lot more settings for the behavior of validation layers than just -the flags specified in the `VkDebugUtilsMessengerCreateInfoEXT` struct. Browse -to the Vulkan SDK and go to the `Config` directory. There you will find a -`vk_layer_settings.txt` file that explains how to configure the layers. +## 설정 -To configure the layer settings for your own application, copy the file to the -`Debug` and `Release` directories of your project and follow the instructions to -set the desired behavior. However, for the remainder of this tutorial I'll -assume that you're using the default settings. +`vk_layer_settings.txt` 파일을 이용한 밸리데이션 레이어 설정은 C++과 동일하게 적용됩니다. 이 파일은 언어에 구애받지 않고 Vulkan SDK가 읽어 들이기 때문입니다. -Throughout this tutorial I'll be making a couple of intentional mistakes to show -you how helpful the validation layers are with catching them and to teach you -how important it is to know exactly what you're doing with Vulkan. Now it's time -to look at [Vulkan devices in the system](!en/Drawing_a_triangle/Setup/Physical_devices_and_queue_families). +`ash`와 Rust를 사용하면 빌더 패턴, 타입 안전성, `Drop`을 통한 자동 리소스 관리(RAII) 등 Rust의 강력한 기능들을 활용하여 C++보다 더 안전하고 간결하게 밸리데이션 레이어를 설정할 수 있습니다. 이제 시스템의 [Vulkan 장치](!ko/Drawing_a_triangle/Setup/Physical_devices_and_queue_families)에 대해 알아볼 시간입니다. -[C++ code](/code/02_validation_layers.cpp) +[Rust 코드](/code/02_validation_layers.rs) \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md b/ko-rust/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md index 5761b9bc..d4f5da7d 100644 --- a/ko-rust/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md @@ -1,364 +1,286 @@ -## Selecting a physical device +## 물리 디바이스 선택하기 -After initializing the Vulkan library through a VkInstance we need to look for -and select a graphics card in the system that supports the features we need. In -fact we can select any number of graphics cards and use them simultaneously, but -in this tutorial we'll stick to the first graphics card that suits our needs. +`Instance`를 통해 벌칸 라이브러리를 초기화한 후에는, 시스템에서 우리가 필요로 하는 기능을 지원하는 그래픽 카드를 찾아 선택해야 합니다. 사실 여러 개의 그래픽 카드를 선택하여 동시에 사용할 수도 있지만, 이 튜토리얼에서는 우리에게 필요한 첫 번째 그래픽 카드만 사용하겠습니다. -We'll add a function `pickPhysicalDevice` and add a call to it in the -`initVulkan` function. +`pick_physical_device` 함수를 추가하고 `init_vulkan` 함수에서 호출하도록 하겠습니다. Rust에서는 메서드를 `impl` 블록 안에 정의합니다. -```c++ -void initVulkan() { - createInstance(); - setupDebugMessenger(); - pickPhysicalDevice(); -} - -void pickPhysicalDevice() { +```rust +impl HelloTriangleApplication { + pub fn init_vulkan(&mut self) { + self.create_instance(); + self.setup_debug_messenger(); + self.pick_physical_device(); + } + fn pick_physical_device(&mut self) { + // ... + } } ``` -The graphics card that we'll end up selecting will be stored in a -VkPhysicalDevice handle that is added as a new class member. This object will be -implicitly destroyed when the VkInstance is destroyed, so we won't need to do -anything new in the `cleanup` function. +최종적으로 선택할 그래픽 카드는 `vk::PhysicalDevice` 핸들에 저장되며, 이 핸들을 구조체의 새 필드로 추가합니다. 이 객체는 `Instance`가 소멸될 때 암시적으로 함께 소멸되므로, `drop` 트레이트에서 따로 처리할 필요는 없습니다. -```c++ -VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; -``` +`vk::PhysicalDevice::null()`은 C++의 `VK_NULL_HANDLE`에 해당하는 값입니다. -Listing the graphics cards is very similar to listing extensions and starts with -querying just the number. +```rust +struct HelloTriangleApplication { + // ... + physical_device: vk::PhysicalDevice, +} -```c++ -uint32_t deviceCount = 0; -vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr); +impl HelloTriangleApplication { + pub fn new() -> Self { + // ... + Self { + // ... + physical_device: vk::PhysicalDevice::null(), + } + } +} ``` -If there are 0 devices with Vulkan support then there is no point going further. +그래픽 카드를 나열하는 것은 `ash`를 사용하면 매우 간단합니다. `Instance`의 `enumerate_physical_devices` 메서드는 사용 가능한 모든 물리 디바이스의 `Vec`을 반환합니다. -```c++ -if (deviceCount == 0) { - throw std::runtime_error("failed to find GPUs with Vulkan support!"); +```rust +fn pick_physical_device(&mut self) { + let devices = unsafe { + self.instance + .enumerate_physical_devices() + .expect("물리 디바이스를 찾는데 실패했습니다!") + }; } ``` -Otherwise we can now allocate an array to hold all of the VkPhysicalDevice -handles. +만약 벌칸을 지원하는 디바이스가 하나도 없다면 벡터는 비어있을 것입니다. -```c++ -std::vector devices(deviceCount); -vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()); +```rust +if devices.is_empty() { + panic!("벌칸을 지원하는 GPU를 찾지 못했습니다!"); +} ``` -Now we need to evaluate each of them and check if they are suitable for the -operations we want to perform, because not all graphics cards are created equal. -For that we'll introduce a new function: +이제 각 디바이스를 평가하여 우리가 수행하려는 작업에 적합한지 확인해야 합니다. 모든 그래픽 카드가 동일하게 만들어지지는 않기 때문입니다. 이를 위해 새로운 헬퍼(helper) 메서드를 도입하겠습니다. -```c++ -bool isDeviceSuitable(VkPhysicalDevice device) { - return true; +```rust +fn is_device_suitable(&self, device: vk::PhysicalDevice) -> bool { + true } ``` -And we'll check if any of the physical devices meet the requirements that we'll -add to that function. +그리고 물리 디바이스 중 어느 것이든 이 메서드의 요구사항을 충족하는지 확인할 것입니다. -```c++ -for (const auto& device : devices) { - if (isDeviceSuitable(device)) { - physicalDevice = device; +```rust +for &device in &devices { + if self.is_device_suitable(device) { + self.physical_device = device; break; } } -if (physicalDevice == VK_NULL_HANDLE) { - throw std::runtime_error("failed to find a suitable GPU!"); +if self.physical_device == vk::PhysicalDevice::null() { + panic!("적합한 GPU를 찾지 못했습니다!"); } ``` -The next section will introduce the first requirements that we'll check for in -the `isDeviceSuitable` function. As we'll start using more Vulkan features in -the later chapters we will also extend this function to include more checks. - -## Base device suitability checks +Rust의 이터레이터(iterator)를 사용하면 더 관용적으로 작성할 수 있습니다. -To evaluate the suitability of a device we can start by querying for some -details. Basic device properties like the name, type and supported Vulkan -version can be queried using vkGetPhysicalDeviceProperties. +```rust +let device = devices.into_iter().find(|&p_device| self.is_device_suitable(p_device)); -```c++ -VkPhysicalDeviceProperties deviceProperties; -vkGetPhysicalDeviceProperties(device, &deviceProperties); +match device { + Some(p_device) => self.physical_device = p_device, + None => panic!("적합한 GPU를 찾지 못했습니다!"), +} ``` -The support for optional features like texture compression, 64 bit floats and -multi viewport rendering (useful for VR) can be queried using -vkGetPhysicalDeviceFeatures: +이 튜토리얼에서는 이해하기 쉽도록 간단한 `for` 루프를 사용하겠습니다. -```c++ -VkPhysicalDeviceFeatures deviceFeatures; -vkGetPhysicalDeviceFeatures(device, &deviceFeatures); -``` +## 기본적인 디바이스 적합성 검사 -There are more details that can be queried from devices that we'll discuss later -concerning device memory and queue families (see the next section). +디바이스의 적합성을 평가하기 위해 몇 가지 세부 정보를 쿼리하는 것으로 시작할 수 있습니다. `ash`에서는 디바이스 속성을 가져오는 것이 더 간단합니다. -As an example, let's say we consider our application only usable for dedicated -graphics cards that support geometry shaders. Then the `isDeviceSuitable` -function would look like this: +```rust +fn is_device_suitable(&self, device: vk::PhysicalDevice) -> bool { + let device_properties = unsafe { self.instance.get_physical_device_properties(device) }; + let device_features = unsafe { self.instance.get_physical_device_features(device) }; + // ... +} +``` -```c++ -bool isDeviceSuitable(VkPhysicalDevice device) { - VkPhysicalDeviceProperties deviceProperties; - VkPhysicalDeviceFeatures deviceFeatures; - vkGetPhysicalDeviceProperties(device, &deviceProperties); - vkGetPhysicalDeviceFeatures(device, &deviceFeatures); +예를 들어, 우리 애플리케이션이 지오메트리 셰이더를 지원하는 외장 그래픽 카드에서만 사용 가능하다고 가정해 봅시다. 그렇다면 `is_device_suitable` 함수는 다음과 같이 보일 것입니다. - return deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU && - deviceFeatures.geometryShader; -} +```rust +return device_properties.device_type == vk::PhysicalDeviceType::DISCRETE_GPU + && device_features.geometry_shader == vk::TRUE; ``` -Instead of just checking if a device is suitable or not and going with the first -one, you could also give each device a score and pick the highest one. That way -you could favor a dedicated graphics card by giving it a higher score, but fall -back to an integrated GPU if that's the only available one. You could implement -something like that as follows: +디바이스가 적합한지 아닌지만 확인하고 첫 번째 것을 사용하는 대신, 각 디바이스에 점수를 매겨 가장 높은 점수를 받은 디바이스를 선택할 수도 있습니다. 이런 방식을 사용하면 외장 그래픽 카드에 더 높은 점수를 주어 선호하되, 사용 가능한 유일한 GPU가 내장 GPU일 경우 차선책으로 선택할 수 있습니다. -```c++ -#include +```rust +use std::collections::BTreeMap; -... +// ... -void pickPhysicalDevice() { - ... +fn pick_physical_device(&mut self) { + // ... - // Use an ordered map to automatically sort candidates by increasing score - std::multimap candidates; + // 후보들을 점수 기준으로 정렬하기 위해 BTreeMap 사용 + let mut candidates = BTreeMap::new(); - for (const auto& device : devices) { - int score = rateDeviceSuitability(device); - candidates.insert(std::make_pair(score, device)); + for &device in &devices { + let score = self.rate_device_suitability(device); + candidates.insert(score, device); } - // Check if the best candidate is suitable at all - if (candidates.rbegin()->first > 0) { - physicalDevice = candidates.rbegin()->second; + // 가장 높은 점수를 받은 후보가 적합한지 확인 + if let Some((score, device)) = candidates.iter().rev().next() { + if *score > 0 { + self.physical_device = *device; + } else { + panic!("적합한 GPU를 찾지 못했습니다!"); + } } else { - throw std::runtime_error("failed to find a suitable GPU!"); + panic!("적합한 GPU를 찾지 못했습니다!"); } } -int rateDeviceSuitability(VkPhysicalDevice device) { - ... +fn rate_device_suitability(&self, device: vk::PhysicalDevice) -> u32 { + let device_properties = unsafe { self.instance.get_physical_device_properties(device) }; + let device_features = unsafe { self.instance.get_physical_device_features(device) }; - int score = 0; + let mut score = 0; - // Discrete GPUs have a significant performance advantage - if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { + // 외장 GPU는 상당한 성능 이점을 가짐 + if device_properties.device_type == vk::PhysicalDeviceType::DISCRETE_GPU { score += 1000; } - // Maximum possible size of textures affects graphics quality - score += deviceProperties.limits.maxImageDimension2D; + // 텍스처의 최대 크기는 그래픽 품질에 영향을 줌 + score += device_properties.limits.max_image_dimension2d; - // Application can't function without geometry shaders - if (!deviceFeatures.geometryShader) { + // 애플리케이션은 지오메트리 셰이더 없이는 작동할 수 없음 + if device_features.geometry_shader != vk::TRUE { return 0; } - return score; + score } ``` -You don't need to implement all that for this tutorial, but it's to give you an -idea of how you could design your device selection process. Of course you can -also just display the names of the choices and allow the user to select. +이 튜토리얼에서 이 모든 것을 구현할 필요는 없지만, 디바이스 선택 프로세스를 어떻게 설계할 수 있는지에 대한 아이디어를 제공하기 위한 것입니다. -Because we're just starting out, Vulkan support is the only thing we need and -therefore we'll settle for just any GPU: +우리는 이제 막 시작하는 단계이므로, 벌칸 지원 여부만이 유일한 요구사항입니다. 따라서 어떤 GPU든 상관없이 사용하겠습니다. -```c++ -bool isDeviceSuitable(VkPhysicalDevice device) { - return true; +```rust +fn is_device_suitable(&self, device: vk::PhysicalDevice) -> bool { + true } ``` -In the next section we'll discuss the first real required feature to check for. +다음 섹션에서는 확인해야 할 첫 번째 실질적인 필수 기능에 대해 논의할 것입니다. -## Queue families +## 큐 패밀리 (Queue families) -It has been briefly touched upon before that almost every operation in Vulkan, -anything from drawing to uploading textures, requires commands to be submitted -to a queue. There are different types of queues that originate from different -*queue families* and each family of queues allows only a subset of commands. For -example, there could be a queue family that only allows processing of compute -commands or one that only allows memory transfer related commands. +이전에도 잠시 언급했듯이, 드로잉부터 텍스처 업로드에 이르기까지 벌칸의 거의 모든 작업은 명령(command)을 큐(queue)에 제출해야 합니다. 큐에는 여러 종류가 있으며, 이들은 각기 다른 *큐 패밀리(queue families)*에서 비롯됩니다. -We need to check which queue families are supported by the device and which one -of these supports the commands that we want to use. For that purpose we'll add a -new function `findQueueFamilies` that looks for all the queue families we need. +우리는 디바이스가 어떤 큐 패밀리를 지원하는지, 그리고 그중 어떤 큐 패밀리가 우리가 사용하려는 명령을 지원하는지 확인해야 합니다. 이를 위해, 우리가 필요로 하는 모든 큐 패밀리를 찾는 새로운 헬퍼 함수 `find_queue_families`를 추가하겠습니다. -Right now we are only going to look for a queue that supports graphics commands, -so the function could look like this: +다음 챕터에서 다른 종류의 큐를 찾게 될 것이므로, 미리 대비하여 인덱스들을 구조체로 묶는 것이 좋습니다. -```c++ -uint32_t findQueueFamilies(VkPhysicalDevice device) { - // Logic to find graphics queue family -} -``` - -However, in one of the next chapters we're already going to look for yet another -queue, so it's better to prepare for that and bundle the indices into a struct: - -```c++ +```rust struct QueueFamilyIndices { - uint32_t graphicsFamily; -}; - -QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { - QueueFamilyIndices indices; - // Logic to find queue family indices to populate struct with - return indices; + graphics_family: Option, } ``` -But what if a queue family is not available? We could throw an exception in -`findQueueFamilies`, but this function is not really the right place to make -decisions about device suitability. For example, we may *prefer* devices with a -dedicated transfer queue family, but not require it. Therefore we need some way -of indicating whether a particular queue family was found. +큐 패밀리가 존재하지 않는 경우를 처리해야 합니다. C++17의 `std::optional`과 같이, Rust에는 이를 위한 완벽한 타입인 `Option`가 내장되어 있습니다. `Option`은 값이 존재함(`Some(value)`) 또는 존재하지 않음(`None`)을 나타내는 열거형(enum)입니다. `is_some()` 메서드를 사용하여 값이 있는지 확인할 수 있습니다. -It's not really possible to use a magic value to indicate the nonexistence of a -queue family, since any value of `uint32_t` could in theory be a valid queue -family index including `0`. Luckily C++17 introduced a data structure to -distinguish between the case of a value existing or not: +이제 `find_queue_families`를 실제로 구현해 보겠습니다. -```c++ -#include +```rust +fn find_queue_families(&self, device: vk::PhysicalDevice) -> QueueFamilyIndices { + let mut indices = QueueFamilyIndices { + graphics_family: None, + }; -... + let queue_families = unsafe { + self.instance + .get_physical_device_queue_family_properties(device) + }; -std::optional graphicsFamily; + // ... -std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // false - -graphicsFamily = 0; - -std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // true -``` - -`std::optional` is a wrapper that contains no value until you assign something -to it. At any point you can query if it contains a value or not by calling its -`has_value()` member function. That means that we can change the logic to: - -```c++ -#include - -... - -struct QueueFamilyIndices { - std::optional graphicsFamily; -}; - -QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { - QueueFamilyIndices indices; - // Assign index to queue families that could be found - return indices; + indices } ``` -We can now begin to actually implement `findQueueFamilies`: +`ash`의 `get_physical_device_queue_family_properties` 메서드는 큐 패밀리 속성 정보가 담긴 `Vec`을 반환합니다. 우리는 `VK_QUEUE_GRAPHICS_BIT`를 지원하는 큐 패밀리를 최소 하나 이상 찾아야 합니다. -```c++ -QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { - QueueFamilyIndices indices; +`ash` 라이브러리는 비트 플래그를 위해 `bitflags` 크레이트를 사용합니다. 따라서 `&` 연산자 대신 `contains()` 메서드를 사용하여 플래그가 설정되어 있는지 확인하는 것이 더 관용적입니다. - ... +```rust +let mut i = 0; +for queue_family in queue_families.iter() { + if queue_family.queue_flags.contains(vk::QueueFlags::GRAPHICS) { + indices.graphics_family = Some(i as u32); + } - return indices; + i += 1; } ``` -The process of retrieving the list of queue families is exactly what you expect -and uses `vkGetPhysicalDeviceQueueFamilyProperties`: +Rust의 `enumerate()`를 사용하면 더 깔끔하게 작성할 수 있습니다. -```c++ -uint32_t queueFamilyCount = 0; -vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr); - -std::vector queueFamilies(queueFamilyCount); -vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data()); -``` - -The VkQueueFamilyProperties struct contains some details about the queue family, -including the type of operations that are supported and the number of queues -that can be created based on that family. We need to find at least one queue -family that supports `VK_QUEUE_GRAPHICS_BIT`. - -```c++ -int i = 0; -for (const auto& queueFamily : queueFamilies) { - if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) { - indices.graphicsFamily = i; +```rust +for (i, queue_family) in queue_families.iter().enumerate() { + if queue_family.queue_flags.contains(vk::QueueFlags::GRAPHICS) { + indices.graphics_family = Some(i as u32); } - - i++; } ``` -Now that we have this fancy queue family lookup function, we can use it as a -check in the `isDeviceSuitable` function to ensure that the device can process -the commands we want to use: +이제 이 큐 패밀리 조회 함수를 `is_device_suitable` 메서드에서 검사 항목으로 사용하여, 디바이스가 우리가 사용하려는 명령을 처리할 수 있는지 확인할 수 있습니다. -```c++ -bool isDeviceSuitable(VkPhysicalDevice device) { - QueueFamilyIndices indices = findQueueFamilies(device); +```rust +fn is_device_suitable(&self, device: vk::PhysicalDevice) -> bool { + let indices = self.find_queue_families(device); - return indices.graphicsFamily.has_value(); + indices.graphics_family.is_some() } ``` -To make this a little bit more convenient, we'll also add a generic check to the -struct itself: +이를 좀 더 편리하게 만들기 위해, 구조체 자체에 헬퍼 메서드를 추가하겠습니다. -```c++ -struct QueueFamilyIndices { - std::optional graphicsFamily; - - bool isComplete() { - return graphicsFamily.has_value(); +```rust +impl QueueFamilyIndices { + pub fn is_complete(&self) -> bool { + self.graphics_family.is_some() } -}; +} -... +// ... -bool isDeviceSuitable(VkPhysicalDevice device) { - QueueFamilyIndices indices = findQueueFamilies(device); +fn is_device_suitable(&self, device: vk::PhysicalDevice) -> bool { + let indices = self.find_queue_families(device); - return indices.isComplete(); + indices.is_complete() } ``` -We can now also use this for an early exit from `findQueueFamilies`: +이 메서드를 `find_queue_families`에서 조기 탈출하는 데에도 사용할 수 있습니다. -```c++ -for (const auto& queueFamily : queueFamilies) { - ... +```rust +for (i, queue_family) in queue_families.iter().enumerate() { + if queue_family.queue_flags.contains(vk::QueueFlags::GRAPHICS) { + indices.graphics_family = Some(i as u32); + } - if (indices.isComplete()) { + if indices.is_complete() { break; } - - i++; } ``` -Great, that's all we need for now to find the right physical device! The next -step is to [create a logical device](!en/Drawing_a_triangle/Setup/Logical_device_and_queues) -to interface with it. +좋습니다, 이것으로 적절한 물리 디바이스를 찾는 데 필요한 모든 작업이 끝났습니다! 다음 단계는 [논리 디바이스를 생성하여](!ko/Drawing_a_triangle/Setup/Logical_device_and_queues) 물리 디바이스와 상호작용하는 것입니다. -[C++ code](/code/03_physical_device_selection.cpp) +[Rust 코드](/code/03_physical_device_selection.rs) \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md b/ko-rust/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md index f2677d08..772ce0c6 100644 --- a/ko-rust/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md @@ -1,171 +1,165 @@ -## Introduction +## 서론 -After selecting a physical device to use we need to set up a *logical device* to -interface with it. The logical device creation process is similar to the -instance creation process and describes the features we want to use. We also -need to specify which queues to create now that we've queried which queue -families are available. You can even create multiple logical devices from the -same physical device if you have varying requirements. +사용할 물리 장치를 선택한 후에는, 이와 상호작용하기 위한 *논리 장치*를 설정해야 합니다. 논리 장치 생성 과정은 인스턴스 생성 과정과 유사하며, 우리가 사용하고자 하는 기능들을 기술합니다. 또한, 어떤 큐 패밀리를 사용할 수 있는지 질의했으므로 이제 어떤 큐를 생성할지 명시해야 합니다. 요구 사항이 다양하다면 동일한 물리 장치에서 여러 개의 논리 장치를 생성할 수도 있습니다. -Start by adding a new class member to store the logical device handle in. +먼저 애플리케이션 구조체에 논리 장치를 저장할 새 필드를 추가합니다. `ash::Device`는 논리 장치를 나타내는 타입입니다. -```c++ -VkDevice device; -``` - -Next, add a `createLogicalDevice` function that is called from `initVulkan`. +```rust +use ash::{Device, vk}; -```c++ -void initVulkan() { - createInstance(); - setupDebugMessenger(); - pickPhysicalDevice(); - createLogicalDevice(); +struct HelloTriangleApplication { + // ... 기존 필드들 ... + physical_device: vk::PhysicalDevice, + device: Device, } +``` -void createLogicalDevice() { - +다음으로, `init_vulkan` 내에서 `create_logical_device` 함수를 호출하도록 구성합니다. + +```rust +impl HelloTriangleApplication { + pub fn new(window: &Window) -> Self { + let mut app = // ... 초기화 ... + app.init_vulkan(); + app + } + + fn init_vulkan(&mut self) { + self.create_instance(); + self.setup_debug_messenger(); + self.pick_physical_device(); + self.create_logical_device(); + } + + fn create_logical_device(&mut self) { + // ... 구현 ... + } } ``` -## Specifying the queues to be created +## 생성할 큐 명시하기 -The creation of a logical device involves specifying a bunch of details in -structs again, of which the first one will be `VkDeviceQueueCreateInfo`. This -structure describes the number of queues we want for a single queue family. -Right now we're only interested in a queue with graphics capabilities. +논리 장치 생성은 여러 구조체를 채우는 것으로 시작합니다. 그 첫 번째는 `vk::DeviceQueueCreateInfo`입니다. 이 구조체는 단일 큐 패밀리에 대해 우리가 원하는 큐의 개수를 기술합니다. 지금은 그래픽스 기능이 있는 큐에만 관심이 있습니다. -```c++ -QueueFamilyIndices indices = findQueueFamilies(physicalDevice); +```rust +// create_logical_device 메서드 내부 +let indices = self.find_queue_families(self.physical_device); -VkDeviceQueueCreateInfo queueCreateInfo{}; -queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; -queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value(); -queueCreateInfo.queueCount = 1; +let queue_create_info = vk::DeviceQueueCreateInfo::builder() + .queue_family_index(indices.graphics_family.unwrap()) + .queue_priorities(&[1.0]) // 큐가 하나뿐이라도 우선순위는 필수 + .build(); ``` -The currently available drivers will only allow you to create a small number of -queues for each queue family and you don't really need more than one. That's -because you can create all of the command buffers on multiple threads and then -submit them all at once on the main thread with a single low-overhead call. +Rust와 `ash`에서는 빌더 패턴을 사용하여 구조체를 더 안전하고 명확하게 생성할 수 있습니다. C++의 `std::optional::value()`는 Rust의 `Option::unwrap()`에 해당합니다. 여기서는 큐 패밀리를 이미 찾았다고 확신할 수 있으므로 `unwrap()`을 사용합니다. -Vulkan lets you assign priorities to queues to influence the scheduling of -command buffer execution using floating point numbers between `0.0` and `1.0`. -This is required even if there is only a single queue: +사용 가능한 최신 드라이버들은 각 큐 패밀리마다 소수의 큐만 생성하도록 허용하며, 실제로 하나보다 더 많이 필요한 경우는 드뭅니다. 여러 스레드에서 모든 커맨드 버퍼를 생성한 뒤, 메인 스레드에서 단 한 번의 저비용 호출로 모두 제출할 수 있기 때문입니다. -```c++ -float queuePriority = 1.0f; -queueCreateInfo.pQueuePriorities = &queuePriority; -``` +Vulkan은 `0.0`에서 `1.0` 사이의 부동 소수점 값을 사용하여 큐에 우선순위를 할당하고 커맨드 버퍼 실행 스케줄링에 영향을 줄 수 있습니다. 큐가 하나만 있어도 이 설정은 필수입니다. 위 코드에서는 `&[1.0]` 슬라이스를 전달하여 이를 설정했습니다. -## Specifying used device features +## 사용할 장치 기능 명시하기 -The next information to specify is the set of device features that we'll be -using. These are the features that we queried support for with -`vkGetPhysicalDeviceFeatures` in the previous chapter, like geometry shaders. -Right now we don't need anything special, so we can simply define it and leave -everything to `VK_FALSE`. We'll come back to this structure once we're about to -start doing more interesting things with Vulkan. +다음 정보는 우리가 사용할 장치 기능의 집합입니다. 이는 이전 장에서 `vkGetPhysicalDeviceFeatures`로 지원 여부를 확인했던 지오메트리 셰이더 같은 기능들입니다. 지금 당장은 특별한 기능이 필요 없으므로, 모든 필드가 기본값(`false`)으로 설정된 구조체를 생성합니다. -```c++ -VkPhysicalDeviceFeatures deviceFeatures{}; +```rust +let device_features = vk::PhysicalDeviceFeatures::builder().build(); ``` -## Creating the logical device +더 흥미로운 Vulkan 기능을 사용하기 시작할 때 이 구조체로 다시 돌아올 것입니다. -With the previous two structures in place, we can start filling in the main -`VkDeviceCreateInfo` structure. +## 논리 장치 생성하기 -```c++ -VkDeviceCreateInfo createInfo{}; -createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; -``` +이제 `vk::DeviceCreateInfo` 구조체를 채울 준비가 되었습니다. -First add pointers to the queue creation info and device features structs: +```rust +// C-호환 문자열을 위한 준비 +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; -```c++ -createInfo.pQueueCreateInfos = &queueCreateInfo; -createInfo.queueCreateInfoCount = 1; +// 필요한 장치 확장 (스왑체인) +let device_extensions = [ + ash::extensions::khr::Swapchain::name().as_ptr(), +]; -createInfo.pEnabledFeatures = &deviceFeatures; -``` +// 유효성 검사 레이어 설정 (C++ 버전과 동일한 로직) +let validation_layer_names: Vec = VALIDATION_LAYERS + .iter() + .map(|&s| CString::new(s).unwrap()) + .collect(); +let validation_layer_name_ptrs: Vec<*const c_char> = validation_layer_names + .iter() + .map(|s| s.as_ptr()) + .collect(); -The remainder of the information bears a resemblance to the -`VkInstanceCreateInfo` struct and requires you to specify extensions and -validation layers. The difference is that these are device specific this time. +let mut create_info = vk::DeviceCreateInfo::builder() + .queue_create_infos(std::slice::from_ref(&queue_create_info)) + .enabled_features(&device_features) + .enabled_extension_names(&device_extensions); -An example of a device specific extension is `VK_KHR_swapchain`, which allows -you to present rendered images from that device to windows. It is possible that -there are Vulkan devices in the system that lack this ability, for example -because they only support compute operations. We will come back to this -extension in the swap chain chapter. +if ENABLE_VALIDATION_LAYERS { + create_info = create_info + .enabled_layer_names(&validation_layer_name_ptrs); +} +``` -Previous implementations of Vulkan made a distinction between instance and device specific validation layers, but this is [no longer the case](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap40.html#extendingvulkan-layers-devicelayerdeprecation). That means that the `enabledLayerCount` and `ppEnabledLayerNames` fields of `VkDeviceCreateInfo` are ignored by up-to-date implementations. However, it is still a good idea to set them anyway to be compatible with older implementations: +큐 생성 정보와 장치 기능 구조체에 대한 참조를 빌더에 전달합니다. `queue_create_infos`는 슬라이스를 받으므로, `std::slice::from_ref`를 사용하여 단일 항목으로부터 슬라이스를 만듭니다. -```c++ -createInfo.enabledExtensionCount = 0; +나머지 정보는 `vk::InstanceCreateInfo`와 유사하며 확장과 유효성 검사 레이어를 지정합니다. 차이점은 이번에는 장치에 한정된다는 점입니다. -if (enableValidationLayers) { - createInfo.enabledLayerCount = static_cast(validationLayers.size()); - createInfo.ppEnabledLayerNames = validationLayers.data(); -} else { - createInfo.enabledLayerCount = 0; -} -``` +장치별 확장의 예로 `VK_KHR_swapchain`이 있습니다. 이 확장은 렌더링된 이미지를 창에 표시하는 데 필수적입니다. 이 튜토리얼에서는 나중에 다루지만, 지금 활성화해 두는 것이 일반적입니다. -We won't need any device specific extensions for now. +이전 Vulkan 구현은 인스턴스와 장치별 유효성 검사 레이어를 구분했지만 [더 이상은 그렇지 않습니다](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap40.html#extendingvulkan-layers-devicelayerdeprecation). 즉, 최신 구현에서는 `vk::DeviceCreateInfo`의 `enabled_layer_names` 필드가 무시됩니다. 하지만 구버전 구현과의 호환성을 위해 설정해 두는 것이 좋습니다. -That's it, we're now ready to instantiate the logical device with a call to the -appropriately named `vkCreateDevice` function. +이제 `create_device` 함수를 호출하여 논리 장치를 인스턴스화할 수 있습니다. 이 함수는 FFI(Foreign Function Interface) 호출이므로 `unsafe` 블록 안에서 호출해야 합니다. -```c++ -if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) { - throw std::runtime_error("failed to create logical device!"); -} +```rust +let device = unsafe { + self.instance + .create_device(self.physical_device, &create_info, None) + .expect("Failed to create logical device!") +}; +self.device = Some(device); // Option 에 저장하거나 직접 할당 ``` -The parameters are the physical device to interface with, the queue and usage -info we just specified, the optional allocation callbacks pointer and a pointer -to a variable to store the logical device handle in. Similarly to the instance -creation function, this call can return errors based on enabling non-existent -extensions or specifying the desired usage of unsupported features. +`ash`에서는 `create_device`가 `Instance`의 메서드입니다. 파라미터는 물리 장치, 생성 정보, 그리고 선택적인 할당 콜백입니다. `expect`를 사용하여 오류 발생 시 프로그램을 패닉시킵니다. -The device should be destroyed in `cleanup` with the `vkDestroyDevice` function: +장치는 애플리케이션이 종료될 때 소멸되어야 합니다. Rust에서는 `Drop` 트레잇을 구현하여 이 작업을 자동화하는 것이 관례입니다. -```c++ -void cleanup() { - vkDestroyDevice(device, nullptr); - ... +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + // ... 다른 리소스 정리 ... + self.device.destroy_device(None); + // ... 인스턴스 정리 ... + } + } } ``` +논리 장치는 인스턴스와 직접 상호작용하지 않으므로 생성 시 인스턴스가 파라미터로 필요하지 않습니다. -Logical devices don't interact directly with instances, which is why it's not -included as a parameter. - -## Retrieving queue handles +## 큐 핸들 가져오기 -The queues are automatically created along with the logical device, but we don't -have a handle to interface with them yet. First add a class member to store a -handle to the graphics queue: +큐는 논리 장치와 함께 자동으로 생성되지만, 상호작용할 핸들이 아직 없습니다. 구조체에 그래픽스 큐 핸들을 저장할 필드를 추가합니다. -```c++ -VkQueue graphicsQueue; +```rust +struct HelloTriangleApplication { + // ... + device: Device, + graphics_queue: vk::Queue, +} ``` -Device queues are implicitly cleaned up when the device is destroyed, so we -don't need to do anything in `cleanup`. +장치 큐는 장치가 파괴될 때 암시적으로 정리되므로, `Drop` 트레잇에서 별도로 정리할 필요가 없습니다. -We can use the `vkGetDeviceQueue` function to retrieve queue handles for each -queue family. The parameters are the logical device, queue family, queue index -and a pointer to the variable to store the queue handle in. Because we're only -creating a single queue from this family, we'll simply use index `0`. +`get_device_queue` 함수를 사용하여 큐 핸들을 가져올 수 있습니다. 파라미터는 큐 패밀리 인덱스와 큐 인덱스입니다. 우리는 이 패밀리에서 큐를 하나만 생성했으므로 인덱스 `0`을 사용합니다. -```c++ -vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue); +```rust +// create_logical_device 메서드 마지막 부분 +let graphics_queue = unsafe { self.device.get_device_queue(indices.graphics_family.unwrap(), 0) }; +self.graphics_queue = graphics_queue; ``` -With the logical device and queue handles we can now actually start using the -graphics card to do things! In the next few chapters we'll set up the resources -to present results to the window system. +이제 논리 장치와 큐 핸들을 사용해 그래픽 카드로 작업을 시작할 준비가 되었습니다! 다음 장들에서는 결과를 창 시스템에 표시하기 위한 리소스를 설정할 것입니다. -[C++ code](/code/04_logical_device.cpp) +[Rust 코드 예시](https://github.com/bwasty/vulkan-tutorial-rs/blob/master/src/04_logical_device.rs) \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md b/ko-rust/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md index 966a8946..83566973 100644 --- a/ko-rust/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md +++ b/ko-rust/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md @@ -1,233 +1,199 @@ -Since Vulkan is a platform agnostic API, it can not interface directly with the -window system on its own. To establish the connection between Vulkan and the -window system to present results to the screen, we need to use the WSI (Window -System Integration) extensions. In this chapter we'll discuss the first one, -which is `VK_KHR_surface`. It exposes a `VkSurfaceKHR` object that represents an -abstract type of surface to present rendered images to. The surface in our -program will be backed by the window that we've already opened with GLFW. - -The `VK_KHR_surface` extension is an instance level extension and we've actually -already enabled it, because it's included in the list returned by -`glfwGetRequiredInstanceExtensions`. The list also includes some other WSI -extensions that we'll use in the next couple of chapters. - -The window surface needs to be created right after the instance creation, -because it can actually influence the physical device selection. The reason we -postponed this is because window surfaces are part of the larger topic of -render targets and presentation for which the explanation would have cluttered -the basic setup. It should also be noted that window surfaces are an entirely -optional component in Vulkan, if you just need off-screen rendering. Vulkan -allows you to do that without hacks like creating an invisible window -(necessary for OpenGL). - -## Window surface creation - -Start by adding a `surface` class member right below the debug callback. - -```c++ -VkSurfaceKHR surface; -``` +Vulkan은 플랫폼에 구애받지 않는 API이므로, 자체적으로 윈도우 시스템과 직접 통신할 수 없습니다. Vulkan과 윈도우 시스템을 연결하여 렌더링 결과를 화면에 표시하려면, WSI(Window System Integration) 확장을 사용해야 합니다. 이 장에서는 그 첫 번째 확장인 `VK_KHR_surface`에 대해 논의합니다. 이 확장은 렌더링된 이미지를 표시하기 위한 추상적인 표면 타입을 나타내는 `vk::SurfaceKHR` 객체를 제공합니다. 우리 프로그램의 서피스는 `winit`으로 이미 열어 둔 창을 기반으로 합니다. -Although the `VkSurfaceKHR` object and its usage is platform agnostic, its -creation isn't because it depends on window system details. For example, it -needs the `HWND` and `HMODULE` handles on Windows. Therefore there is a -platform-specific addition to the extension, which on Windows is called -`VK_KHR_win32_surface` and is also automatically included in the list from -`glfwGetRequiredInstanceExtensions`. - -I will demonstrate how this platform specific extension can be used to create a -surface on Windows, but we won't actually use it in this tutorial. It doesn't -make any sense to use a library like GLFW and then proceed to use -platform-specific code anyway. GLFW actually has `glfwCreateWindowSurface` that -handles the platform differences for us. Still, it's good to see what it does -behind the scenes before we start relying on it. - -To access native platform functions, you need to update the includes at the top: - -```c++ -#define VK_USE_PLATFORM_WIN32_KHR -#define GLFW_INCLUDE_VULKAN -#include -#define GLFW_EXPOSE_NATIVE_WIN32 -#include -``` +`VK_KHR_surface` 확장은 인스턴스 수준 확장(instance-level extension)이며, `ash_window::enumerate_required_extensions`가 반환하는 목록에 포함되어 있으므로 이미 활성화했습니다. 이 목록에는 다음 장들에서 사용할 다른 WSI 확장들도 포함됩니다. -Because a window surface is a Vulkan object, it comes with a -`VkWin32SurfaceCreateInfoKHR` struct that needs to be filled in. It has two -important parameters: `hwnd` and `hinstance`. These are the handles to the -window and the process. +윈도우 서피스는 물리 장치 선택에 영향을 줄 수 있으므로, 인스턴스 생성 직후에 만들어야 합니다. 이 과정을 뒤로 미룬 이유는 윈도우 서피스가 렌더 타겟 및 프레젠테이션이라는 더 큰 주제의 일부이며, 이를 기본 설정 과정에서 다루면 내용이 복잡해지기 때문입니다. 또한, 오프스크린 렌더링(off-screen rendering)만 필요하다면 윈도우 서피스는 전적으로 선택 사항입니다. Vulkan은 OpenGL처럼 보이지 않는 창을 만드는 꼼수 없이도 오프스크린 렌더링을 허용합니다. -```c++ -VkWin32SurfaceCreateInfoKHR createInfo{}; -createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; -createInfo.hwnd = glfwGetWin32Window(window); -createInfo.hinstance = GetModuleHandle(nullptr); -``` +## 윈도우 서피스 생성 -The `glfwGetWin32Window` function is used to get the raw `HWND` from the GLFW -window object. The `GetModuleHandle` call returns the `HINSTANCE` handle of the -current process. +먼저 메인 애플리케이션 구조체에 `surface_loader`와 `surface` 필드를 추가합니다. `ash`에서는 확장 기능 함수들을 사용하기 위해 해당 확장 로더(loader)가 필요합니다. -After that the surface can be created with `vkCreateWin32SurfaceKHR`, which includes a parameter for the instance, surface creation details, custom allocators and the variable for the surface handle to be stored in. Technically this is a WSI extension function, but it is so commonly used that the standard Vulkan loader includes it, so unlike other extensions you don't need to explicitly load it. +```rust +use ash::extensions::khr; -```c++ -if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) { - throw std::runtime_error("failed to create window surface!"); +struct VulkanApp { + // ... 기존 필드들 + surface_loader: khr::Surface, + surface: vk::SurfaceKHR, + // ... } ``` -The process is similar for other platforms like Linux, where -`vkCreateXcbSurfaceKHR` takes an XCB connection and window as creation details -with X11. - -The `glfwCreateWindowSurface` function performs exactly this operation with a -different implementation for each platform. We'll now integrate it into our -program. Add a function `createSurface` to be called from `initVulkan` right -after instance creation and `setupDebugMessenger`. - -```c++ -void initVulkan() { - createInstance(); - setupDebugMessenger(); - createSurface(); - pickPhysicalDevice(); - createLogicalDevice(); +`vk::SurfaceKHR` 객체와 그 사용법은 플랫폼에 독립적이지만, 생성 과정은 윈도우 시스템 세부 사항에 따라 달라지므로 플랫폼 종속적입니다. 예를 들어, Windows에서는 `HWND`와 `HMODULE` 핸들이 필요합니다. 따라서 플랫폼별 추가 확장이 있으며, Windows용으로는 `VK_KHR_win32_surface`가 있습니다. + +이 플랫폼별 확장을 사용하여 Windows에서 서피스를 만드는 방법을 보여드리겠지만, 이 튜토리얼에서 실제로 사용하지는 않을 것입니다. `winit`과 같은 라이브러리를 사용하면서 플랫폼 종속 코드를 직접 사용하는 것은 바람직하지 않습니다. 다행히 `ash-window` 크레이트가 플랫폼 간 차이를 추상화해줍니다. 그럼에도 불구하고, 라이브러리에 의존하기 전에 내부적으로 어떤 일이 일어나는지 살펴보는 것은 좋습니다. + +네이티브 플랫폼 함수에 접근하려면 `raw-window-handle` 크레이트가 필요합니다. + +```rust +use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; +use ash::extensions::khr::Win32Surface; + +// 이 코드는 시연용이며 실제로는 사용하지 않습니다. +fn create_surface_natively( + entry: &ash::Entry, + instance: &ash::Instance, + window: &winit::window::Window, +) -> vk::SurfaceKHR { + let handle = window.raw_window_handle(); + if let RawWindowHandle::Win32(win_handle) = handle { + let surface_info = vk::Win32SurfaceCreateInfoKHR::builder() + .hinstance(win_handle.hinstance) + .hwnd(win_handle.hwnd); + + let win32_surface_loader = Win32Surface::new(entry, instance); + unsafe { + win32_surface_loader + .create_win32_surface(&surface_info, None) + .expect("Failed to create Win32 surface.") + } + } else { + panic!("Unsupported window handle type"); + } } +``` -void createSurface() { +`ash-window` 크레이트의 `create_surface` 함수는 각 플랫폼에 맞춰 정확히 이 작업을 수행합니다. 이제 이 함수를 우리 프로그램에 통합해 보겠습니다. `init_vulkan` 함수에서 인스턴스 생성과 디버그 메신저 설정 직후에 호출될 `create_surface` 메서드를 추가합니다. -} -``` +```rust +impl VulkanApp { + pub fn init_vulkan(&mut self, window: &winit::window::Window) { + self.create_instance(window); + self.setup_debug_messenger(); + self.create_surface(window); + self.pick_physical_device(); + self.create_logical_device(); + } -The GLFW call takes simple parameters instead of a struct which makes the -implementation of the function very straightforward: + fn create_surface(&mut self, window: &winit::window::Window) { + // 서피스 확장 로더 생성 + self.surface_loader = khr::Surface::new(&self.entry, &self.instance); -```c++ -void createSurface() { - if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) { - throw std::runtime_error("failed to create window surface!"); + // 서피스 생성 + self.surface = unsafe { + ash_window::create_surface(&self.entry, &self.instance, window, None) + .expect("Failed to create window surface") + }; } } ``` -The parameters are the `VkInstance`, GLFW window pointer, custom allocators and -pointer to `VkSurfaceKHR` variable. It simply passes through the `VkResult` from -the relevant platform call. GLFW doesn't offer a special function for destroying -a surface, but that can easily be done through the original API: - -```c++ -void cleanup() { - ... - vkDestroySurfaceKHR(instance, surface, nullptr); - vkDestroyInstance(instance, nullptr); - ... +`ash_window::create_surface`는 `ash::Entry`, `ash::Instance`, `winit` 윈도우 객체, 그리고 커스텀 할당자를 인자로 받습니다. 이 함수는 내부적으로 플랫폼에 맞는 함수를 호출하고 그 결과를 반환합니다. + +서피스를 정리할 때는 원본 Vulkan API를 사용합니다. 서피스는 반드시 인스턴스보다 먼저 파괴되어야 합니다. + +```rust +impl Drop for VulkanApp { + fn drop(&mut self) { + unsafe { + // ... 다른 객체들 정리 + self.surface_loader.destroy_surface(self.surface, None); + self.instance.destroy_instance(None); + } } +} ``` -Make sure that the surface is destroyed before the instance. - -## Querying for presentation support +## 프레젠테이션 지원 여부 쿼리 -Although the Vulkan implementation may support window system integration, that -does not mean that every device in the system supports it. Therefore we need to -extend `isDeviceSuitable` to ensure that a device can present images to the -surface we created. Since the presentation is a queue-specific feature, the -problem is actually about finding a queue family that supports presenting to the -surface we created. +Vulkan 구현이 윈도우 시스템 통합을 지원하더라도, 시스템의 모든 장치가 이를 지원하는 것은 아닙니다. 따라서 `is_device_suitable`을 확장하여 장치가 우리가 생성한 서피스로 이미지를 출력(present)할 수 있는지 확인해야 합니다. 프레젠테이션은 큐에 특화된 기능이므로, 이 문제는 결국 우리가 만든 서피스로의 프레젠테이션을 지원하는 큐 패밀리를 찾는 문제가 됩니다. -It's actually possible that the queue families supporting drawing commands and -the ones supporting presentation do not overlap. Therefore we have to take into -account that there could be a distinct presentation queue by modifying the -`QueueFamilyIndices` structure: +드로잉 커맨드를 지원하는 큐 패밀리와 프레젠테이션을 지원하는 큐 패밀리가 다를 수 있습니다. 따라서 별도의 프레젠테이션 큐가 존재할 수 있다는 점을 고려하여 `QueueFamilyIndices` 구조체를 수정합니다. -```c++ +```rust +#[derive(Default)] struct QueueFamilyIndices { - std::optional graphicsFamily; - std::optional presentFamily; + graphics_family: Option, + present_family: Option, +} - bool isComplete() { - return graphicsFamily.has_value() && presentFamily.has_value(); +impl QueueFamilyIndices { + fn is_complete(&self) -> bool { + self.graphics_family.is_some() && self.present_family.is_some() } -}; +} ``` -Next, we'll modify the `findQueueFamilies` function to look for a queue family -that has the capability of presenting to our window surface. The function to -check for that is `vkGetPhysicalDeviceSurfaceSupportKHR`, which takes the -physical device, queue family index and surface as parameters. Add a call to it -in the same loop as the `VK_QUEUE_GRAPHICS_BIT`: +다음으로, `find_queue_families` 함수를 수정하여 우리 윈도우 서피스로 프레젠테이션할 수 있는 큐 패밀리를 찾도록 합니다. 이를 확인하는 함수는 `get_physical_device_surface_support`이며, `surface_loader`를 통해 호출합니다. -```c++ -VkBool32 presentSupport = false; -vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport); -``` +```rust +// find_queue_families 메서드 내부의 루프 +for (i, queue_family) in queue_families.iter().enumerate() { + let i = i as u32; + + if queue_family.queue_flags.contains(vk::QueueFlags::GRAPHICS) { + indices.graphics_family = Some(i); + } + + let present_support = unsafe { + self.surface_loader + .get_physical_device_surface_support(device, i, self.surface) + .unwrap_or(false) + }; -Then simply check the value of the boolean and store the presentation family -queue index: + if present_support { + indices.present_family = Some(i); + } -```c++ -if (presentSupport) { - indices.presentFamily = i; + if indices.is_complete() { + break; + } } ``` -Note that it's very likely that these end up being the same queue family after -all, but throughout the program we will treat them as if they were separate -queues for a uniform approach. Nevertheless, you could add logic to explicitly -prefer a physical device that supports drawing and presentation in the same -queue for improved performance. +두 큐 패밀리가 결국 동일한 큐 패밀리로 결정될 가능성이 높지만, 프로그램 전반에 걸쳐 일관된 접근을 위해 별개의 큐인 것처럼 다룰 것입니다. 물론, 성능 향상을 위해 드로잉과 프레젠테이션을 동일한 큐에서 지원하는 물리 장치를 선호하도록 로직을 추가할 수도 있습니다. -## Creating the presentation queue +## 프레젠테이션 큐 생성하기 -The one thing that remains is modifying the logical device creation procedure to -create the presentation queue and retrieve the `VkQueue` handle. Add a member -variable for the handle: +이제 남은 일은 논리 장치 생성 절차를 수정하여 프레젠테이션 큐를 만들고 `vk::Queue` 핸들을 가져오는 것입니다. 구조체에 핸들을 저장할 필드를 추가합니다. -```c++ -VkQueue presentQueue; +```rust +struct VulkanApp { + // ... + present_queue: vk::Queue, + // ... +} ``` -Next, we need to have multiple `VkDeviceQueueCreateInfo` structs to create a -queue from both families. An elegant way to do that is to create a set of all -unique queue families that are necessary for the required queues: +다음으로, 두 큐 패밀리로부터 큐를 생성하기 위해 여러 개의 `vk::DeviceQueueCreateInfo` 구조체가 필요할 수 있습니다. 이를 Rust답게 처리하는 방법은 `HashSet`을 사용하여 필요한 큐 패밀리의 고유한 인덱스 집합을 만드는 것입니다. -```c++ -#include +```rust +use std::collections::HashSet; -... +// create_logical_device 메서드 내부 +let indices = self.find_queue_families(self.physical_device); -QueueFamilyIndices indices = findQueueFamilies(physicalDevice); +let mut unique_queue_families = HashSet::new(); +unique_queue_families.insert(indices.graphics_family.unwrap()); +unique_queue_families.insert(indices.present_family.unwrap()); -std::vector queueCreateInfos; -std::set uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()}; - -float queuePriority = 1.0f; -for (uint32_t queueFamily : uniqueQueueFamilies) { - VkDeviceQueueCreateInfo queueCreateInfo{}; - queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; - queueCreateInfo.queueFamilyIndex = queueFamily; - queueCreateInfo.queueCount = 1; - queueCreateInfo.pQueuePriorities = &queuePriority; - queueCreateInfos.push_back(queueCreateInfo); -} +let queue_priority = [1.0]; +let queue_create_infos: Vec<_> = unique_queue_families + .iter() + .map(|&queue_family_index| { + vk::DeviceQueueCreateInfo::builder() + .queue_family_index(queue_family_index) + .queue_priorities(&queue_priority) + .build() + }) + .collect(); ``` -And modify `VkDeviceCreateInfo` to point to the vector: +그리고 `vk::DeviceCreateInfo`가 이 `Vec`을 가리키도록 수정합니다. -```c++ -createInfo.queueCreateInfoCount = static_cast(queueCreateInfos.size()); -createInfo.pQueueCreateInfos = queueCreateInfos.data(); +```rust +let device_create_info = vk::DeviceCreateInfo::builder() + .queue_create_infos(&queue_create_infos) + // ... 다른 빌더 호출들 + .build(); ``` -If the queue families are the same, then we only need to pass its index once. -Finally, add a call to retrieve the queue handle: +만약 큐 패밀리가 같다면, `HashSet` 덕분에 해당 인덱스에 대한 정보는 한 번만 전달됩니다. 마지막으로, 프레젠테이션 큐 핸들을 가져오는 호출을 추가합니다. -```c++ -vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue); +```rust +// 논리 장치 생성 후 +self.graphics_queue = unsafe { self.device.get_device_queue(indices.graphics_family.unwrap(), 0) }; +self.present_queue = unsafe { self.device.get_device_queue(indices.present_family.unwrap(), 0) }; ``` -In case the queue families are the same, the two handles will most likely have -the same value now. In the next chapter we're going to look at swap chains and -how they give us the ability to present images to the surface. - -[C++ code](/code/05_window_surface.cpp) +만약 두 큐 패밀리가 같다면, `graphics_queue`와 `present_queue` 핸들은 대부분 같은 값을 갖게 됩니다. 다음 장에서는 스왑 체인(swap chain)을 살펴보고, 이를 통해 어떻게 서피스에 이미지를 출력하는지 알아보겠습니다. \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md b/ko-rust/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md index f593b5a6..a3eb419f 100644 --- a/ko-rust/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md +++ b/ko-rust/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md @@ -1,603 +1,352 @@ -Vulkan does not have the concept of a "default framebuffer", hence it requires an infrastructure that will own the buffers we will render to before we visualize them on the screen. This infrastructure is -known as the *swap chain* and must be created explicitly in Vulkan. The swap -chain is essentially a queue of images that are waiting to be presented to the -screen. Our application will acquire such an image to draw to it, and then -return it to the queue. How exactly the queue works and the conditions for -presenting an image from the queue depend on how the swap chain is set up, but -the general purpose of the swap chain is to synchronize the presentation of -images with the refresh rate of the screen. - -## Checking for swap chain support - -Not all graphics cards are capable of presenting images directly to a screen for -various reasons, for example because they are designed for servers and don't -have any display outputs. Secondly, since image presentation is heavily tied -into the window system and the surfaces associated with windows, it is not -actually part of the Vulkan core. You have to enable the `VK_KHR_swapchain` -device extension after querying for its support. - -For that purpose we'll first extend the `isDeviceSuitable` function to check if -this extension is supported. We've previously seen how to list the extensions -that are supported by a `VkPhysicalDevice`, so doing that should be fairly -straightforward. Note that the Vulkan header file provides a nice macro -`VK_KHR_SWAPCHAIN_EXTENSION_NAME` that is defined as `VK_KHR_swapchain`. The -advantage of using this macro is that the compiler will catch misspellings. - -First declare a list of required device extensions, similar to the list of -validation layers to enable. - -```c++ -const std::vector deviceExtensions = { - VK_KHR_SWAPCHAIN_EXTENSION_NAME -}; -``` +Vulkan에는 '기본 프레임버퍼(default framebuffer)'라는 개념이 없습니다. 따라서 우리가 렌더링할 버퍼를 화면에 시각화하기 전에 소유할 인프라가 필요합니다. 이 인프라를 **스왑 체인(swap chain)** 이라고 하며, Vulkan에서는 명시적으로 생성해야 합니다. -Next, create a new function `checkDeviceExtensionSupport` that is called from -`isDeviceSuitable` as an additional check: +스왑 체인은 본질적으로 화면에 표시되기를 기다리는 이미지들의 큐(queue)입니다. 우리 애플리케이션은 렌더링할 이미지를 이 큐에서 가져와(acquire) 렌더링한 다음, 다시 큐에 반환합니다. 큐가 정확히 어떻게 작동하고 큐에서 이미지를 표시하는 조건은 스왑 체인 설정 방식에 따라 다르지만, 스왑 체인의 일반적인 목적은 이미지 표시를 화면의 주사율(refresh rate)과 동기화하는 것입니다. -```c++ -bool isDeviceSuitable(VkPhysicalDevice device) { - QueueFamilyIndices indices = findQueueFamilies(device); +## 스왑 체인 지원 확인 - bool extensionsSupported = checkDeviceExtensionSupport(device); - - return indices.isComplete() && extensionsSupported; -} - -bool checkDeviceExtensionSupport(VkPhysicalDevice device) { - return true; -} -``` +모든 그래픽 카드가 이미지를 화면에 직접 표시할 수 있는 것은 아닙니다. 예를 들어 서버용으로 설계되어 디스플레이 출력이 없는 경우가 그렇습니다. 둘째로, 이미지 표시는 창 시스템(window system) 및 창과 관련된 표면(surface)과 밀접하게 연관되어 있으므로 실제 Vulkan 코어의 일부가 아닙니다. 따라서 `VK_KHR_swapchain` 장치 확장 기능의 지원 여부를 쿼리한 후 활성화해야 합니다. -Modify the body of the function to enumerate the extensions and check if all of -the required extensions are amongst them. +이를 위해 먼저 `is_device_suitable` 함수를 확장하여 이 확장이 지원되는지 확인합니다. `ash` 라이브러리는 `ash::extensions::khr::Swapchain::name()`과 같이 확장 기능의 이름을 안전하게 가져올 수 있는 상수를 제공하여 오타를 방지합니다. -```c++ -bool checkDeviceExtensionSupport(VkPhysicalDevice device) { - uint32_t extensionCount; - vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr); +먼저, 필요한 장치 확장 기능 목록을 상수로 정의합니다. `CStr` 타입을 사용하여 C 문자열과의 호환성을 보장합니다. - std::vector availableExtensions(extensionCount); - vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data()); - - std::set requiredExtensions(deviceExtensions.begin(), deviceExtensions.end()); - - for (const auto& extension : availableExtensions) { - requiredExtensions.erase(extension.extensionName); - } - - return requiredExtensions.empty(); -} -``` +```rust +use std::ffi::CStr; +// ... 다른 use 구문들 -I've chosen to use a set of strings here to represent the unconfirmed required -extensions. That way we can easily tick them off while enumerating the sequence -of available extensions. Of course you can also use a nested loop like in -`checkValidationLayerSupport`. The performance difference is irrelevant. Now run -the code and verify that your graphics card is indeed capable of creating a -swap chain. It should be noted that the availability of a presentation queue, -as we checked in the previous chapter, implies that the swap chain extension -must be supported. However, it's still good to be explicit about things, and -the extension does have to be explicitly enabled. - -## Enabling device extensions - -Using a swapchain requires enabling the `VK_KHR_swapchain` extension first. -Enabling the extension just requires a small change to the logical device -creation structure: - -```c++ -createInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); -createInfo.ppEnabledExtensionNames = deviceExtensions.data(); +const DEVICE_EXTENSIONS: [&'static CStr; 1] = [ash::extensions::khr::Swapchain::name()]; ``` -Make sure to replace the existing line `createInfo.enabledExtensionCount = 0;` when you do so. +다음으로, `is_device_suitable`에서 추가 검사로 호출될 새로운 함수 `check_device_extension_support`를 만듭니다. -## Querying details of swap chain support +```rust +// is_device_suitable 함수 내부에서... +let extensions_supported = + check_device_extension_support(&self.instance, physical_device); -Just checking if a swap chain is available is not sufficient, because it may not -actually be compatible with our window surface. Creating a swap chain also -involves a lot more settings than instance and device creation, so we need to -query for some more details before we're able to proceed. +// ... -There are basically three kinds of properties we need to check: - -* Basic surface capabilities (min/max number of images in swap chain, min/max -width and height of images) -* Surface formats (pixel format, color space) -* Available presentation modes - -Similar to `findQueueFamilies`, we'll use a struct to pass these details around -once they've been queried. The three aforementioned types of properties come in -the form of the following structs and lists of structs: - -```c++ -struct SwapChainSupportDetails { - VkSurfaceCapabilitiesKHR capabilities; - std::vector formats; - std::vector presentModes; -}; +indices.is_complete() && extensions_supported ``` -We'll now create a new function `querySwapChainSupport` that will populate this -struct. - -```c++ -SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) { - SwapChainSupportDetails details; - - return details; -} -``` - -This section covers how to query the structs that include this information. The -meaning of these structs and exactly which data they contain is discussed in the -next section. - -Let's start with the basic surface capabilities. These properties are simple to -query and are returned into a single `VkSurfaceCapabilitiesKHR` struct. - -```c++ -vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities); -``` +```rust +use std::collections::HashSet; +use ash::Instance; +use ash::vk; -This function takes the specified `VkPhysicalDevice` and `VkSurfaceKHR` window -surface into account when determining the supported capabilities. All of the -support querying functions have these two as first parameters because they are -the core components of the swap chain. +fn check_device_extension_support( + instance: &Instance, + physical_device: vk::PhysicalDevice, +) -> bool { + // 필요한 확장 기능들을 HashSet으로 변환합니다. + let mut required_extensions = HashSet::from_iter(DEVICE_EXTENSIONS.iter().map(|s| *s)); -The next step is about querying the supported surface formats. Because this is a -list of structs, it follows the familiar ritual of 2 function calls: + // 장치가 지원하는 확장 기능들을 가져옵니다. + // ash는 C++의 2중 호출 패턴 대신 Result>를 바로 반환해 편리합니다. + let available_extensions = unsafe { + instance + .enumerate_device_extension_properties(physical_device) + .expect("Failed to enumerate device extension properties.") + }; -```c++ -uint32_t formatCount; -vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr); + // 사용 가능한 확장 기능들을 순회하며 필요한 확장 기능 목록에서 제거합니다. + for extension in available_extensions.iter() { + // C 스타일의 char 배열을 CStr로 변환합니다. + let extension_name = unsafe { CStr::from_ptr(extension.extension_name.as_ptr()) }; + required_extensions.remove(extension_name); + } -if (formatCount != 0) { - details.formats.resize(formatCount); - vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data()); + // 모든 필요한 확장 기능이 제거되었다면 지원하는 것입니다. + required_extensions.is_empty() } ``` -Make sure that the vector is resized to hold all the available formats. And -finally, querying the supported presentation modes works exactly the same way -with `vkGetPhysicalDeviceSurfacePresentModesKHR`: +Rust에서는 C++의 `std::set` 대신 `std::collections::HashSet`을 사용했습니다. 로직은 동일합니다. 사용 가능한 확장을 순회하면서 필요한 확장 목록에서 하나씩 제거하고, 최종적으로 목록이 비었는지 확인합니다. 이제 코드를 실행하여 그래픽 카드가 실제로 스왑 체인을 생성할 수 있는지 확인하십시오. 이전 장에서 확인했던 표현 큐(presentation queue)의 가용성은 스왑 체인 확장이 지원되어야 함을 의미하지만, 명시적으로 확인하고 활성화하는 것이 좋습니다. -```c++ -uint32_t presentModeCount; -vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr); +## 장치 확장 기능 활성화 -if (presentModeCount != 0) { - details.presentModes.resize(presentModeCount); - vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data()); -} -``` +스왑 체인을 사용하려면 먼저 `VK_KHR_swapchain` 확장을 활성화해야 합니다. 논리 장치를 생성할 때 `ash`의 빌더 패턴을 사용하여 활성화할 수 있습니다. -All of the details are in the struct now, so let's extend `isDeviceSuitable` -once more to utilize this function to verify that swap chain support is -adequate. Swap chain support is sufficient for this tutorial if there is at -least one supported image format and one supported presentation mode given the -window surface we have. - -```c++ -bool swapChainAdequate = false; -if (extensionsSupported) { - SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); - swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); -} -``` - -It is important that we only try to query for swap chain support after verifying -that the extension is available. The last line of the function changes to: +```rust +// CStr 슬라이스에서 C-호환 포인터 슬라이스를 만듭니다. +let extension_names_raw: Vec<*const i8> = DEVICE_EXTENSIONS + .iter() + .map(|s| s.as_ptr()) + .collect(); -```c++ -return indices.isComplete() && extensionsSupported && swapChainAdequate; +let device_create_info = vk::DeviceCreateInfo::builder() + .queue_create_infos(&queue_create_infos) + .enabled_features(&device_features) + .enabled_extension_names(&extension_names_raw); // 여기서 확장 기능을 지정합니다. ``` -## Choosing the right settings for the swap chain - -If the `swapChainAdequate` conditions were met then the support is definitely -sufficient, but there may still be many different modes of varying optimality. -We'll now write a couple of functions to find the right settings for the best -possible swap chain. There are three types of settings to determine: - -* Surface format (color depth) -* Presentation mode (conditions for "swapping" images to the screen) -* Swap extent (resolution of images in swap chain) - -For each of these settings we'll have an ideal value in mind that we'll go with -if it's available and otherwise we'll create some logic to find the next best -thing. - -### Surface format - -The function for this setting starts out like this. We'll later pass the -`formats` member of the `SwapChainSupportDetails` struct as argument. - -```c++ -VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { - -} -``` +## 스왑 체인 지원 상세 정보 쿼리 -Each `VkSurfaceFormatKHR` entry contains a `format` and a `colorSpace` member. The -`format` member specifies the color channels and types. For example, -`VK_FORMAT_B8G8R8A8_SRGB` means that we store the B, G, R and alpha channels in -that order with an 8 bit unsigned integer for a total of 32 bits per pixel. The -`colorSpace` member indicates if the SRGB color space is supported or not using -the `VK_COLOR_SPACE_SRGB_NONLINEAR_KHR` flag. Note that this flag used to be -called `VK_COLORSPACE_SRGB_NONLINEAR_KHR` in old versions of the specification. +스왑 체인이 사용 가능한지 확인하는 것만으로는 충분하지 않습니다. 스왑 체인이 우리 창 표면(window surface)과 호환되지 않을 수 있기 때문입니다. 이제 스왑 체인 생성에 필요한 세부 정보들을 쿼리해야 합니다. -For the color space we'll use SRGB if it is available, because it [results in more accurate perceived colors](http://stackoverflow.com/questions/12524623/). It is also pretty much the standard color space for images, like the textures we'll use later on. -Because of that we should also use an SRGB color format, of which one of the most common ones is `VK_FORMAT_B8G8R8A8_SRGB`. +* 기본 표면 기능 (스왑 체인의 최소/최대 이미지 수, 이미지의 최소/최대 너비 및 높이) +* 표면 형식 (픽셀 형식, 색 공간) +* 사용 가능한 표현 모드 -Let's go through the list and see if the preferred combination is available: +이 정보들을 담을 구조체를 정의합니다. C++ 버전과 유사하지만 Rust 스타일을 따릅니다. -```c++ -for (const auto& availableFormat : availableFormats) { - if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { - return availableFormat; - } +```rust +struct SwapChainSupportDetails { + capabilities: vk::SurfaceCapabilitiesKHR, + formats: Vec, + present_modes: Vec, } ``` -If that also fails then we could start ranking the available formats based on -how "good" they are, but in most cases it's okay to just settle with the first -format that is specified. - -```c++ -VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { - for (const auto& availableFormat : availableFormats) { - if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { - return availableFormat; +이 구조체를 채울 `query_swapchain_support` 함수를 만듭니다. `ash`에서는 `Surface` 로더를 사용해야 합니다. + +```rust +// 주 애플리케이션 구조체에 surface_loader 필드가 있어야 합니다. +// self.surface_loader: ash::extensions::khr::Surface + +fn query_swapchain_support( + physical_device: vk::PhysicalDevice, + surface_loader: &ash::extensions::khr::Surface, + surface: vk::SurfaceKHR, +) -> SwapChainSupportDetails { + unsafe { + // capabilities 쿼리 + let capabilities = surface_loader + .get_physical_device_surface_capabilities(physical_device, surface) + .expect("Failed to query for surface capabilities."); + + // formats 쿼리 + let formats = surface_loader + .get_physical_device_surface_formats(physical_device, surface) + .expect("Failed to query for surface formats."); + + // present_modes 쿼리 + let present_modes = surface_loader + .get_physical_device_surface_present_modes(physical_device, surface) + .expect("Failed to query for surface present modes."); + + SwapChainSupportDetails { + capabilities, + formats, + present_modes, } } - - return availableFormats[0]; } ``` -### Presentation mode - -The presentation mode is arguably the most important setting for the swap chain, -because it represents the actual conditions for showing images to the screen. -There are four possible modes available in Vulkan: - -* `VK_PRESENT_MODE_IMMEDIATE_KHR`: Images submitted by your application are -transferred to the screen right away, which may result in tearing. -* `VK_PRESENT_MODE_FIFO_KHR`: The swap chain is a queue where the display takes -an image from the front of the queue when the display is refreshed and the -program inserts rendered images at the back of the queue. If the queue is full -then the program has to wait. This is most similar to vertical sync as found in -modern games. The moment that the display is refreshed is known as "vertical -blank". -* `VK_PRESENT_MODE_FIFO_RELAXED_KHR`: This mode only differs from the previous -one if the application is late and the queue was empty at the last vertical -blank. Instead of waiting for the next vertical blank, the image is transferred -right away when it finally arrives. This may result in visible tearing. -* `VK_PRESENT_MODE_MAILBOX_KHR`: This is another variation of the second mode. -Instead of blocking the application when the queue is full, the images that are -already queued are simply replaced with the newer ones. This mode can be used to -render frames as fast as possible while still avoiding tearing, resulting in fewer latency issues than standard vertical sync. This is commonly known as "triple buffering", although the existence of three buffers alone does not necessarily mean that the framerate is unlocked. - -Only the `VK_PRESENT_MODE_FIFO_KHR` mode is guaranteed to be available, so we'll -again have to write a function that looks for the best mode that is available: - -```c++ -VkPresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { - return VK_PRESENT_MODE_FIFO_KHR; -} -``` - -I personally think that `VK_PRESENT_MODE_MAILBOX_KHR` is a very nice trade-off if energy usage is not a concern. It allows us to avoid tearing while still maintaining a fairly low latency by rendering new images that are as up-to-date as possible right until the vertical blank. On mobile devices, where energy usage is more important, you will probably want to use `VK_PRESENT_MODE_FIFO_KHR` instead. Now, let's look through the list to see if `VK_PRESENT_MODE_MAILBOX_KHR` is available: - -```c++ -VkPresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { - for (const auto& availablePresentMode : availablePresentModes) { - if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) { - return availablePresentMode; - } - } +이제 `is_device_suitable` 함수를 다시 수정하여, 확장 기능 지원이 확인된 후에 스왑 체인 지원이 충분한지 확인합니다. 형식이 하나 이상, 표현 모드가 하나 이상이면 충분하다고 간주합니다. - return VK_PRESENT_MODE_FIFO_KHR; +```rust +// is_device_suitable 함수 내부에서... +if extensions_supported { + let swapchain_support = query_swapchain_support(physical_device, &self.surface_loader, self.surface); + let swapchain_adequate = !swapchain_support.formats.is_empty() + && !swapchain_support.present_modes.is_empty(); + + indices.is_complete() && swapchain_adequate +} else { + false } ``` -### Swap extent +## 스왑 체인에 적합한 설정 선택하기 -That leaves only one major property, for which we'll add one last function: +`swapchain_adequate` 조건이 충족되었다면, 이제 사용 가능한 옵션 중에서 최적의 설정을 선택해야 합니다. -```c++ -VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) { +### 표면 형식 -} -``` - -The swap extent is the resolution of the swap chain images and it's almost -always exactly equal to the resolution of the window that we're drawing to _in -pixels_ (more on that in a moment). The range of the possible resolutions is -defined in the `VkSurfaceCapabilitiesKHR` structure. Vulkan tells us to match -the resolution of the window by setting the width and height in the -`currentExtent` member. However, some window managers do allow us to differ here -and this is indicated by setting the width and height in `currentExtent` to a -special value: the maximum value of `uint32_t`. In that case we'll pick the -resolution that best matches the window within the `minImageExtent` and -`maxImageExtent` bounds. But we must specify the resolution in the correct unit. - -GLFW uses two units when measuring sizes: pixels and -[screen coordinates](https://www.glfw.org/docs/latest/intro_guide.html#coordinate_systems). -For example, the resolution `{WIDTH, HEIGHT}` that we specified earlier when -creating the window is measured in screen coordinates. But Vulkan works with -pixels, so the swap chain extent must be specified in pixels as well. -Unfortunately, if you are using a high DPI display (like Apple's Retina -display), screen coordinates don't correspond to pixels. Instead, due to the -higher pixel density, the resolution of the window in pixel will be larger than -the resolution in screen coordinates. So if Vulkan doesn't fix the swap extent -for us, we can't just use the original `{WIDTH, HEIGHT}`. Instead, we must use -`glfwGetFramebufferSize` to query the resolution of the window in pixel before -matching it against the minimum and maximum image extent. - -```c++ -#include // Necessary for uint32_t -#include // Necessary for std::numeric_limits -#include // Necessary for std::clamp - -... - -VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) { - if (capabilities.currentExtent.width != std::numeric_limits::max()) { - return capabilities.currentExtent; - } else { - int width, height; - glfwGetFramebufferSize(window, &width, &height); - - VkExtent2D actualExtent = { - static_cast(width), - static_cast(height) - }; - - actualExtent.width = std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); - actualExtent.height = std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); +가장 이상적인 형식은 `B8G8R8A8_SRGB` 형식과 `SRGB_NONLINEAR` 색 공간의 조합입니다. 이 조합을 우선적으로 찾고, 없다면 첫 번째 사용 가능한 형식을 선택합니다. - return actualExtent; - } +```rust +fn choose_swap_surface_format( + available_formats: &[vk::SurfaceFormatKHR], +) -> vk::SurfaceFormatKHR { + available_formats + .iter() + .find(|format| { + format.format == vk::Format::B8G8R8A8_SRGB + && format.color_space == vk::ColorSpaceKHR::SRGB_NONLINEAR + }) + .map(|format| *format) // find는 &T를 반환하므로 복사 + .unwrap_or(available_formats[0]) } ``` +Rust의 이터레이터 메서드(`find`, `map`)를 사용하면 더 간결하고 표현력 있는 코드를 작성할 수 있습니다. -The `clamp` function is used here to bound the values of `width` and `height` between the allowed minimum and maximum extents that are supported by the implementation. - -## Creating the swap chain - -Now that we have all of these helper functions assisting us with the choices we -have to make at runtime, we finally have all the information that is needed to -create a working swap chain. +### 표현 모드 -Create a `createSwapChain` function that starts out with the results of these -calls and make sure to call it from `initVulkan` after logical device creation. +표현 모드는 스왑 체인의 동작을 결정하는 가장 중요한 설정입니다. 네 가지 모드가 있습니다: -```c++ -void initVulkan() { - createInstance(); - setupDebugMessenger(); - createSurface(); - pickPhysicalDevice(); - createLogicalDevice(); - createSwapChain(); -} +* `IMMEDIATE`: 즉시 표시 (티어링 가능성 있음) +* `FIFO`: 수직 동기화와 유사한 큐 방식 (보장된 가용성) +* `FIFO_RELAXED`: `FIFO`의 변형으로, 큐가 비었을 때 지연 없이 표시 (티어링 가능성 있음) +* `MAILBOX`: 삼중 버퍼링과 유사. 큐가 가득 차면 새 이미지로 교체 (낮은 지연 시간, 티어링 없음) -void createSwapChain() { - SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice); +`MAILBOX` 모드가 성능과 품질 면에서 훌륭한 절충안이므로 우선적으로 선택하고, 없다면 보장된 `FIFO` 모드를 사용합니다. - VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats); - VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes); - VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities); +```rust +fn choose_swap_present_mode( + available_present_modes: &[vk::PresentModeKHR], +) -> vk::PresentModeKHR { + available_present_modes + .iter() + .find(|&&mode| mode == vk::PresentModeKHR::MAILBOX) + .map(|mode| *mode) + .unwrap_or(vk::PresentModeKHR::FIFO) } ``` -Aside from these properties we also have to decide how many images we would like to have in the swap chain. The implementation specifies the minimum number that it requires to function: - -```c++ -uint32_t imageCount = swapChainSupport.capabilities.minImageCount; -``` - -However, simply sticking to this minimum means that we may sometimes have to wait on the driver to complete internal operations before we can acquire another image to render to. Therefore it is recommended to request at least one more image than the minimum: +### 스왑 범위 -```c++ -uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1; -``` +스왑 범위는 스왑 체인 이미지의 해상도이며, 보통 창의 픽셀 단위 해상도와 같습니다. -We should also make sure to not exceed the maximum number of images while doing this, where `0` is a special value that means that there is no maximum: +창 관리자가 해상도를 정해주는 경우(`current_extent`의 너비가 `u32::MAX`가 아닌 경우), 그 값을 그대로 사용합니다. 그렇지 않으면, `winit` (또는 사용하는 창 라이브러리)에서 프레임버퍼의 픽셀 크기를 직접 쿼리하여 사용해야 합니다. 고해상도(HiDPI) 디스플레이에서는 창의 논리적 크기와 픽셀 크기가 다를 수 있기 때문입니다. -```c++ -if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) { - imageCount = swapChainSupport.capabilities.maxImageCount; -} -``` - -As is tradition with Vulkan objects, creating the swap chain object requires -filling in a large structure. It starts out very familiarly: - -```c++ -VkSwapchainCreateInfoKHR createInfo{}; -createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; -createInfo.surface = surface; -``` +```rust +use winit::window::Window; -After specifying which surface the swap chain should be tied to, the details of -the swap chain images are specified: +fn choose_swap_extent( + capabilities: &vk::SurfaceCapabilitiesKHR, + window: &Window, +) -> vk::Extent2D { + if capabilities.current_extent.width != u32::MAX { + capabilities.current_extent + } else { + let framebuffer_size = window.inner_size(); // winit 0.28+ 에서는 inner_size()가 픽셀 단위 -```c++ -createInfo.minImageCount = imageCount; -createInfo.imageFormat = surfaceFormat.format; -createInfo.imageColorSpace = surfaceFormat.colorSpace; -createInfo.imageExtent = extent; -createInfo.imageArrayLayers = 1; -createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; -``` + let mut actual_extent = vk::Extent2D { + width: framebuffer_size.width, + height: framebuffer_size.height, + }; -The `imageArrayLayers` specifies the amount of layers each image consists of. -This is always `1` unless you are developing a stereoscopic 3D application. The -`imageUsage` bit field specifies what kind of operations we'll use the images in -the swap chain for. In this tutorial we're going to render directly to them, -which means that they're used as color attachment. It is also possible that -you'll render images to a separate image first to perform operations like -post-processing. In that case you may use a value like -`VK_IMAGE_USAGE_TRANSFER_DST_BIT` instead and use a memory operation to transfer -the rendered image to a swap chain image. - -```c++ -QueueFamilyIndices indices = findQueueFamilies(physicalDevice); -uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()}; - -if (indices.graphicsFamily != indices.presentFamily) { - createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT; - createInfo.queueFamilyIndexCount = 2; - createInfo.pQueueFamilyIndices = queueFamilyIndices; -} else { - createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; - createInfo.queueFamilyIndexCount = 0; // Optional - createInfo.pQueueFamilyIndices = nullptr; // Optional + // 해상도를 Vulkan 구현체가 지원하는 최소/최대 범위 내로 클램핑합니다. + actual_extent.width = actual_extent.width.clamp( + capabilities.min_image_extent.width, + capabilities.max_image_extent.width, + ); + actual_extent.height = actual_extent.height.clamp( + capabilities.min_image_extent.height, + capabilities.max_image_extent.height, + ); + + actual_extent + } } ``` -Next, we need to specify how to handle swap chain images that will be used -across multiple queue families. That will be the case in our application if the -graphics queue family is different from the presentation queue. We'll be drawing -on the images in the swap chain from the graphics queue and then submitting them -on the presentation queue. There are two ways to handle images that are -accessed from multiple queues: - -* `VK_SHARING_MODE_EXCLUSIVE`: An image is owned by one queue family at a time -and ownership must be explicitly transferred before using it in another queue -family. This option offers the best performance. -* `VK_SHARING_MODE_CONCURRENT`: Images can be used across multiple queue -families without explicit ownership transfers. - -If the queue families differ, then we'll be using the concurrent mode in this -tutorial to avoid having to do the ownership chapters, because these involve -some concepts that are better explained at a later time. Concurrent mode -requires you to specify in advance between which queue families ownership will -be shared using the `queueFamilyIndexCount` and `pQueueFamilyIndices` -parameters. If the graphics queue family and presentation queue family are the -same, which will be the case on most hardware, then we should stick to exclusive -mode, because concurrent mode requires you to specify at least two distinct -queue families. - -```c++ -createInfo.preTransform = swapChainSupport.capabilities.currentTransform; -``` +## 스왑 체인 생성 -We can specify that a certain transform should be applied to images in the swap -chain if it is supported (`supportedTransforms` in `capabilities`), like a 90 -degree clockwise rotation or horizontal flip. To specify that you do not want -any transformation, simply specify the current transformation. +이제 모든 준비가 끝났습니다. 지금까지 만든 헬퍼 함수들을 사용하여 스왑 체인을 생성해 봅시다. 주 애플리케이션 구조체에 스왑 체인 관련 필드들을 추가해야 합니다. -```c++ -createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; -``` - -The `compositeAlpha` field specifies if the alpha channel should be used for -blending with other windows in the window system. You'll almost always want to -simply ignore the alpha channel, hence `VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR`. - -```c++ -createInfo.presentMode = presentMode; -createInfo.clipped = VK_TRUE; -``` - -The `presentMode` member speaks for itself. If the `clipped` member is set to -`VK_TRUE` then that means that we don't care about the color of pixels that are -obscured, for example because another window is in front of them. Unless you -really need to be able to read these pixels back and get predictable results, -you'll get the best performance by enabling clipping. - -```c++ -createInfo.oldSwapchain = VK_NULL_HANDLE; +```rust +struct VulkanApp { + // ... 기존 필드들 + swapchain_loader: ash::extensions::khr::Swapchain, + swapchain: vk::SwapchainKHR, + swapchain_images: Vec, + swapchain_format: vk::Format, + swapchain_extent: vk::Extent2D, +} ``` +`create_swapchain` 함수를 구현합니다. -That leaves one last field, `oldSwapchain`. With Vulkan it's possible that your swap chain becomes invalid or unoptimized while your application is -running, for example because the window was resized. In that case the swap chain -actually needs to be recreated from scratch and a reference to the old one must -be specified in this field. This is a complex topic that we'll learn more about -in [a future chapter](!en/Drawing_a_triangle/Swap_chain_recreation). For now we'll -assume that we'll only ever create one swap chain. +```rust +// create_logical_device 이후에 호출 +fn create_swapchain(&mut self, window: &Window) { + let swapchain_support = query_swapchain_support(self.physical_device, &self.surface_loader, self.surface); -Now add a class member to store the `VkSwapchainKHR` object: + let surface_format = choose_swap_surface_format(&swapchain_support.formats); + let present_mode = choose_swap_present_mode(&swapchain_support.present_modes); + let extent = choose_swap_extent(&swapchain_support.capabilities, window); -```c++ -VkSwapchainKHR swapChain; -``` + // 스왑 체인에 포함될 이미지 개수를 결정합니다. + // 최소값보다 하나 더 요청하는 것이 일반적입니다. + let mut image_count = swapchain_support.capabilities.min_image_count + 1; + if swapchain_support.capabilities.max_image_count > 0 { + image_count = image_count.min(swapchain_support.capabilities.max_image_count); + } -Creating the swap chain is now as simple as calling `vkCreateSwapchainKHR`: + let indices = find_queue_families(&self.instance, self.physical_device, &self.surface_loader, self.surface); + let queue_family_indices = [ + indices.graphics_family.unwrap(), + indices.present_family.unwrap(), + ]; + + let (image_sharing_mode, queue_family_indices_slice) = + if indices.graphics_family != indices.present_family { + (vk::SharingMode::CONCURRENT, &queue_family_indices[..]) + } else { + (vk::SharingMode::EXCLUSIVE, &[] as &[u32]) + }; -```c++ -if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) { - throw std::runtime_error("failed to create swap chain!"); + let create_info = vk::SwapchainCreateInfoKHR::builder() + .surface(self.surface) + .min_image_count(image_count) + .image_format(surface_format.format) + .image_color_space(surface_format.color_space) + .image_extent(extent) + .image_array_layers(1) + .image_usage(vk::ImageUsageFlags::COLOR_ATTACHMENT) + .image_sharing_mode(image_sharing_mode) + .queue_family_indices(queue_family_indices_slice) + .pre_transform(swapchain_support.capabilities.current_transform) + .composite_alpha(vk::CompositeAlphaFlagsKHR::OPAQUE) + .present_mode(present_mode) + .clipped(true) + .old_swapchain(vk::SwapchainKHR::null()); // 창 크기 변경 시 필요 + + // ash에서는 Swapchain 확장 기능 로더가 필요합니다. + let swapchain_loader = ash::extensions::khr::Swapchain::new(&self.instance, &self.device); + let swapchain = unsafe { + swapchain_loader + .create_swapchain(&create_info, None) + .expect("Failed to create Swapchain!") + }; + + // 스왑 체인 이미지 핸들을 가져옵니다. + let swapchain_images = unsafe { + swapchain_loader + .get_swapchain_images(swapchain) + .expect("Failed to get Swapchain Images.") + }; + + self.swapchain_loader = swapchain_loader; + self.swapchain = swapchain; + self.swapchain_images = swapchain_images; + self.swapchain_format = surface_format.format; + self.swapchain_extent = extent; } ``` +`ash`에서는 `Swapchain` 확장 함수를 호출하기 위해 `ash::extensions::khr::Swapchain` 로더를 생성해야 합니다. 이 로더는 인스턴스와 논리 장치를 기반으로 만들어지며, 애플리케이션의 상태로 저장되어야 나중에 스왑 체인을 파괴할 때 사용할 수 있습니다. -The parameters are the logical device, swap chain creation info, optional custom -allocators and a pointer to the variable to store the handle in. No surprises -there. It should be cleaned up using `vkDestroySwapchainKHR` before the device: +애플리케이션이 종료될 때 스왑 체인을 정리하기 위해 `Drop` 트레잇을 구현합니다. -```c++ -void cleanup() { - vkDestroySwapchainKHR(device, swapChain, nullptr); - ... +```rust +impl Drop for VulkanApp { + fn drop(&mut self) { + unsafe { + self.swapchain_loader.destroy_swapchain(self.swapchain, None); + self.device.destroy_device(None); + self.surface_loader.destroy_surface(self.surface, None); + // ... 나머지 리소스 정리 + } + } } ``` -Now run the application to ensure that the swap chain is created successfully! If at this point you get an access violation error in `vkCreateSwapchainKHR` or see a message like `Failed to find 'vkGetInstanceProcAddress' in layer SteamOverlayVulkanLayer.dll`, then see the [FAQ entry](!en/FAQ) about the Steam overlay layer. - -Try removing the `createInfo.imageExtent = extent;` line with validation layers -enabled. You'll see that one of the validation layers immediately catches the -mistake and a helpful message is printed: +## 스왑 체인 이미지 가져오기 -![](/images/swap_chain_validation_layer.png) +`ash`를 사용하면 스왑 체인 이미지를 매우 간단하게 가져올 수 있습니다. `get_swapchain_images` 함수는 이미지 핸들이 담긴 `Vec`를 바로 반환합니다. 위 `create_swapchain` 함수에서 이미 이 과정을 포함시켰습니다. -## Retrieving the swap chain images - -The swap chain has been created now, so all that remains is retrieving the -handles of the `VkImage`s in it. We'll reference these during rendering -operations in later chapters. Add a class member to store the handles: - -```c++ -std::vector swapChainImages; -``` - -The images were created by the implementation for the swap chain and they will -be automatically cleaned up once the swap chain has been destroyed, therefore we -don't need to add any cleanup code. - -I'm adding the code to retrieve the handles to the end of the `createSwapChain` -function, right after the `vkCreateSwapchainKHR` call. Retrieving them is very -similar to the other times where we retrieved an array of objects from Vulkan. Remember that we only specified a minimum number of images in the swap chain, so the implementation is allowed to create a swap chain with more. That's why we'll first query the final number of images with `vkGetSwapchainImagesKHR`, then resize the container and finally call it again -to retrieve the handles. - -```c++ -vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr); -swapChainImages.resize(imageCount); -vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data()); -``` - -One last thing, store the format and extent we've chosen for the swap chain -images in member variables. We'll need them in future chapters. - -```c++ -VkSwapchainKHR swapChain; -std::vector swapChainImages; -VkFormat swapChainImageFormat; -VkExtent2D swapChainExtent; - -... - -swapChainImageFormat = surfaceFormat.format; -swapChainExtent = extent; +```rust +let swapchain_images = unsafe { + swapchain_loader + .get_swapchain_images(swapchain) + .expect("Failed to get Swapchain Images.") +}; ``` +이미지들은 스왑 체인에 의해 소유되므로, 스왑 체인이 파괴될 때 자동으로 정리됩니다. 별도로 이미지를 파괴할 필요는 없습니다. -We now have a set of images that can be drawn onto and can be presented to the -window. The next chapter will begin to cover how we can set up the images as -render targets and then we start looking into the actual graphics pipeline and -drawing commands! +이제 우리는 렌더링하고 창에 표시할 수 있는 이미지 집합을 갖게 되었습니다. 다음 장에서는 이 이미지들을 렌더 타겟으로 설정하고, 실제 그래픽 파이프라인과 그리기 명령에 대해 알아보기 시작하겠습니다! -[C++ code](/code/06_swap_chain_creation.cpp) +[Rust 코드 예시](https://github.com/ash-rs/ash/blob/master/examples/triangle.rs) (전체적인 구조 참고용) \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/01_Presentation/02_Image_views.md b/ko-rust/03_Drawing_a_triangle/01_Presentation/02_Image_views.md index 5988468a..2a7b9951 100644 --- a/ko-rust/03_Drawing_a_triangle/01_Presentation/02_Image_views.md +++ b/ko-rust/03_Drawing_a_triangle/01_Presentation/02_Image_views.md @@ -1,127 +1,158 @@ -To use any `VkImage`, including those in the swap chain, in the render pipeline -we have to create a `VkImageView` object. An image view is quite literally a -view into an image. It describes how to access the image and which part of the -image to access, for example if it should be treated as a 2D texture depth -texture without any mipmapping levels. +### Rust와 Ash 라이브러리를 사용한 이미지 뷰 생성 -In this chapter we'll write a `createImageViews` function that creates a basic -image view for every image in the swap chain so that we can use them as color -targets later on. +스왑 체인에 있는 이미지를 포함한 모든 `VkImage`를 렌더 파이프라인에서 사용하려면 `VkImageView` 객체를 생성해야 합니다. 이미지 뷰는 말 그대로 이미지로의 뷰(view)입니다. 이는 이미지에 접근하는 방법과 접근할 이미지의 부분을 기술합니다. 예를 들어, 밉매핑 레벨이 없는 2D 텍스처나 깊이 텍스처로 취급해야 하는지 등을 명시합니다. -First add a class member to store the image views in: +이번 장에서는 스왑 체인의 모든 이미지에 대한 기본적인 이미지 뷰를 만드는 `create_image_views` 함수를 작성할 것입니다. 이렇게 하면 나중에 이미지 뷰들을 컬러 타겟으로 사용할 수 있습니다. -```c++ -std::vector swapChainImageViews; -``` - -Create the `createImageViews` function and call it right after swap chain -creation. - -```c++ -void initVulkan() { - createInstance(); - setupDebugMessenger(); - createSurface(); - pickPhysicalDevice(); - createLogicalDevice(); - createSwapChain(); - createImageViews(); -} - -void createImageViews() { +먼저, 이미지 뷰를 저장할 구조체 필드를 추가합니다. +```rust +struct VulkanApp { + // ... 다른 필드들 + swapchain_images: Vec, + swapchain_format: vk::Format, + swapchain_extent: vk::Extent2D, + swapchain_image_views: Vec, // 이 필드를 추가합니다. } ``` -The first thing we need to do is resize the list to fit all of the image views -we'll be creating: +`create_image_views` 함수를 만들고 스왑 체인 생성 직후에 호출하도록 합니다. Rust에서는 보통 `new` 생성자나 초기화 함수 내에서 순서대로 호출합니다. -```c++ -void createImageViews() { - swapChainImageViews.resize(swapChainImages.size()); +```rust +impl VulkanApp { + pub fn new(window: &winit::window::Window) -> Self { + // ... + app.create_logical_device(); + app.create_swapchain(); + app.create_image_views(); // 스왑체인 생성 직후 호출 + // ... + } + fn create_image_views(&mut self) { + // 이 함수를 구현합니다. + } } ``` -Next, set up the loop that iterates over all of the swap chain images. - -```c++ -for (size_t i = 0; i < swapChainImages.size(); i++) { +이제 `create_image_views` 함수를 구현해 보겠습니다. C++ 버전처럼 벡터의 크기를 미리 조정하는 대신, 각 스왑 체인 이미지를 순회하면서 생성된 이미지 뷰를 새 벡터에 추가하는 방식을 사용하겠습니다. +```rust +fn create_image_views(&mut self) { + self.swapchain_image_views = self.swapchain_images + .iter() + .map(|&image| { + // 여기에 각 이미지에 대한 이미지 뷰 생성 로직이 들어갑니다. + }) + .collect(); } ``` -The parameters for image view creation are specified in a -`VkImageViewCreateInfo` structure. The first few parameters are straightforward. - -```c++ -VkImageViewCreateInfo createInfo{}; -createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; -createInfo.image = swapChainImages[i]; -``` - -The `viewType` and `format` fields specify how the image data should be -interpreted. The `viewType` parameter allows you to treat images as 1D textures, -2D textures, 3D textures and cube maps. +`map` 클로저 내부에서 각 이미지에 대한 이미지 뷰를 생성합니다. 이미지 뷰 생성을 위한 파라미터는 `vk::ImageViewCreateInfo` 구조체에 명시됩니다. `ash` 라이브러리는 빌더(builder) 패턴을 제공하여 이 구조체를 더 안전하고 편리하게 생성할 수 있습니다. -```c++ -createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; -createInfo.format = swapChainImageFormat; +```rust +// map 클로저 내부 +let components = vk::ComponentMapping { + r: vk::ComponentSwizzle::IDENTITY, + g: vk::ComponentSwizzle::IDENTITY, + b: vk::ComponentSwizzle::IDENTITY, + a: vk::ComponentSwizzle::IDENTITY, +}; ``` -The `components` field allows you to swizzle the color channels around. For -example, you can map all of the channels to the red channel for a monochrome -texture. You can also map constant values of `0` and `1` to a channel. In our -case we'll stick to the default mapping. - -```c++ -createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; -createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; -createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; -createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; +`components` 필드는 컬러 채널을 스위즐(swizzle)할 수 있게 해줍니다. 예를 들어, 모든 채널을 빨간색 채널에 매핑하여 단색 텍스처를 만들 수 있습니다. 여기서는 기본 매핑을 사용합니다. + +```rust +// map 클로저 내부, components 정의 다음 +let subresource_range = vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, +}; ``` -The `subresourceRange` field describes what the image's purpose is and which -part of the image should be accessed. Our images will be used as color targets -without any mipmapping levels or multiple layers. - -```c++ -createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; -createInfo.subresourceRange.baseMipLevel = 0; -createInfo.subresourceRange.levelCount = 1; -createInfo.subresourceRange.baseArrayLayer = 0; -createInfo.subresourceRange.layerCount = 1; +`subresource_range` 필드는 이미지의 용도와 접근할 이미지의 부분을 기술합니다. 우리의 이미지는 밉매핑 레벨이나 여러 레이어 없이 컬러 타겟으로 사용될 것입니다. +* `aspect_mask`: 컬러 이미지를 다루므로 `vk::ImageAspectFlags::COLOR`을 사용합니다. +* `base_mip_level`, `level_count`: 밉매핑을 사용하지 않으므로 기본 레벨 0에, 레벨 수는 1로 설정합니다. +* `base_array_layer`, `layer_count`: 스테레오스코픽 3D 앱이 아니므로 기본 배열 레이어 0에, 레이어 수는 1로 설정합니다. + +이제 이 정보들을 사용하여 `ImageViewCreateInfo`를 빌드하고 `create_image_view` 함수를 호출합니다. + +```rust +// map 클로저 내부, subresource_range 정의 다음 +let create_info = vk::ImageViewCreateInfo::builder() + .image(image) + .view_type(vk::ImageViewType::TYPE_2D) + .format(self.swapchain_format) + .components(components) + .subresource_range(subresource_range); + +let image_view = unsafe { + self.device + .create_image_view(&create_info, None) + .expect("Failed to create image view!") +}; +image_view // map 클로저의 반환 값 ``` -If you were working on a stereographic 3D application, then you would create a -swap chain with multiple layers. You could then create multiple image views for -each image representing the views for the left and right eyes by accessing -different layers. - -Creating the image view is now a matter of calling `vkCreateImageView`: - -```c++ -if (vkCreateImageView(device, &createInfo, nullptr, &swapChainImageViews[i]) != VK_SUCCESS) { - throw std::runtime_error("failed to create image views!"); +`ash`의 생성 함수는 `unsafe` 블록 안에서 호출해야 합니다. 왜냐하면 유효하지 않은 파라미터(예: 해제된 `device`나 `image`)를 전달하면 정의되지 않은 동작을 유발할 수 있기 때문입니다. `ash` 함수는 `Result`를 반환하므로, `expect`를 사용하여 에러 발생 시 프로그램을 중단하고 메시지를 출력할 수 있습니다. `None`은 커스텀 할당자를 사용하지 않음을 의미합니다. + +전체 `create_image_views` 함수는 다음과 같습니다. + +```rust +fn create_image_views(&mut self) { + self.swapchain_image_views = self + .swapchain_images + .iter() + .map(|&image| { + let components = vk::ComponentMapping { + r: vk::ComponentSwizzle::IDENTITY, + g: vk::ComponentSwizzle::IDENTITY, + b: vk::ComponentSwizzle::IDENTITY, + a: vk::ComponentSwizzle::IDENTITY, + }; + + let subresource_range = vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }; + + let create_info = vk::ImageViewCreateInfo::builder() + .image(image) + .view_type(vk::ImageViewType::TYPE_2D) + .format(self.swapchain_format) + .components(components) + .subresource_range(subresource_range); + + unsafe { + self.device + .create_image_view(&create_info, None) + .expect("Failed to create image view!") + } + }) + .collect(); } ``` -Unlike images, the image views were explicitly created by us, so we need to add -a similar loop to destroy them again at the end of the program: - -```c++ -void cleanup() { - for (auto imageView : swapChainImageViews) { - vkDestroyImageView(device, imageView, nullptr); +이미지와 달리, 이미지 뷰는 우리가 명시적으로 생성했으므로, 프로그램이 끝날 때 이를 정리하는 코드를 추가해야 합니다. Rust에서는 보통 `Drop` 트레이트 구현을 통해 리소스 해제를 자동화하지만, 이 튜토리얼의 구조를 따라 `cleanup` 함수에 추가하겠습니다. + +```rust +impl VulkanApp { + pub fn cleanup(&mut self) { + unsafe { + for &image_view in self.swapchain_image_views.iter() { + self.device.destroy_image_view(image_view, None); + } + // ... 다른 리소스 정리 + } } - - ... } ``` -An image view is sufficient to start using an image as a texture, but it's not -quite ready to be used as a render target just yet. That requires one more step -of indirection, known as a framebuffer. But first we'll have to set up the -graphics pipeline. +`destroy_image_view` 또한 `unsafe` 함수입니다. -[C++ code](/code/07_image_views.cpp) +이미지 뷰는 이미지를 텍스처로 사용하기 시작하기에는 충분하지만, 렌더 타겟으로 사용되기에는 아직 완전히 준비되지 않았습니다. 이를 위해서는 프레임버퍼(framebuffer)라고 알려진 한 단계의 간접 과정이 더 필요합니다. 하지만 그 전에 그래픽 파이프라인을 먼저 설정해야 합니다. \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md index 9ee7f739..d8f1b543 100644 --- a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md @@ -1,99 +1,54 @@ -Over the course of the next few chapters we'll be setting up a graphics pipeline -that is configured to draw our first triangle. The graphics pipeline is the -sequence of operations that take the vertices and textures of your meshes all -the way to the pixels in the render targets. A simplified overview is displayed -below: +앞으로 몇 개의 챕터에 걸쳐 첫 번째 삼각형을 그리기 위해 구성된 그래픽스 파이프라인을 설정할 것입니다. 그래픽스 파이프라인은 메시의 정점(vertices)과 텍스처(textures)를 가져와 렌더 타겟의 픽셀에 이르는 일련의 작업 순서입니다. 아래에 간소화된 개요가 표시되어 있습니다: ![](/images/vulkan_simplified_pipeline.svg) -The *input assembler* collects the raw vertex data from the buffers you specify -and may also use an index buffer to repeat certain elements without having to -duplicate the vertex data itself. - -The *vertex shader* is run for every vertex and generally applies -transformations to turn vertex positions from model space to screen space. It -also passes per-vertex data down the pipeline. - -The *tessellation shaders* allow you to subdivide geometry based on certain -rules to increase the mesh quality. This is often used to make surfaces like -brick walls and staircases look less flat when they are nearby. - -The *geometry shader* is run on every primitive (triangle, line, point) and can -discard it or output more primitives than came in. This is similar to the -tessellation shader, but much more flexible. However, it is not used much in -today's applications because the performance is not that good on most graphics -cards except for Intel's integrated GPUs. - -The *rasterization* stage discretizes the primitives into *fragments*. These are -the pixel elements that they fill on the framebuffer. Any fragments that fall -outside the screen are discarded and the attributes outputted by the vertex -shader are interpolated across the fragments, as shown in the figure. Usually -the fragments that are behind other primitive fragments are also discarded here -because of depth testing. - -The *fragment shader* is invoked for every fragment that survives and determines -which framebuffer(s) the fragments are written to and with which color and depth -values. It can do this using the interpolated data from the vertex shader, which -can include things like texture coordinates and normals for lighting. - -The *color blending* stage applies operations to mix different fragments that -map to the same pixel in the framebuffer. Fragments can simply overwrite each -other, add up or be mixed based upon transparency. - -Stages with a green color are known as *fixed-function* stages. These stages -allow you to tweak their operations using parameters, but the way they work is -predefined. - -Stages with an orange color on the other hand are `programmable`, which means -that you can upload your own code to the graphics card to apply exactly the -operations you want. This allows you to use fragment shaders, for example, to -implement anything from texturing and lighting to ray tracers. These programs -run on many GPU cores simultaneously to process many objects, like vertices and -fragments in parallel. - -If you've used older APIs like OpenGL and Direct3D before, then you'll be used -to being able to change any pipeline settings at will with calls like -`glBlendFunc` and `OMSetBlendState`. The graphics pipeline in Vulkan is almost -completely immutable, so you must recreate the pipeline from scratch if you want -to change shaders, bind different framebuffers or change the blend function. The -disadvantage is that you'll have to create a number of pipelines that represent -all of the different combinations of states you want to use in your rendering -operations. However, because all of the operations you'll be doing in the -pipeline are known in advance, the driver can optimize for it much better. - -Some of the programmable stages are optional based on what you intend to do. For -example, the tessellation and geometry stages can be disabled if you are just -drawing simple geometry. If you are only interested in depth values then you can -disable the fragment shader stage, which is useful for [shadow map](https://en.wikipedia.org/wiki/Shadow_mapping) -generation. - -In the next chapter we'll first create the two programmable stages required to -put a triangle onto the screen: the vertex shader and fragment shader. The -fixed-function configuration like blending mode, viewport, rasterization will be -set up in the chapter after that. The final part of setting up the graphics -pipeline in Vulkan involves the specification of input and output framebuffers. - -Create a `createGraphicsPipeline` function that is called right after -`createImageViews` in `initVulkan`. We'll work on this function throughout the -following chapters. - -```c++ -void initVulkan() { - createInstance(); - setupDebugMessenger(); - createSurface(); - pickPhysicalDevice(); - createLogicalDevice(); - createSwapChain(); - createImageViews(); - createGraphicsPipeline(); -} +* **입력 조립기(Input Assembler)**는 지정한 버퍼에서 원시 정점 데이터를 수집하고, 인덱스 버퍼를 사용하여 정점 데이터 자체를 복제하지 않고도 특정 요소를 반복할 수도 있습니다. -... +* **정점 셰이더(Vertex Shader)**는 모든 정점에 대해 실행되며, 일반적으로 정점 위치를 모델 공간(model space)에서 스크린 공간(screen space)으로 변환하는 작업을 적용합니다. 또한 정점별 데이터를 파이프라인의 다음 단계로 전달합니다. -void createGraphicsPipeline() { +* **테셀레이션 셰이더(Tessellation Shaders)**를 사용하면 특정 규칙에 따라 지오메트리(geometry)를 세분화하여 메시 품질을 높일 수 있습니다. 이는 벽돌 벽이나 계단과 같은 표면이 가까이 있을 때 덜 평평하게 보이도록 만드는 데 자주 사용됩니다. -} -``` +* **지오메트리 셰이더(Geometry Shader)**는 모든 프리미티브(primitive, 삼각형, 선, 점)에 대해 실행되며, 이를 폐기하거나 들어온 것보다 더 많은 프리미티브를 출력할 수 있습니다. 이는 테셀레이션 셰이더와 유사하지만 훨씬 더 유연합니다. 하지만 Intel의 내장 GPU를 제외한 대부분의 그래픽 카드에서는 성능이 좋지 않기 때문에 오늘날의 애플리케이션에서는 많이 사용되지 않습니다. + +* **래스터화(Rasterization)** 단계는 프리미티브를 *프래그먼트(fragment)*로 이산화(discretize)합니다. 이것들은 프레임버퍼에서 채우는 픽셀 요소입니다. 화면 밖에 있는 모든 프래그먼트는 폐기되고, 정점 셰이더에서 출력된 속성들은 그림과 같이 프래그먼트 전체에 걸쳐 보간(interpolated)됩니다. 일반적으로 다른 프리미티브 프래그먼트 뒤에 있는 프래그먼트도 깊이 테스팅(depth testing)으로 인해 여기서 폐기됩니다. + +* **프래그먼트 셰이더(Fragment Shader)**는 살아남은 모든 프래그먼트에 대해 호출되며, 프래그먼트가 어떤 프레임버퍼에 어떤 색상과 깊이 값으로 기록될지를 결정합니다. 이는 텍스처 좌표 및 조명을 위한 법선(normal)과 같은 것들을 포함할 수 있는, 정점 셰이더로부터 보간된 데이터를 사용하여 수행할 수 있습니다. + +* **색상 혼합(Color Blending)** 단계는 프레임버퍼의 동일한 픽셀에 매핑되는 다른 프래그먼트들을 혼합하는 연산을 적용합니다. 프래그먼트는 서로를 덮어쓰거나, 더해지거나, 투명도에 따라 혼합될 수 있습니다. + +녹색으로 표시된 단계는 **고정 기능(fixed-function)** 단계라고 합니다. 이 단계에서는 매개변수를 사용하여 작업을 조정할 수 있지만, 작동 방식은 미리 정의되어 있습니다. + +반면에 주황색으로 표시된 단계는 **프로그래밍 가능(programmable)**하며, 이는 그래픽 카드에 자신만의 코드를 업로드하여 원하는 작업을 정확하게 적용할 수 있음을 의미합니다. 이를 통해 예를 들어 프래그먼트 셰이더를 사용하여 텍스처링과 조명에서부터 레이 트레이서에 이르기까지 모든 것을 구현할 수 있습니다. 이 프로그램들은 많은 GPU 코어에서 동시에 실행되어 정점이나 프래그먼트와 같은 많은 객체를 병렬로 처리합니다. -[C++ code](/code/08_graphics_pipeline.cpp) +이전에 OpenGL이나 Direct3D와 같은 오래된 API를 사용해 본 적이 있다면, 파이프라인 설정을 마음대로 변경하는 데 익숙할 것입니다. Vulkan의 그래픽스 파이프라인은 거의 완전히 **불변(immutable)**하므로, 셰이더를 변경하거나, 다른 프레임버퍼를 바인딩하거나, 혼합 함수를 변경하려면 파이프라인을 처음부터 다시 생성해야 합니다. 단점은 렌더링 작업에서 사용하려는 모든 다른 상태 조합을 나타내는 여러 개의 파이프라인을 만들어야 한다는 것입니다. 하지만 파이프라인에서 수행할 모든 작업이 미리 알려져 있기 때문에 드라이버가 이를 훨씬 더 잘 최적화할 수 있습니다. + +일부 프로그래밍 가능 단계는 무엇을 하려는지에 따라 선택 사항입니다. 예를 들어, 간단한 지오메트리만 그리는 경우 테셀레이션 및 지오메트리 단계를 비활성화할 수 있습니다. 깊이 값에만 관심이 있다면 프래그먼트 셰이더 단계를 비활성화할 수 있으며, 이는 [그림자 맵핑(shadow mapping)](https://en.wikipedia.org/wiki/Shadow_mapping) 생성에 유용합니다. + +다음 챕터에서는 먼저 화면에 삼각형을 표시하는 데 필요한 두 가지 프로그래밍 가능 단계인 정점 셰이더와 프래그먼트 셰이더를 만들 것입니다. 혼합 모드, 뷰포트, 래스터화와 같은 고정 기능 구성은 그 다음 챕터에서 설정할 것입니다. Vulkan에서 그래픽스 파이프라인 설정의 마지막 부분은 입력 및 출력 프레임버퍼를 지정하는 것입니다. + +Rust에서는 일반적으로 `App` 또는 이와 유사한 `struct` 내에서 Vulkan 객체를 관리합니다. `init_vulkan`과 같은 초기화 함수에서 `create_image_views` 바로 뒤에 `create_graphics_pipeline`을 호출하도록 수정하세요. 앞으로의 챕터에 걸쳐 이 함수를 구현해 나갈 것입니다. + +```rust +// In Rust, these functions are typically methods on a struct that holds Vulkan state. +// Let's assume a struct named `App`. +impl App { + fn init_vulkan(&mut self, window: &Window) -> Result<()> { + self.create_instance(window)?; + self.setup_debug_utils()?; + self.create_surface(window)?; + self.pick_physical_device()?; + self.create_logical_device()?; + self.create_swapchain()?; + self.create_image_views()?; + self.create_graphics_pipeline()?; // Add this call + Ok(()) + } + + // ... other methods ... + + fn create_graphics_pipeline(&mut self) -> Result<()> { + // We will fill this in the upcoming chapters. + Ok(()) + } +} +``` \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md index ef12e836..cfbb31d6 100644 --- a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md @@ -1,196 +1,12 @@ -Unlike earlier APIs, shader code in Vulkan has to be specified in a bytecode -format as opposed to human-readable syntax like [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language) -and [HLSL](https://en.wikipedia.org/wiki/High-Level_Shading_Language). This -bytecode format is called [SPIR-V](https://www.khronos.org/spir) and is designed -to be used with both Vulkan and OpenCL (both Khronos APIs). It is a format that -can be used to write graphics and compute shaders, but we will focus on shaders -used in Vulkan's graphics pipelines in this tutorial. - -The advantage of using a bytecode format is that the compilers written by GPU -vendors to turn shader code into native code are significantly less complex. The -past has shown that with human-readable syntax like GLSL, some GPU vendors were -rather flexible with their interpretation of the standard. If you happen to -write non-trivial shaders with a GPU from one of these vendors, then you'd risk -other vendor's drivers rejecting your code due to syntax errors, or worse, your -shader running differently because of compiler bugs. With a straightforward -bytecode format like SPIR-V that will hopefully be avoided. - -However, that does not mean that we need to write this bytecode by hand. Khronos -has released their own vendor-independent compiler that compiles GLSL to SPIR-V. -This compiler is designed to verify that your shader code is fully standards -compliant and produces one SPIR-V binary that you can ship with your program. -You can also include this compiler as a library to produce SPIR-V at runtime, -but we won't be doing that in this tutorial. Although we can use this compiler directly via `glslangValidator.exe`, we will be using `glslc.exe` by Google instead. The advantage of `glslc` is that it uses the same parameter format as well-known compilers like GCC and Clang and includes some extra functionality like *includes*. Both of them are already included in the Vulkan SDK, so you don't need to download anything extra. - -GLSL is a shading language with a C-style syntax. Programs written in it have a -`main` function that is invoked for every object. Instead of using parameters -for input and a return value as output, GLSL uses global variables to handle -input and output. The language includes many features to aid in graphics -programming, like built-in vector and matrix primitives. Functions for -operations like cross products, matrix-vector products and reflections around a -vector are included. The vector type is called `vec` with a number indicating -the amount of elements. For example, a 3D position would be stored in a `vec3`. -It is possible to access single components through members like `.x`, but it's -also possible to create a new vector from multiple components at the same time. -For example, the expression `vec3(1.0, 2.0, 3.0).xy` would result in `vec2`. The -constructors of vectors can also take combinations of vector objects and scalar -values. For example, a `vec3` can be constructed with -`vec3(vec2(1.0, 2.0), 3.0)`. - -As the previous chapter mentioned, we need to write a vertex shader and a -fragment shader to get a triangle on the screen. The next two sections will -cover the GLSL code of each of those and after that I'll show you how to produce -two SPIR-V binaries and load them into the program. - -## Vertex shader - -The vertex shader processes each incoming vertex. It takes its attributes, like -model space position, color, normal and texture coordinates as input. The output is -the final position in clip coordinates and the attributes that need to be passed -on to the fragment shader, like color and texture coordinates. These values will -then be interpolated over the fragments by the rasterizer to produce a smooth -gradient. - -A *clip coordinate* is a four dimensional vector from the vertex shader that is -subsequently turned into a *normalized device coordinate* by dividing the whole -vector by its last component. These normalized device coordinates are -[homogeneous coordinates](https://en.wikipedia.org/wiki/Homogeneous_coordinates) -that map the framebuffer to a [-1, 1] by [-1, 1] coordinate system that looks -like the following: - -![](/images/normalized_device_coordinates.svg) - -You should already be familiar with these if you have dabbled in computer -graphics before. If you have used OpenGL before, then you'll notice that the -sign of the Y coordinates is now flipped. The Z coordinate now uses the same -range as it does in Direct3D, from 0 to 1. - -For our first triangle we won't be applying any transformations, we'll just -specify the positions of the three vertices directly as normalized device -coordinates to create the following shape: - -![](/images/triangle_coordinates.svg) - -We can directly output normalized device coordinates by outputting them as clip -coordinates from the vertex shader with the last component set to `1`. That way -the division to transform clip coordinates to normalized device coordinates will -not change anything. - -Normally these coordinates would be stored in a vertex buffer, but creating a -vertex buffer in Vulkan and filling it with data is not trivial. Therefore I've -decided to postpone that until after we've had the satisfaction of seeing a -triangle pop up on the screen. We're going to do something a little unorthodox -in the meanwhile: include the coordinates directly inside the vertex shader. The -code looks like this: +이전 API들과 달리, Vulkan의 셰이더 코드는 [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language)이나 [HLSL](https://en.wikipedia.org/wiki/High-Level_Shading_Language)처럼 사람이 읽을 수 있는 문법이 아닌 바이트코드 형식으로 명시되어야 합니다. 이 바이트코드 형식을 [SPIR-V](https://www.khronos.org/spir)라고 부르며, Vulkan과 OpenCL(둘 다 Khronos API) 모두에서 사용하도록 설계되었습니다. SPIR-V는 그래픽 및 컴퓨트 셰이더를 작성하는 데 사용될 수 있지만, 이 튜토리얼에서는 Vulkan의 그래픽 파이프라인에서 사용되는 셰이더에 초점을 맞출 것입니다. -```glsl -#version 450 - -vec2 positions[3] = vec2[]( - vec2(0.0, -0.5), - vec2(0.5, 0.5), - vec2(-0.5, 0.5) -); - -void main() { - gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); -} -``` - -The `main` function is invoked for every vertex. The built-in `gl_VertexIndex` -variable contains the index of the current vertex. This is usually an index into -the vertex buffer, but in our case it will be an index into a hardcoded array -of vertex data. The position of each vertex is accessed from the constant array -in the shader and combined with dummy `z` and `w` components to produce a -position in clip coordinates. The built-in variable `gl_Position` functions as -the output. - -## Fragment shader - -The triangle that is formed by the positions from the vertex shader fills an -area on the screen with fragments. The fragment shader is invoked on these -fragments to produce a color and depth for the framebuffer (or framebuffers). A -simple fragment shader that outputs the color red for the entire triangle looks -like this: - -```glsl -#version 450 - -layout(location = 0) out vec4 outColor; - -void main() { - outColor = vec4(1.0, 0.0, 0.0, 1.0); -} -``` - -The `main` function is called for every fragment just like the vertex shader -`main` function is called for every vertex. Colors in GLSL are 4-component -vectors with the R, G, B and alpha channels within the [0, 1] range. Unlike -`gl_Position` in the vertex shader, there is no built-in variable to output a -color for the current fragment. You have to specify your own output variable for -each framebuffer where the `layout(location = 0)` modifier specifies the index -of the framebuffer. The color red is written to this `outColor` variable that is -linked to the first (and only) framebuffer at index `0`. - -## Per-vertex colors - -Making the entire triangle red is not very interesting, wouldn't something like -the following look a lot nicer? - -![](/images/triangle_coordinates_colors.png) - -We have to make a couple of changes to both shaders to accomplish this. First -off, we need to specify a distinct color for each of the three vertices. The -vertex shader should now include an array with colors just like it does for -positions: - -```glsl -vec3 colors[3] = vec3[]( - vec3(1.0, 0.0, 0.0), - vec3(0.0, 1.0, 0.0), - vec3(0.0, 0.0, 1.0) -); -``` - -Now we just need to pass these per-vertex colors to the fragment shader so it -can output their interpolated values to the framebuffer. Add an output for color -to the vertex shader and write to it in the `main` function: - -```glsl -layout(location = 0) out vec3 fragColor; - -void main() { - gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); - fragColor = colors[gl_VertexIndex]; -} -``` +바이트코드 형식을 사용하는 것의 장점은, GPU 제조사가 셰이더 코드를 네이티브 코드로 변환하기 위해 작성하는 컴파일러가 훨씬 덜 복잡해진다는 점입니다. 과거 GLSL과 같이 사람이 읽을 수 있는 문법의 경우, 일부 GPU 제조사는 표준을 다소 유연하게 해석하는 경향이 있었습니다. 만약 여러분이 이런 제조사 중 하나의 GPU로 복잡한 셰이더를 작성했다면, 다른 제조사의 드라이버가 문법 오류로 코드를 거부하거나, 더 심하게는 컴파일러 버그로 셰이더가 다르게 동작할 위험이 있었습니다. SPIR-V와 같은 직관적인 바이트코드 형식을 사용하면 이러한 문제를 피할 수 있을 것입니다. -Next, we need to add a matching input in the fragment shader: +하지만 그렇다고 해서 우리가 이 바이트코드를 직접 손으로 작성해야 한다는 의미는 아닙니다. Khronos는 GLSL을 SPIR-V로 컴파일하는 자체적인 벤더 독립적 컴파일러를 출시했습니다. 이 컴파일러는 여러분의 셰이더 코드가 표준을 완벽하게 준수하는지 확인하고, 프로그램과 함께 배포할 수 있는 단일 SPIR-V 바이너리를 생성하도록 설계되었습니다. 우리는 `glslc` 컴파일러를 사용하여 GLSL 셰이더를 SPIR-V로 변환할 것입니다. `glslc`는 GCC나 Clang과 같은 잘 알려진 컴파일러와 동일한 파라미터 형식을 사용하고, *인클루드*와 같은 추가 기능을 포함합니다. 이 컴파일러는 Vulkan SDK에 이미 포함되어 있으므로 추가로 다운로드할 필요가 없습니다. -```glsl -layout(location = 0) in vec3 fragColor; - -void main() { - outColor = vec4(fragColor, 1.0); -} -``` - -The input variable does not necessarily have to use the same name, they will be -linked together using the indexes specified by the `location` directives. The -`main` function has been modified to output the color along with an alpha value. -As shown in the image above, the values for `fragColor` will be automatically -interpolated for the fragments between the three vertices, resulting in a smooth -gradient. - -## Compiling the shaders - -Create a directory called `shaders` in the root directory of your project and -store the vertex shader in a file called `shader.vert` and the fragment shader -in a file called `shader.frag` in that directory. GLSL shaders don't have an -official extension, but these two are commonly used to distinguish them. - -The contents of `shader.vert` should be: +*(이후 GLSL에 대한 설명, Vertex/Fragment 셰이더 코드, 컴파일 방법은 원문과 동일하므로 생략하고 Rust 코드 구현 부분부터 시작하겠습니다. 아래의 GLSL 코드를 각각 `shaders/shader.vert`와 `shaders/shader.frag` 파일로 저장하고 컴파일해 `vert.spv`와 `frag.spv` 파일을 준비했다고 가정합니다.)* +**Vertex Shader (`shader.vert`)** ```glsl #version 450 @@ -214,8 +30,7 @@ void main() { } ``` -And the contents of `shader.frag` should be: - +**Fragment Shader (`shader.frag`)** ```glsl #version 450 @@ -228,240 +43,137 @@ void main() { } ``` -We're now going to compile these into SPIR-V bytecode using the -`glslc` program. - -**Windows** - -Create a `compile.bat` file with the following contents: - -```bash -C:/VulkanSDK/x.x.x.x/Bin/glslc.exe shader.vert -o vert.spv -C:/VulkanSDK/x.x.x.x/Bin/glslc.exe shader.frag -o frag.spv -pause -``` - -Replace the path to `glslc.exe` with the path to where you installed -the Vulkan SDK. Double click the file to run it. - -**Linux** - -Create a `compile.sh` file with the following contents: - -```bash -/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.vert -o vert.spv -/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.frag -o frag.spv -``` - -Replace the path to `glslc` with the path to where you installed the -Vulkan SDK. Make the script executable with `chmod +x compile.sh` and run it. - -**End of platform-specific instructions** - -These two commands tell the compiler to read the GLSL source file and output a SPIR-V bytecode file using the `-o` (output) flag. +## 셰이더 로드하기 -If your shader contains a syntax error then the compiler will tell you the line -number and problem, as you would expect. Try leaving out a semicolon for example -and run the compile script again. Also try running the compiler without any -arguments to see what kinds of flags it supports. It can, for example, also -output the bytecode into a human-readable format so you can see exactly what -your shader is doing and any optimizations that have been applied at this stage. +이제 SPIR-V 셰이더를 생성했으니, 프로그램에 로드하여 그래픽 파이프라인에 연결할 시간입니다. Rust의 표준 라이브러리를 사용하면 파일에서 바이너리 데이터를 매우 쉽게 읽을 수 있습니다. -Compiling shaders on the commandline is one of the most straightforward options and it's the one that we'll use in this tutorial, but it's also possible to compile shaders directly from your own code. The Vulkan SDK includes [libshaderc](https://github.com/google/shaderc), which is a library to compile GLSL code to SPIR-V from within your program. +`create_graphics_pipeline` 함수에서 두 셰이더의 바이트코드를 로드합니다. -## Loading a shader - -Now that we have a way of producing SPIR-V shaders, it's time to load them into -our program to plug them into the graphics pipeline at some point. We'll first -write a simple helper function to load the binary data from the files. - -```c++ -#include - -... - -static std::vector readFile(const std::string& filename) { - std::ifstream file(filename, std::ios::ate | std::ios::binary); - - if (!file.is_open()) { - throw std::runtime_error("failed to open file!"); - } +```rust +// In `VulkanApp::create_graphics_pipeline` +fn create_graphics_pipeline(&mut self) { + let vert_shader_code = std::fs::read("shaders/vert.spv") + .expect("Failed to read vertex shader file!"); + let frag_shader_code = std::fs::read("shaders/frag.spv") + .expect("Failed to read fragment shader file!"); + // ... } ``` -The `readFile` function will read all of the bytes from the specified file and -return them in a byte array managed by `std::vector`. We start by opening the -file with two flags: +`std::fs::read` 함수는 파일의 모든 바이트를 읽어 `Vec` (바이트 벡터)로 반환합니다. C++ 예제처럼 수동으로 파일 크기를 계산하고 버퍼를 할당할 필요가 없습니다. 파일 읽기에 실패하면 `expect`가 프로그램을 패닉시키므로, 실제 애플리케이션에서는 `Result`를 적절히 처리해야 합니다. -* `ate`: Start reading at the end of the file -* `binary`: Read the file as binary file (avoid text transformations) +## 셰이더 모듈 생성하기 -The advantage of starting to read at the end of the file is that we can use the -read position to determine the size of the file and allocate a buffer: +코드를 파이프라인에 전달하기 전에 `ash::vk::ShaderModule` 객체로 감싸야 합니다. 이를 위해 `create_shader_module` 헬퍼 함수를 만들어 보겠습니다. -```c++ -size_t fileSize = (size_t) file.tellg(); -std::vector buffer(fileSize); +```rust +// In `impl VulkanApp` +fn create_shader_module(&self, code: &[u8]) -> vk::ShaderModule { + // ... +} ``` -After that, we can seek back to the beginning of the file and read all of the -bytes at once: +이 함수는 바이트코드가 담긴 슬라이스를 파라미터로 받아 `vk::ShaderModule`을 생성합니다. -```c++ -file.seekg(0); -file.read(buffer.data(), fileSize); -``` +셰이더 모듈을 만드는 것은 간단합니다. 바이트코드와 그 길이를 지정하면 됩니다. C++ 버전에서는 `char` 포인터를 `uint32_t` 포인터로 `reinterpret_cast`해야 했고, 이 과정에서 데이터 정렬(alignment) 문제가 발생할 수 있었습니다. SPIR-V는 `u32`의 슬라이스를 기대하기 때문입니다. -And finally close the file and return the bytes: +Rust에서는 `ash`가 제공하는 유틸리티 함수를 사용해 이 과정을 안전하고 간단하게 처리할 수 있습니다. `ash::util::read_spv`는 바이트 슬라이스(`&[u8]`)를 받아 `Vec`로 변환해주므로 정렬 문제를 걱정할 필요가 없습니다. -```c++ -file.close(); +```rust +use ash::{util, vk}; +use std::io::Cursor; -return buffer; -``` +// ... -We'll now call this function from `createGraphicsPipeline` to load the bytecode -of the two shaders: +fn create_shader_module(&self, code: &[u8]) -> vk::ShaderModule { + let mut cursor = Cursor::new(code); + let code = util::read_spv(&mut cursor).expect("Failed to read SPV-V shader code"); + + let create_info = vk::ShaderModuleCreateInfo::builder() + .code(&code); -```c++ -void createGraphicsPipeline() { - auto vertShaderCode = readFile("shaders/vert.spv"); - auto fragShaderCode = readFile("shaders/frag.spv"); + unsafe { + self.device + .create_shader_module(&create_info, None) + .expect("Failed to create shader module!") + } } ``` -Make sure that the shaders are loaded correctly by printing the size of the -buffers and checking if they match the actual file size in bytes. Note that the code doesn't need to be null terminated since it's binary code and we will later be explicit about its size. - -## Creating shader modules - -Before we can pass the code to the pipeline, we have to wrap it in a -`VkShaderModule` object. Let's create a helper function `createShaderModule` to -do that. +이제 `create_shader_module` 함수를 `create_graphics_pipeline` 내에서 호출합니다. -```c++ -VkShaderModule createShaderModule(const std::vector& code) { +```rust +// In `VulkanApp::create_graphics_pipeline` +fn create_graphics_pipeline(&mut self) { + let vert_shader_code = std::fs::read("shaders/vert.spv") + .expect("Failed to read vertex shader file!"); + let frag_shader_code = std::fs::read("shaders/frag.spv") + .expect("Failed to read fragment shader file!"); + let vert_shader_module = self.create_shader_module(&vert_shader_code); + let frag_shader_module = self.create_shader_module(&frag_shader_code); + + // ... } ``` -The function will take a buffer with the bytecode as parameter and create a -`VkShaderModule` from it. - -Creating a shader module is simple, we only need to specify a pointer to the -buffer with the bytecode and the length of it. This information is specified in -a `VkShaderModuleCreateInfo` structure. The one catch is that the size of the -bytecode is specified in bytes, but the bytecode pointer is a `uint32_t` pointer -rather than a `char` pointer. Therefore we will need to cast the pointer with -`reinterpret_cast` as shown below. When you perform a cast like this, you also -need to ensure that the data satisfies the alignment requirements of `uint32_t`. -Lucky for us, the data is stored in an `std::vector` where the default allocator -already ensures that the data satisfies the worst case alignment requirements. - -```c++ -VkShaderModuleCreateInfo createInfo{}; -createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; -createInfo.codeSize = code.size(); -createInfo.pCode = reinterpret_cast(code.data()); -``` - -The `VkShaderModule` can then be created with a call to `vkCreateShaderModule`: - -```c++ -VkShaderModule shaderModule; -if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) { - throw std::runtime_error("failed to create shader module!"); -} -``` +셰이더 모듈은 우리가 이전에 파일에서 로드한 셰이더 바이트코드와 그 안에 정의된 함수들을 얇게 감싼 래퍼일 뿐입니다. SPIR-V 바이트코드를 GPU가 실행할 수 있는 기계어 코드로 컴파일하고 링크하는 작업은 그래픽 파이프라인이 생성될 때까지 일어나지 않습니다. -The parameters are the same as those in previous object creation functions: the -logical device, pointer to create info structure, optional pointer to custom -allocators and handle output variable. The buffer with the code can be freed -immediately after creating the shader module. Don't forget to return the created -shader module: +이는 파이프라인 생성이 완료되는 즉시 셰이더 모듈을 파괴해도 된다는 의미입니다. Rust의 소유권(ownership) 모델 덕분에 이 과정은 자동으로 처리됩니다. `vert_shader_module`과 `frag_shader_module` 변수는 `create_graphics_pipeline` 함수가 끝날 때 범위를 벗어나고, 이때 자동으로 `vkDestroyShaderModule`이 호출됩니다. C++ 예제처럼 수동으로 `vkDestroyShaderModule`을 호출할 필요가 없어 코드가 더 깔끔하고 안전합니다. -```c++ -return shaderModule; -``` +## 셰이더 스테이지 생성 -Shader modules are just a thin wrapper around the shader bytecode that we've previously loaded from a file and the functions defined in it. The compilation and linking of the SPIR-V bytecode to machine code for execution by the GPU doesn't happen until the graphics pipeline is created. That means that we're allowed to destroy the shader modules again as soon as pipeline creation is finished, which is why we'll make them local variables in the `createGraphicsPipeline` function instead of class members: +셰이더를 실제로 사용하려면 실제 파이프라인 생성 과정의 일부로서 `vk::PipelineShaderStageCreateInfo` 구조체를 통해 특정 파이프라인 단계에 할당해야 합니다. -```c++ -void createGraphicsPipeline() { - auto vertShaderCode = readFile("shaders/vert.spv"); - auto fragShaderCode = readFile("shaders/frag.spv"); +`create_graphics_pipeline` 함수에서 버텍스 셰이더를 위한 구조체를 채우는 것으로 시작하겠습니다. `ash`의 빌더 패턴을 사용하면 코드가 더 읽기 쉬워집니다. - VkShaderModule vertShaderModule = createShaderModule(vertShaderCode); - VkShaderModule fragShaderModule = createShaderModule(fragShaderCode); -``` +```rust +// ... inside create_graphics_pipeline, after creating shader modules -The cleanup should then happen at the end of the function by adding two calls to `vkDestroyShaderModule`. All of the remaining code in this chapter will be inserted before these lines. +// Vulkan은 C 문자열을 기대하므로 CString으로 변환해야 합니다. +let main_function_name = std::ffi::CString::new("main").unwrap(); -```c++ - ... - vkDestroyShaderModule(device, fragShaderModule, nullptr); - vkDestroyShaderModule(device, vertShaderModule, nullptr); -} +let vert_shader_stage_info = vk::PipelineShaderStageCreateInfo::builder() + .stage(vk::ShaderStageFlags::VERTEX) + .module(vert_shader_module) + .name(&main_function_name); ``` -## Shader stage creation +첫 번째 단계는 셰이더가 사용될 파이프라인 단계(`stage`)를 알려주는 것입니다. 여기서는 `VERTEX` 단계를 지정합니다. 그 다음 코드를 포함하는 셰이더 모듈(`module`)과 호출할 함수, 즉 *엔트리포인트(entrypoint)*의 이름(`name`)을 지정합니다. 여기서는 표준적인 `main`을 사용합니다. -To actually use the shaders we'll need to assign them to a specific pipeline stage through `VkPipelineShaderStageCreateInfo` structures as part of the actual pipeline creation process. +`pSpecializationInfo`라는 선택적 멤버도 있지만, 여기서는 사용하지 않으므로 빌더에서 설정하지 않으면 기본값인 `null`로 처리됩니다. -We'll start by filling in the structure for the vertex shader, again in the -`createGraphicsPipeline` function. +프래그먼트 셰이더에 맞게 구조체를 수정하는 것은 쉽습니다. -```c++ -VkPipelineShaderStageCreateInfo vertShaderStageInfo{}; -vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; -vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; +```rust +let frag_shader_stage_info = vk::PipelineShaderStageCreateInfo::builder() + .stage(vk::ShaderStageFlags::FRAGMENT) + .module(frag_shader_module) + .name(&main_function_name); ``` -The first step, besides the obligatory `sType` member, is telling Vulkan in -which pipeline stage the shader is going to be used. There is an enum value for -each of the programmable stages described in the previous chapter. - -```c++ -vertShaderStageInfo.module = vertShaderModule; -vertShaderStageInfo.pName = "main"; -``` +마지막으로 이 두 구조체를 포함하는 슬라이스를 정의합니다. 이 슬라이스는 나중에 실제 파이프라인 생성 단계에서 이들을 참조하는 데 사용될 것입니다. -The next two members specify the shader module containing the code, and the -function to invoke, known as the *entrypoint*. That means that it's possible to combine multiple fragment -shaders into a single shader module and use different entry points to -differentiate between their behaviors. In this case we'll stick to the standard -`main`, however. - -There is one more (optional) member, `pSpecializationInfo`, which we won't be -using here, but is worth discussing. It allows you to specify values for shader -constants. You can use a single shader module where its behavior can be -configured at pipeline creation by specifying different values for the constants -used in it. This is more efficient than configuring the shader using variables -at render time, because the compiler can do optimizations like eliminating `if` -statements that depend on these values. If you don't have any constants like -that, then you can set the member to `nullptr`, which our struct initialization -does automatically. - -Modifying the structure to suit the fragment shader is easy: - -```c++ -VkPipelineShaderStageCreateInfo fragShaderStageInfo{}; -fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; -fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT; -fragShaderStageInfo.module = fragShaderModule; -fragShaderStageInfo.pName = "main"; +```rust +let shader_stages = [vert_shader_stage_info.build(), frag_shader_stage_info.build()]; ``` +`.build()`를 호출하여 빌더를 최종 구조체로 변환하는 것을 잊지 마세요. -Finish by defining an array that contains these two structs, which we'll later -use to reference them in the actual pipeline creation step. +이제 파이프라인의 프로그래밍 가능 단계를 모두 기술했습니다. 함수 마지막에 셰이더 모듈을 수동으로 정리하는 코드를 추가합니다. -```c++ -VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; +```rust +// ... at the end of create_graphics_pipeline + // 셰이더 모듈은 더 이상 필요 없으므로 파이프라인 생성 후 즉시 파괴합니다. + unsafe { + self.device.destroy_shader_module(frag_shader_module, None); + self.device.destroy_shader_module(vert_shader_module, None); + } +} ``` +*참고: 위에서는 RAII의 장점을 설명했지만, 튜토리얼의 흐름상 C++ 코드와 동일하게 명시적으로 파괴하는 코드를 추가했습니다. Rust에서는 변수가 범위를 벗어날 때 자동으로 리소스가 해제되도록 래퍼 타입을 만들어 관리하는 것이 일반적입니다.* -That's all there is to describing the programmable stages of the pipeline. In -the next chapter we'll look at the fixed-function stages. +다음 장에서는 고정 함수 단계를 살펴보겠습니다. -[C++ code](/code/09_shader_modules.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +[Rust 코드](/rust_code/09_shader_modules.rs) / +[버텍스 셰이더](/code/09_shader_base.vert) / +[프래그먼트 셰이더](/code/09_shader_base.frag) \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md index 5b4bfdec..b0fa4a09 100644 --- a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md @@ -1,439 +1,229 @@ +이전의 그래픽 API들은 그래픽 파이프라인의 대부분 단계에 대한 기본 상태를 제공했습니다. Vulkan에서는 대부분의 파이프라인 상태를 명시적으로 지정해야 하며, 이 상태들은 불변(immutable)의 파이프라인 상태 객체(PSO)로 구워지기(baked) 때문입니다. 이번 장에서는 이러한 고정 함수(fixed-function) 연산을 구성하기 위한 모든 구조체를 채워 넣을 것입니다. Rust와 `ash` 라이브러리를 사용하면 빌더 패턴을 통해 이 과정을 더 명확하게 진행할 수 있습니다. -The older graphics APIs provided default state for most of the stages of the -graphics pipeline. In Vulkan you have to be explicit about most pipeline states as -it'll be baked into an immutable pipeline state object. In this chapter we'll fill -in all of the structures to configure these fixed-function operations. +## 동적 상태 (Dynamic state) -## Dynamic state +*대부분의* 파이프라인 상태는 파이프라인 상태 객체에 구워져야 하지만, 제한된 일부 상태는 파이프라인을 다시 만들지 않고도 드로우 타임(draw time)에 변경할 수 *있습니다*. 뷰포트의 크기, 선 두께, 블렌딩 상수 등이 그 예입니다. 만약 동적 상태를 사용하고 이러한 속성들을 파이프라인 생성 시에 고정하지 않으려면, `vk::PipelineDynamicStateCreateInfo` 구조체를 채워야 합니다. `ash`의 빌더를 사용하면 다음과 같이 작성할 수 있습니다. -While *most* of the pipeline state needs to be baked into the pipeline state, -a limited amount of the state *can* actually be changed without recreating the -pipeline at draw time. Examples are the size of the viewport, line width -and blend constants. If you want to use dynamic state and keep these properties out, -then you'll have to fill in a `VkPipelineDynamicStateCreateInfo` structure like this: +```rust +let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]; -```c++ -std::vector dynamicStates = { - VK_DYNAMIC_STATE_VIEWPORT, - VK_DYNAMIC_STATE_SCISSOR -}; - -VkPipelineDynamicStateCreateInfo dynamicState{}; -dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; -dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); -dynamicState.pDynamicStates = dynamicStates.data(); -``` - -This will cause the configuration of these values to be ignored and you will be -able (and required) to specify the data at drawing time. This results in a more flexible -setup and is very common for things like viewport and scissor state, which would -result in a more complex setup when being baked into the pipeline state. - -## Vertex input - -The `VkPipelineVertexInputStateCreateInfo` structure describes the format of the -vertex data that will be passed to the vertex shader. It describes this in -roughly two ways: - -* Bindings: spacing between data and whether the data is per-vertex or -per-instance (see [instancing](https://en.wikipedia.org/wiki/Geometry_instancing)) -* Attribute descriptions: type of the attributes passed to the vertex shader, -which binding to load them from and at which offset - -Because we're hard coding the vertex data directly in the vertex shader, we'll -fill in this structure to specify that there is no vertex data to load for now. -We'll get back to it in the vertex buffer chapter. - -```c++ -VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; -vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; -vertexInputInfo.vertexBindingDescriptionCount = 0; -vertexInputInfo.pVertexBindingDescriptions = nullptr; // Optional -vertexInputInfo.vertexAttributeDescriptionCount = 0; -vertexInputInfo.pVertexAttributeDescriptions = nullptr; // Optional -``` - -The `pVertexBindingDescriptions` and `pVertexAttributeDescriptions` members -point to an array of structs that describe the aforementioned details for -loading vertex data. Add this structure to the `createGraphicsPipeline` function -right after the `shaderStages` array. - -## Input assembly - -The `VkPipelineInputAssemblyStateCreateInfo` struct describes two things: what -kind of geometry will be drawn from the vertices and if primitive restart should -be enabled. The former is specified in the `topology` member and can have values -like: - -* `VK_PRIMITIVE_TOPOLOGY_POINT_LIST`: points from vertices -* `VK_PRIMITIVE_TOPOLOGY_LINE_LIST`: line from every 2 vertices without reuse -* `VK_PRIMITIVE_TOPOLOGY_LINE_STRIP`: the end vertex of every line is used as -start vertex for the next line -* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST`: triangle from every 3 vertices without -reuse -* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP `: the second and third vertex of every -triangle are used as first two vertices of the next triangle - -Normally, the vertices are loaded from the vertex buffer by index in sequential -order, but with an *element buffer* you can specify the indices to use yourself. -This allows you to perform optimizations like reusing vertices. If you set the -`primitiveRestartEnable` member to `VK_TRUE`, then it's possible to break up -lines and triangles in the `_STRIP` topology modes by using a special index of -`0xFFFF` or `0xFFFFFFFF`. - -We intend to draw triangles throughout this tutorial, so we'll stick to the -following data for the structure: - -```c++ -VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; -inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; -inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; -inputAssembly.primitiveRestartEnable = VK_FALSE; +let dynamic_state_info = vk::PipelineDynamicStateCreateInfo::builder() + .dynamic_states(&dynamic_states); ``` -## Viewports and scissors +`ash`의 빌더는 슬라이스(`&[T]`)를 받아 내부적으로 개수(`dynamicStateCount`)와 포인터(`pDynamicStates`)를 자동으로 설정해줍니다. 이렇게 하면 이 값들의 구성이 파이프라인 생성 시에는 무시되며, 드로잉 시점에 이 데이터를 지정해야만 합니다. 이는 더 유연한 설정을 가능하게 하며, 뷰포트나 시저 상태처럼 파이프라인 상태에 고정시킬 경우 설정이 더 복잡해질 수 있는 항목들에 대해 매우 일반적인 방식입니다. -A viewport basically describes the region of the framebuffer that the output -will be rendered to. This will almost always be `(0, 0)` to `(width, height)` -and in this tutorial that will also be the case. +## 정점 입력 (Vertex input) -```c++ -VkViewport viewport{}; -viewport.x = 0.0f; -viewport.y = 0.0f; -viewport.width = (float) swapChainExtent.width; -viewport.height = (float) swapChainExtent.height; -viewport.minDepth = 0.0f; -viewport.maxDepth = 1.0f; -``` - -Remember that the size of the swap chain and its images may differ from the -`WIDTH` and `HEIGHT` of the window. The swap chain images will be used as -framebuffers later on, so we should stick to their size. - -The `minDepth` and `maxDepth` values specify the range of depth values to use -for the framebuffer. These values must be within the `[0.0f, 1.0f]` range, but -`minDepth` may be higher than `maxDepth`. If you aren't doing anything special, -then you should stick to the standard values of `0.0f` and `1.0f`. +`vk::PipelineVertexInputStateCreateInfo` 구조체는 정점 셰이더로 전달될 정점 데이터의 형식을 설명합니다. 이 설명은 크게 두 가지 방식으로 이루어집니다. -While viewports define the transformation from the image to the framebuffer, -scissor rectangles define in which regions pixels will actually be stored. Any -pixels outside the scissor rectangles will be discarded by the rasterizer. They -function like a filter rather than a transformation. The difference is -illustrated below. Note that the left scissor rectangle is just one of the many -possibilities that would result in that image, as long as it's larger than the -viewport. +* **바인딩(Bindings)**: 데이터 간의 간격 및 데이터가 정점별(per-vertex)인지 인스턴스별(per-instance)인지 여부 (자세한 내용은 [인스턴싱](https://ko.wikipedia.org/wiki/인스턴싱) 참조) +* **속성 서술(Attribute descriptions)**: 정점 셰이더로 전달되는 속성의 유형, 어떤 바인딩에서 로드할지, 그리고 어떤 오프셋에 있는지 -![](/images/viewports_scissors.png) +지금은 정점 데이터를 정점 셰이더에 직접 하드코딩하고 있으므로, 이 구조체를 채워서 로드할 정점 데이터가 없음을 명시할 것입니다. `ash` 빌더의 기본값은 비어있는 상태이므로 코드가 매우 간결해집니다. -So if we wanted to draw to the entire framebuffer, we would specify a scissor rectangle that covers it entirely: - -```c++ -VkRect2D scissor{}; -scissor.offset = {0, 0}; -scissor.extent = swapChainExtent; +```rust +let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::builder() + .vertex_binding_descriptions(&[]) + .vertex_attribute_descriptions(&[]); ``` -Viewport(s) and scissor rectangle(s) can either be specified as a static part of the pipeline or as a [dynamic state](#dynamic-state) set in the command buffer. While the former is more in line with the other states it's often convenient to make viewport and scissor state dynamic as it gives you a lot more flexibility. This is very common and all implementations can handle this dynamic state without a performance penalty. +`vertex_binding_descriptions`와 `vertex_attribute_descriptions` 메서드에 빈 슬라이스(`&[]`)를 전달하면, `ash`가 자동으로 카운트를 0으로, 포인터는 널(null)로 설정합니다. 이 구조체는 나중에 정점 버퍼 장에서 다시 다룰 것입니다. `create_graphics_pipeline` 함수에서 셰이더 단계 정의 바로 다음에 이 코드를 추가하세요. -When opting for dynamic viewport(s) and scissor rectangle(s) you need to enable the respective dynamic states for the pipeline: +## 입력 조립 (Input assembly) -```c++ -std::vector dynamicStates = { - VK_DYNAMIC_STATE_VIEWPORT, - VK_DYNAMIC_STATE_SCISSOR -}; +`vk::PipelineInputAssemblyStateCreateInfo` 구조체는 정점들로부터 어떤 종류의 지오메트리를 그릴 것인지와 프리미티브 재시작(primitive restart) 활성화 여부를 설명합니다. -VkPipelineDynamicStateCreateInfo dynamicState{}; -dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; -dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); -dynamicState.pDynamicStates = dynamicStates.data(); -``` +사용 가능한 `topology`의 종류는 다음과 같습니다: -And then you only need to specify their count at pipeline creation time: +* `vk::PrimitiveTopology::POINT_LIST`: 점 +* `vk::PrimitiveTopology::LINE_LIST`: 선 (재사용 없음) +* `vk::PrimitiveTopology::LINE_STRIP`: 연결된 선 +* `vk::PrimitiveTopology::TRIANGLE_LIST`: 삼각형 (재사용 없음) +* `vk::PrimitiveTopology::TRIANGLE_STRIP `: 연결된 삼각형 -```c++ -VkPipelineViewportStateCreateInfo viewportState{}; -viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; -viewportState.viewportCount = 1; -viewportState.scissorCount = 1; -``` - -The actual viewport(s) and scissor rectangle(s) will then later be set up at drawing time. - -With dynamic state it's even possible to specify different viewports and or scissor rectangles within a single command buffer. +이 튜토리얼에서는 삼각형을 그릴 것이므로 다음과 같이 구조체를 설정합니다. -Without dynamic state, the viewport and scissor rectangle need to be set in the pipeline using the `VkPipelineViewportStateCreateInfo` struct. This makes the viewport and scissor rectangle for this pipeline immutable. -Any changes required to these values would require a new pipeline to be created with the new values. - -```c++ -VkPipelineViewportStateCreateInfo viewportState{}; -viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; -viewportState.viewportCount = 1; -viewportState.pViewports = &viewport; -viewportState.scissorCount = 1; -viewportState.pScissors = &scissor; +```rust +let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::builder() + .topology(vk::PrimitiveTopology::TRIANGLE_LIST) + .primitive_restart_enable(false); ``` -Independent of how you set them, it's possible to use multiple viewports and scissor rectangles on some graphics cards, so the structure members reference an array of them. Using multiple requires enabling a GPU feature (see logical device creation). - -## Rasterizer +`primitive_restart_enable`을 `true`로 설정하면, `_STRIP` 토폴로지 모드에서 특수 인덱스(`0xFFFF` 또는 `0xFFFFFFFF`)를 사용하여 프리미티브를 끊을 수 있습니다. -The rasterizer takes the geometry that is shaped by the vertices from the vertex -shader and turns it into fragments to be colored by the fragment shader. It also -performs [depth testing](https://en.wikipedia.org/wiki/Z-buffering), -[face culling](https://en.wikipedia.org/wiki/Back-face_culling) and the scissor -test, and it can be configured to output fragments that fill entire polygons or -just the edges (wireframe rendering). All this is configured using the -`VkPipelineRasterizationStateCreateInfo` structure. +## 뷰포트와 시저 (Viewports and scissors) -```c++ -VkPipelineRasterizationStateCreateInfo rasterizer{}; -rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; -rasterizer.depthClampEnable = VK_FALSE; -``` +뷰포트(viewport)는 출력이 렌더링될 프레임버퍼의 영역을 설명합니다. 시저 사각형(scissor rectangle)은 픽셀이 실제로 저장될 영역을 정의하며, 이 영역 밖의 픽셀은 버려집니다. -If `depthClampEnable` is set to `VK_TRUE`, then fragments that are beyond the -near and far planes are clamped to them as opposed to discarding them. This is -useful in some special cases like shadow maps. Using this requires enabling a -GPU feature. - -```c++ -rasterizer.rasterizerDiscardEnable = VK_FALSE; -``` - -If `rasterizerDiscardEnable` is set to `VK_TRUE`, then geometry never passes -through the rasterizer stage. This basically disables any output to the -framebuffer. +```rust +let viewport = vk::Viewport { + x: 0.0, + y: 0.0, + width: self.swapchain_extent.width as f32, + height: self.swapchain_extent.height as f32, + min_depth: 0.0, + max_depth: 1.0, +}; -```c++ -rasterizer.polygonMode = VK_POLYGON_MODE_FILL; +let scissor = vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: self.swapchain_extent, +}; ``` -The `polygonMode` determines how fragments are generated for geometry. The -following modes are available: +뷰포트와 시저는 정적 또는 동적 상태로 설정할 수 있습니다. 동적 상태를 사용하는 것이 일반적이며 성능 저하도 없습니다. -* `VK_POLYGON_MODE_FILL`: fill the area of the polygon with fragments -* `VK_POLYGON_MODE_LINE`: polygon edges are drawn as lines -* `VK_POLYGON_MODE_POINT`: polygon vertices are drawn as points +동적 상태를 사용할 경우, 앞서 정의한 `dynamic_state_info`가 이 역할을 합니다. 파이프라인 생성 시에는 뷰포트와 시저의 개수만 지정하면 됩니다. -Using any mode other than fill requires enabling a GPU feature. - -```c++ -rasterizer.lineWidth = 1.0f; -``` - -The `lineWidth` member is straightforward, it describes the thickness of lines -in terms of number of fragments. The maximum line width that is supported -depends on the hardware and any line thicker than `1.0f` requires you to enable -the `wideLines` GPU feature. - -```c++ -rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; -rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE; +```rust +let viewport_state = vk::PipelineViewportStateCreateInfo::builder() + .viewport_count(1) + .scissor_count(1); ``` -The `cullMode` variable determines the type of face culling to use. You can -disable culling, cull the front faces, cull the back faces or both. The -`frontFace` variable specifies the vertex order for faces to be considered -front-facing and can be clockwise or counterclockwise. +이 경우 실제 `viewport`와 `scissor` 데이터는 나중에 커맨드 버퍼에 직접 기록해야 합니다. -```c++ -rasterizer.depthBiasEnable = VK_FALSE; -rasterizer.depthBiasConstantFactor = 0.0f; // Optional -rasterizer.depthBiasClamp = 0.0f; // Optional -rasterizer.depthBiasSlopeFactor = 0.0f; // Optional -``` +만약 동적 상태를 사용하지 않고 파이프라인에 이 값들을 고정시키려면, 빌더에 실제 데이터를 전달해야 합니다. -The rasterizer can alter the depth values by adding a constant value or biasing -them based on a fragment's slope. This is sometimes used for shadow mapping, but -we won't be using it. Just set `depthBiasEnable` to `VK_FALSE`. - -## Multisampling - -The `VkPipelineMultisampleStateCreateInfo` struct configures multisampling, -which is one of the ways to perform [anti-aliasing](https://en.wikipedia.org/wiki/Multisample_anti-aliasing). -It works by combining the fragment shader results of multiple polygons that -rasterize to the same pixel. This mainly occurs along edges, which is also where -the most noticeable aliasing artifacts occur. Because it doesn't need to run the -fragment shader multiple times if only one polygon maps to a pixel, it is -significantly less expensive than simply rendering to a higher resolution and -then downscaling. Enabling it requires enabling a GPU feature. - -```c++ -VkPipelineMultisampleStateCreateInfo multisampling{}; -multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; -multisampling.sampleShadingEnable = VK_FALSE; -multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; -multisampling.minSampleShading = 1.0f; // Optional -multisampling.pSampleMask = nullptr; // Optional -multisampling.alphaToCoverageEnable = VK_FALSE; // Optional -multisampling.alphaToOneEnable = VK_FALSE; // Optional +```rust +// 동적 상태를 사용하지 않을 경우 +let viewport_state = vk::PipelineViewportStateCreateInfo::builder() + .viewports(&[viewport]) + .scissors(&[scissor]); ``` +`ash` 빌더의 `.viewports()`와 `.scissors()` 메서드는 슬라이스를 인자로 받습니다. 우리는 동적 상태를 사용할 것이므로 위의 첫 번째 방법을 따릅니다. -We'll revisit multisampling in later chapter, for now let's keep it disabled. - -## Depth and stencil testing +## 래스터라이저 (Rasterizer) -If you are using a depth and/or stencil buffer, then you also need to configure -the depth and stencil tests using `VkPipelineDepthStencilStateCreateInfo`. We -don't have one right now, so we can simply pass a `nullptr` instead of a pointer -to such a struct. We'll get back to it in the depth buffering chapter. +래스터라이저는 정점 셰이더가 만든 지오메트리를 프래그먼트로 변환합니다. `vk::PipelineRasterizationStateCreateInfo`로 이 단계를 설정합니다. -## Color blending - -After a fragment shader has returned a color, it needs to be combined with the -color that is already in the framebuffer. This transformation is known as color -blending and there are two ways to do it: - -* Mix the old and new value to produce a final color -* Combine the old and new value using a bitwise operation - -There are two types of structs to configure color blending. The first struct, -`VkPipelineColorBlendAttachmentState` contains the configuration per attached -framebuffer and the second struct, `VkPipelineColorBlendStateCreateInfo` -contains the *global* color blending settings. In our case we only have one -framebuffer: - -```c++ -VkPipelineColorBlendAttachmentState colorBlendAttachment{}; -colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; -colorBlendAttachment.blendEnable = VK_FALSE; -colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; // Optional -colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional -colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; // Optional -colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; // Optional -colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional -colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // Optional +```rust +let rasterizer = vk::PipelineRasterizationStateCreateInfo::builder() + .depth_clamp_enable(false) + .rasterizer_discard_enable(false) + .polygon_mode(vk::PolygonMode::FILL) + .line_width(1.0) + .cull_mode(vk::CullModeFlags::BACK) + .front_face(vk::FrontFace::CLOCKWISE) + .depth_bias_enable(false) + .depth_bias_constant_factor(0.0) // Optional + .depth_bias_clamp(0.0) // Optional + .depth_bias_slope_factor(0.0); // Optional ``` +* `depth_clamp_enable`: `true`이면 깊이 범위를 벗어난 프래그먼트를 버리는 대신 클램핑합니다 (GPU 기능 필요). +* `rasterizer_discard_enable`: `true`이면 지오메트리가 래스터라이저를 통과하지 않아 프레임버퍼에 아무것도 출력되지 않습니다. +* `polygon_mode`: `FILL`(채우기), `LINE`(선), `POINT`(점) 모드를 설정합니다 (`FILL` 외에는 GPU 기능 필요). +* `line_width`: 선의 두께입니다 (`1.0f` 초과는 GPU 기능 필요). +* `cull_mode`, `front_face`: 면 컬링(face culling) 방식과 앞면(front-face)으로 간주할 정점 순서(시계/반시계 방향)를 결정합니다. +* `depth_bias...`: 섀도우 매핑 등에서 깊이 값에 편향을 줄 때 사용합니다. -This per-framebuffer struct allows you to configure the first way of color -blending. The operations that will be performed are best demonstrated using the -following pseudocode: +## 멀티샘플링 (Multisampling) -```c++ -if (blendEnable) { - finalColor.rgb = (srcColorBlendFactor * newColor.rgb) (dstColorBlendFactor * oldColor.rgb); - finalColor.a = (srcAlphaBlendFactor * newColor.a) (dstAlphaBlendFactor * oldColor.a); -} else { - finalColor = newColor; -} +`vk::PipelineMultisampleStateCreateInfo`는 안티 앨리어싱 기법 중 하나인 멀티샘플링을 설정합니다. 지금은 비활성화합니다 (GPU 기능 필요). -finalColor = finalColor & colorWriteMask; +```rust +let multisampling = vk::PipelineMultisampleStateCreateInfo::builder() + .sample_shading_enable(false) + .rasterization_samples(vk::SampleCountFlags::TYPE_1) + .min_sample_shading(1.0) // Optional + .sample_mask(&[]) // Optional + .alpha_to_coverage_enable(false) // Optional + .alpha_to_one_enable(false); // Optional ``` +이 부분은 나중 장에서 다시 다룰 것입니다. 지금은 샘플링을 1회만 수행하도록 설정합니다. -If `blendEnable` is set to `VK_FALSE`, then the new color from the fragment -shader is passed through unmodified. Otherwise, the two mixing operations are -performed to compute a new color. The resulting color is AND'd with the -`colorWriteMask` to determine which channels are actually passed through. +## 깊이 및 스텐실 테스팅 (Depth and stencil testing) -The most common way to use color blending is to implement alpha blending, where -we want the new color to be blended with the old color based on its opacity. The -`finalColor` should then be computed as follows: +깊이/스텐실 버퍼를 사용한다면 `vk::PipelineDepthStencilStateCreateInfo`로 관련 테스트를 설정해야 합니다. 지금은 사용하지 않으므로, 최종 파이프라인 생성 정보에 이 부분은 널 포인터(`std::ptr::null()`)를 전달하여 비활성화할 것입니다. -```c++ -finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor; -finalColor.a = newAlpha.a; -``` +## 색상 혼합 (Color blending) -This can be accomplished with the following parameters: +프래그먼트 셰이더가 반환한 색상을 프레임버퍼의 기존 색상과 결합하는 단계입니다. 프레임버퍼별로 `vk::PipelineColorBlendAttachmentState`를, 전역적으로 `vk::PipelineColorBlendStateCreateInfo`를 설정합니다. -```c++ -colorBlendAttachment.blendEnable = VK_TRUE; -colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; -colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; -colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; -colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; -colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; -colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; -``` +먼저, 단일 프레임버퍼에 대한 혼합 상태입니다. 혼합을 비활성화하면 프래그먼트 셰이더의 출력이 그대로 프레임버퍼에 쓰입니다. -You can find all of the possible operations in the `VkBlendFactor` and -`VkBlendOp` enumerations in the specification. - -The second structure references the array of structures for all of the -framebuffers and allows you to set blend constants that you can use as blend -factors in the aforementioned calculations. - -```c++ -VkPipelineColorBlendStateCreateInfo colorBlending{}; -colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; -colorBlending.logicOpEnable = VK_FALSE; -colorBlending.logicOp = VK_LOGIC_OP_COPY; // Optional -colorBlending.attachmentCount = 1; -colorBlending.pAttachments = &colorBlendAttachment; -colorBlending.blendConstants[0] = 0.0f; // Optional -colorBlending.blendConstants[1] = 0.0f; // Optional -colorBlending.blendConstants[2] = 0.0f; // Optional -colorBlending.blendConstants[3] = 0.0f; // Optional +```rust +let color_blend_attachment = vk::PipelineColorBlendAttachmentState::builder() + .color_write_mask(vk::ColorComponentFlags::RGBA) + .blend_enable(false) + .src_color_blend_factor(vk::BlendFactor::ONE) // Optional + .dst_color_blend_factor(vk::BlendFactor::ZERO) // Optional + .color_blend_op(vk::BlendOp::ADD) // Optional + .src_alpha_blend_factor(vk::BlendFactor::ONE) // Optional + .dst_alpha_blend_factor(vk::BlendFactor::ZERO) // Optional + .alpha_blend_op(vk::BlendOp::ADD); // Optional ``` +`ash`에서는 `vk::ColorComponentFlags::R | G | B | A` 대신 `vk::ColorComponentFlags::RGBA`라는 편리한 상수를 제공합니다. -If you want to use the second method of blending (bitwise combination), then you -should set `logicOpEnable` to `VK_TRUE`. The bitwise operation can then be -specified in the `logicOp` field. Note that this will automatically disable the -first method, as if you had set `blendEnable` to `VK_FALSE` for every -attached framebuffer! The `colorWriteMask` will also be used in this mode to -determine which channels in the framebuffer will actually be affected. It is -also possible to disable both modes, as we've done here, in which case the -fragment colors will be written to the framebuffer unmodified. - -## Pipeline layout - -You can use `uniform` values in shaders, which are globals similar to dynamic -state variables that can be changed at drawing time to alter the behavior of -your shaders without having to recreate them. They are commonly used to pass the -transformation matrix to the vertex shader, or to create texture samplers in the -fragment shader. - -These uniform values need to be specified during pipeline creation by creating a -`VkPipelineLayout` object. Even though we won't be using them until a future -chapter, we are still required to create an empty pipeline layout. - -Create a class member to hold this object, because we'll refer to it from other -functions at a later point in time: - -```c++ -VkPipelineLayout pipelineLayout; +알파 블렌딩(반투명 효과)을 구현하려면 `blend_enable`을 `true`로 설정하고 관련 인자들을 다음과 같이 조정해야 합니다. +`finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor;` +```rust +// 알파 블렌딩 예시 +let color_blend_attachment_alpha = vk::PipelineColorBlendAttachmentState::builder() + .color_write_mask(vk::ColorComponentFlags::RGBA) + .blend_enable(true) + .src_color_blend_factor(vk::BlendFactor::SRC_ALPHA) + .dst_color_blend_factor(vk::BlendFactor::ONE_MINUS_SRC_ALPHA) + .color_blend_op(vk::BlendOp::ADD) + .src_alpha_blend_factor(vk::BlendFactor::ONE) + .dst_alpha_blend_factor(vk::BlendFactor::ZERO) + .alpha_blend_op(vk::BlendOp::ADD); ``` -And then create the object in the `createGraphicsPipeline` function: +이제 전역 색상 혼합 상태를 설정합니다. 위에서 만든 첨부 상태(attachment state)를 참조합니다. -```c++ -VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; -pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; -pipelineLayoutInfo.setLayoutCount = 0; // Optional -pipelineLayoutInfo.pSetLayouts = nullptr; // Optional -pipelineLayoutInfo.pushConstantRangeCount = 0; // Optional -pipelineLayoutInfo.pPushConstantRanges = nullptr; // Optional - -if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) { - throw std::runtime_error("failed to create pipeline layout!"); +```rust +let color_blend_attachment_states = [color_blend_attachment.build()]; +let color_blending = vk::PipelineColorBlendStateCreateInfo::builder() + .logic_op_enable(false) + .logic_op(vk::LogicOp::COPY) // Optional + .attachments(&color_blend_attachment_states) + .blend_constants([0.0, 0.0, 0.0, 0.0]); // Optional +``` +`logic_op_enable`을 `true`로 설정하면 전통적인 혼합 대신 비트 연산을 사용하게 됩니다. 우리는 두 방식 모두 비활성화하여, 프래그먼트 색상이 수정 없이 프레임버퍼에 쓰이도록 합니다. + +## 파이프라인 레이아웃 (Pipeline layout) + +셰이더에서 사용하는 `uniform` 값(예: 변환 행렬, 텍스처 샘플러)을 파이프라인에 바인딩하기 위해 `vk::PipelineLayout` 객체가 필요합니다. 지금 당장 uniform을 사용하지 않더라도, 비어 있는 파이프라인 레이아웃을 반드시 생성해야 합니다. + +`App` 구조체에 필드를 추가하여 이 객체를 저장합니다. +```rust +struct App { + // ... + pipeline_layout: vk::PipelineLayout, + // ... } ``` -The structure also specifies *push constants*, which are another way of passing -dynamic values to shaders that we may get into in a future chapter. The pipeline -layout will be referenced throughout the program's lifetime, so it should be -destroyed at the end: +`create_graphics_pipeline` 함수 내에서 객체를 생성합니다. +```rust +let pipeline_layout_info = vk::PipelineLayoutCreateInfo::builder() + .set_layouts(&[]) + .push_constant_ranges(&[]); -```c++ -void cleanup() { - vkDestroyPipelineLayout(device, pipelineLayout, nullptr); - ... +self.pipeline_layout = unsafe { + self.device + .create_pipeline_layout(&pipeline_layout_info, None) + .expect("Failed to create pipeline layout!") +}; +``` +`ash`의 빌더는 빈 슬라이스를 처리하여 `setLayoutCount`와 `pushConstantRangeCount`를 0으로 설정합니다. `create_pipeline_layout`은 `unsafe` 함수이며 `Result`를 반환하므로, `expect`로 오류를 처리합니다. + +파이프라인 레이아웃은 프로그램이 실행되는 동안 계속 사용되므로, 프로그램 종료 시 파괴해야 합니다. 이는 Rust의 `Drop` 트레이트를 구현하여 관리하는 것이 가장 이상적입니다. +```rust +impl Drop for App { + fn drop(&mut self) { + unsafe { + self.device.destroy_pipeline_layout(self.pipeline_layout, None); + // ... other cleanup ... + } + } } ``` -## Conclusion - -That's it for all of the fixed-function state! It's a lot of work to set all of -this up from scratch, but the advantage is that we're now nearly fully aware of -everything that is going on in the graphics pipeline! This reduces the chance of -running into unexpected behavior because the default state of certain components -is not what you expect. +## 결론 -There is however one more object to create before we can finally create the -graphics pipeline and that is a [render pass](!en/Drawing_a_triangle/Graphics_pipeline_basics/Render_passes). +이것으로 모든 고정 함수 상태 설정이 끝났습니다! 처음부터 모든 것을 설정하는 것은 많은 작업이지만, 그 장점은 이제 그래픽 파이프라인에서 일어나는 거의 모든 일을 완전히 인지하게 되었다는 것입니다. 이는 특정 컴포넌트의 기본 상태가 예상과 달라 예기치 않은 동작에 부딪힐 가능성을 줄여줍니다. -[C++ code](/code/10_fixed_functions.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +하지만 그래픽 파이프라인을 최종적으로 생성하기 전에 만들어야 할 객체가 하나 더 있으며, 그것은 바로 [렌더 패스](!en/Drawing_a_triangle/Graphics_pipeline_basics/Render_passes)입니다. \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md index a635d32f..8c9bdaba 100644 --- a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md @@ -1,215 +1,137 @@ -## Setup - -Before we can finish creating the pipeline, we need to tell Vulkan about the -framebuffer attachments that will be used while rendering. We need to specify -how many color and depth buffers there will be, how many samples to use for each -of them and how their contents should be handled throughout the rendering -operations. All of this information is wrapped in a *render pass* object, for -which we'll create a new `createRenderPass` function. Call this function from -`initVulkan` before `createGraphicsPipeline`. - -```c++ -void initVulkan() { - createInstance(); - setupDebugMessenger(); - createSurface(); - pickPhysicalDevice(); - createLogicalDevice(); - createSwapChain(); - createImageViews(); - createRenderPass(); - createGraphicsPipeline(); -} +## 설정 -... +파이프라인 생성을 완료하기 전에, Vulkan에게 렌더링 중에 사용될 프레임버퍼 어태치먼트(attachment)에 대해 알려주어야 합니다. 우리는 몇 개의 색상 및 깊이 버퍼가 있을지, 각각에 몇 개의 샘플을 사용할지, 그리고 렌더링 작업 전반에 걸쳐 해당 콘텐츠를 어떻게 처리해야 하는지 지정해야 합니다. 이 모든 정보는 *렌더 패스(render pass)* 객체에 담기게 되며, 이를 위해 새로운 `create_render_pass` 함수를 만들 것입니다. 이 함수를 주 애플리케이션 초기화 로직에서 `create_graphics_pipeline` 앞에 호출하세요. -void createRenderPass() { +```rust +// 애플리케이션 초기화 함수 내에서... +self.create_swapchain(); +self.create_image_views(); +self.create_render_pass(); +self.create_graphics_pipeline(); +... + +// 애플리케이션 구현(impl) 블록 내에 함수 추가 +fn create_render_pass(&mut self) -> Result<(), Box> { + // ... 구현 ... + Ok(()) } ``` -## Attachment description +## 어태치먼트 명세 (Attachment description) + +우리의 경우, 스왑체인의 이미지 중 하나로 표현되는 단일 색상 버퍼 어태치먼트만 갖게 될 것입니다. Ash에서는 빌더(builder) 패턴을 사용하여 구조체를 명확하고 안전하게 생성하는 것이 일반적입니다. + +```rust +// create_render_pass 함수 내에서 +let color_attachment = vk::AttachmentDescription::builder() + .format(self.swapchain_format) + .samples(vk::SampleCountFlags::TYPE_1) + .load_op(vk::AttachmentLoadOp::CLEAR) + .store_op(vk::AttachmentStoreOp::STORE) + .stencil_load_op(vk::AttachmentLoadOp::DONT_CARE) + .stencil_store_op(vk::AttachmentStoreOp::DONT_CARE) + .initial_layout(vk::ImageLayout::UNDEFINED) + .final_layout(vk::ImageLayout::PRESENT_SRC_KHR) + .build(); +``` -In our case we'll have just a single color buffer attachment represented by one -of the images from the swap chain. +색상 어태치먼트의 `format`은 스왑체인 이미지의 포맷과 일치해야 하며, 아직 멀티샘플링은 다루지 않으므로 `vk::SampleCountFlags::TYPE_1` 샘플을 사용하겠습니다. -```c++ -void createRenderPass() { - VkAttachmentDescription colorAttachment{}; - colorAttachment.format = swapChainImageFormat; - colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; -} -``` +`load_op`와 `store_op`는 렌더링 전후에 어태치먼트의 데이터를 어떻게 처리할지를 결정합니다. `load_op`에 사용할 수 있는 선택지는 다음과 같습니다: -The `format` of the color attachment should match the format of the swap chain -images, and we're not doing anything with multisampling yet, so we'll stick to 1 -sample. +* `vk::AttachmentLoadOp::LOAD`: 어태치먼트의 기존 내용을 보존합니다. +* `vk::AttachmentLoadOp::CLEAR`: 시작 시 값을 특정 상수로 지웁니다. +* `vk::AttachmentLoadOp::DONT_CARE`: 기존 내용이 정의되지 않음(undefined); 신경 쓰지 않습니다. -```c++ -colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; -colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; -``` +우리의 경우, 새 프레임을 그리기 전에 프레임버퍼를 검은색으로 지우기 위해 clear 작업을 사용할 것입니다. `store_op`에는 두 가지 가능성만 있습니다: -The `loadOp` and `storeOp` determine what to do with the data in the attachment -before rendering and after rendering. We have the following choices for -`loadOp`: +* `vk::AttachmentStoreOp::STORE`: 렌더링된 내용은 메모리에 저장되어 나중에 읽을 수 있습니다. +* `vk::AttachmentStoreOp::DONT_CARE`: 렌더링 작업 후 프레임버퍼의 내용은 정의되지 않습니다. -* `VK_ATTACHMENT_LOAD_OP_LOAD`: Preserve the existing contents of the attachment -* `VK_ATTACHMENT_LOAD_OP_CLEAR`: Clear the values to a constant at the start -* `VK_ATTACHMENT_LOAD_OP_DONT_CARE`: Existing contents are undefined; we don't -care about them +우리는 렌더링된 삼각형을 화면에서 보고 싶으므로, store 작업을 사용할 것입니다. -In our case we're going to use the clear operation to clear the framebuffer to -black before drawing a new frame. There are only two possibilities for the -`storeOp`: +`load_op`와 `store_op`는 색상 및 깊이 데이터에 적용되며, `stencil_load_op` / `stencil_store_op`는 스텐실 데이터에 적용됩니다. 우리 애플리케이션은 스텐실 버퍼를 사용하지 않으므로, 로딩과 저장 결과는 중요하지 않습니다. -* `VK_ATTACHMENT_STORE_OP_STORE`: Rendered contents will be stored in memory and -can be read later -* `VK_ATTACHMENT_STORE_OP_DONT_CARE`: Contents of the framebuffer will be -undefined after the rendering operation +Vulkan에서 텍스처와 프레임버퍼는 특정 픽셀 포맷을 가진 `VkImage` 객체로 표현됩니다. 하지만 이미지로 무엇을 하려는지에 따라 메모리 내 픽셀의 레이아웃이 변경될 수 있습니다. -We're interested in seeing the rendered triangle on the screen, so we're going -with the store operation here. +가장 일반적인 레이아웃 중 일부는 다음과 같습니다: -```c++ -colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; -colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; -``` +* `vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL`: 색상 어태치먼트로 사용되는 이미지 +* `vk::ImageLayout::PRESENT_SRC_KHR`: 스왑체인에 표시(present)될 이미지 +* `vk::ImageLayout::TRANSFER_DST_OPTIMAL`: 메모리 복사 작업의 대상으로 사용될 이미지 -The `loadOp` and `storeOp` apply to color and depth data, and `stencilLoadOp` / -`stencilStoreOp` apply to stencil data. Our application won't do anything with -the stencil buffer, so the results of loading and storing are irrelevant. +`initial_layout`은 렌더 패스가 시작되기 전에 이미지가 어떤 레이아웃을 가질지 지정합니다. `final_layout`은 렌더 패스가 끝날 때 자동으로 전환될 레이아웃을 지정합니다. `initial_layout`에 `vk::ImageLayout::UNDEFINED`를 사용하면 이미지의 이전 레이아웃이 무엇이었는지 신경 쓰지 않겠다는 의미입니다. 우리는 렌더링 후에 이미지가 스왑체인을 통해 화면에 표시될 준비가 되기를 원하므로, `final_layout`으로 `vk::ImageLayout::PRESENT_SRC_KHR`을 사용합니다. -```c++ -colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; -colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; -``` +## 서브패스와 어태치먼트 참조 -Textures and framebuffers in Vulkan are represented by `VkImage` objects with a -certain pixel format, however the layout of the pixels in memory can change -based on what you're trying to do with an image. - -Some of the most common layouts are: - -* `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`: Images used as color attachment -* `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`: Images to be presented in the swap chain -* `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`: Images to be used as destination for a -memory copy operation - -We'll discuss this topic in more depth in the texturing chapter, but what's -important to know right now is that images need to be transitioned to specific -layouts that are suitable for the operation that they're going to be involved in -next. - -The `initialLayout` specifies which layout the image will have before the render -pass begins. The `finalLayout` specifies the layout to automatically transition -to when the render pass finishes. Using `VK_IMAGE_LAYOUT_UNDEFINED` for -`initialLayout` means that we don't care what previous layout the image was in. -The caveat of this special value is that the contents of the image are not -guaranteed to be preserved, but that doesn't matter since we're going to clear -it anyway. We want the image to be ready for presentation using the swap chain -after rendering, which is why we use `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR` as -`finalLayout`. - -## Subpasses and attachment references - -A single render pass can consist of multiple subpasses. Subpasses are subsequent -rendering operations that depend on the contents of framebuffers in previous -passes, for example a sequence of post-processing effects that are applied one -after another. If you group these rendering operations into one render pass, -then Vulkan is able to reorder the operations and conserve memory bandwidth for -possibly better performance. For our very first triangle, however, we'll stick -to a single subpass. - -Every subpass references one or more of the attachments that we've described -using the structure in the previous sections. These references are themselves -`VkAttachmentReference` structs that look like this: - -```c++ -VkAttachmentReference colorAttachmentRef{}; -colorAttachmentRef.attachment = 0; -colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; +하나의 렌더 패스는 여러 개의 서브패스(subpass)로 구성될 수 있습니다. 서브패스는 이전 패스의 프레임버퍼 내용에 의존하는 후속 렌더링 작업입니다. 우리의 첫 번째 삼각형에서는 단일 서브패스만 사용할 것입니다. + +모든 서브패스는 어태치먼트 명세 중 하나 이상을 참조합니다. 이러한 참조는 `vk::AttachmentReference` 구조체를 통해 이루어집니다. + +```rust +let color_attachment_ref = vk::AttachmentReference::builder() + .attachment(0) + .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .build(); ``` -The `attachment` parameter specifies which attachment to reference by its index -in the attachment descriptions array. Our array consists of a single -`VkAttachmentDescription`, so its index is `0`. The `layout` specifies which -layout we would like the attachment to have during a subpass that uses this -reference. Vulkan will automatically transition the attachment to this layout -when the subpass is started. We intend to use the attachment to function as a -color buffer and the `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL` layout will give -us the best performance, as its name implies. +`attachment` 파라미터는 어태치먼트 명세 배열의 인덱스(`0`)를 통해 참조할 어태치먼트를 지정합니다. `layout`은 이 참조를 사용하는 서브패스 동안 어태치먼트가 가지길 원하는 레이아웃을 지정합니다. 우리는 이 어태치먼트를 색상 버퍼로 사용할 것이며, `vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL` 레이아웃이 최상의 성능을 제공할 것입니다. -The subpass is described using a `VkSubpassDescription` structure: +서브패스는 `vk::SubpassDescription` 구조체를 사용하여 설명됩니다. Rust에서는 C++의 포인터와 개수 대신 슬라이스(`&[...]`)를 사용합니다. -```c++ -VkSubpassDescription subpass{}; -subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; +```rust +let subpass = vk::SubpassDescription::builder() + .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS) + .color_attachments(&[color_attachment_ref]) // 참조를 슬라이스로 전달 + .build(); ``` -Vulkan may also support compute subpasses in the future, so we have to be -explicit about this being a graphics subpass. Next, we specify the reference to -the color attachment: +Vulkan은 미래에 컴퓨트 서브패스도 지원할 수 있으므로, `pipeline_bind_point`를 통해 이것이 그래픽스 서브패스임을 명시적으로 지정해야 합니다. `color_attachments` 필드는 색상 어태치먼트 참조의 슬라이스를 받습니다. -```c++ -subpass.colorAttachmentCount = 1; -subpass.pColorAttachments = &colorAttachmentRef; -``` +이 배열에서 어태치먼트의 인덱스는 프래그먼트 셰이더에서 `layout(location = 0) out vec4 outColor` 지시문을 통해 직접 참조됩니다! -The index of the attachment in this array is directly referenced from the -fragment shader with the `layout(location = 0) out vec4 outColor` directive! +## 렌더 패스 -The following other types of attachments can be referenced by a subpass: +이제 어태치먼트와 이를 참조하는 서브패스가 설명되었으므로, 렌더 패스 자체를 생성할 수 있습니다. 애플리케이션 구조체에 `render_pass` 필드를 추가하세요. -* `pInputAttachments`: Attachments that are read from a shader -* `pResolveAttachments`: Attachments used for multisampling color attachments -* `pDepthStencilAttachment`: Attachment for depth and stencil data -* `pPreserveAttachments`: Attachments that are not used by this subpass, but for -which the data must be preserved +```rust +struct HelloTriangleApplication { + // ... + render_pass: vk::RenderPass, + pipeline_layout: vk::PipelineLayout, + // ... +} +``` -## Render pass +이제 `vk::RenderPassCreateInfo` 구조체를 채워 렌더 패스를 생성합니다. 이 구조체는 어태치먼트와 서브패스의 슬라이스를 받습니다. -Now that the attachment and a basic subpass referencing it have been described, -we can create the render pass itself. Create a new class member variable to hold -the `VkRenderPass` object right above the `pipelineLayout` variable: +```rust +let attachments = [color_attachment]; +let subpasses = [subpass]; -```c++ -VkRenderPass renderPass; -VkPipelineLayout pipelineLayout; -``` +let render_pass_info = vk::RenderPassCreateInfo::builder() + .attachments(&attachments) + .subpasses(&subpasses) + .build(); -The render pass object can then be created by filling in the -`VkRenderPassCreateInfo` structure with an array of attachments and subpasses. -The `VkAttachmentReference` objects reference attachments using the indices of -this array. - -```c++ -VkRenderPassCreateInfo renderPassInfo{}; -renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; -renderPassInfo.attachmentCount = 1; -renderPassInfo.pAttachments = &colorAttachment; -renderPassInfo.subpassCount = 1; -renderPassInfo.pSubpasses = &subpass; - -if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) { - throw std::runtime_error("failed to create render pass!"); -} +self.render_pass = unsafe { + self.device + .create_render_pass(&render_pass_info, None) +}?; ``` -Just like the pipeline layout, the render pass will be referenced throughout the -program, so it should only be cleaned up at the end: +Ash 라이브러리에서 Vulkan 객체를 생성하거나 파괴하는 대부분의 함수는 `unsafe`로 표시되어 있습니다. 이는 개발자가 유효한 파라미터와 올바른 소멸 순서를 보장해야 함을 상기시켜 줍니다. Rust의 `?` 연산자를 사용하면 `Result`를 반환하는 함수에서 에러를 간결하게 처리할 수 있습니다. -```c++ -void cleanup() { - vkDestroyPipelineLayout(device, pipelineLayout, nullptr); - vkDestroyRenderPass(device, renderPass, nullptr); - ... +파이프라인 레이아웃과 마찬가지로 렌더 패스는 프로그램 전반에서 참조되므로, 프로그램이 끝날 때 정리해야 합니다. 일반적으로 Rust에서는 `Drop` 트레이트 구현을 통해 이 작업을 수행합니다. + +```rust +// Drop 트레이트 구현 내에서 +unsafe { + self.device.destroy_pipeline_layout(self.pipeline_layout, None); + self.device.destroy_render_pass(self.render_pass, None); + // ... } ``` -That was a lot of work, but in the next chapter it all comes together to finally -create the graphics pipeline object! - -[C++ code](/code/11_render_passes.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +상당히 많은 작업이었지만, 다음 장에서는 이 모든 것이 합쳐져 마침내 그래픽스 파이프라인 객체를 생성하게 될 것입니다 \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md index 4a16585e..3ce6187f 100644 --- a/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md @@ -1,122 +1,89 @@ -We can now combine all of the structures and objects from the previous chapters -to create the graphics pipeline! Here's the types of objects we have now, as a -quick recap: - -* Shader stages: the shader modules that define the functionality of the -programmable stages of the graphics pipeline -* Fixed-function state: all of the structures that define the fixed-function -stages of the pipeline, like input assembly, rasterizer, viewport and color -blending -* Pipeline layout: the uniform and push values referenced by the shader that can -be updated at draw time -* Render pass: the attachments referenced by the pipeline stages and their usage - -All of these combined fully define the functionality of the graphics pipeline, -so we can now begin filling in the `VkGraphicsPipelineCreateInfo` structure at -the end of the `createGraphicsPipeline` function. But before the calls to -`vkDestroyShaderModule` because these are still to be used during the creation. - -```c++ -VkGraphicsPipelineCreateInfo pipelineInfo{}; -pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; -pipelineInfo.stageCount = 2; -pipelineInfo.pStages = shaderStages; +이제 이전 챕터들에서 다룬 모든 구조체와 객체를 조합하여 그래픽스 파이프라인을 만들 수 있습니다! 우리가 지금까지 다룬 객체 유형을 간단히 요약하면 다음과 같습니다: + +* **셰이더 스테이지(Shader stages)**: 그래픽스 파이프라인의 프로그래밍 가능한 스테이지의 기능을 정의하는 셰이더 모듈 +* **고정 함수 상태(Fixed-function state)**: 입력 어셈블리, 래스터라이저, 뷰포트, 색상 혼합과 같이 파이프라인의 고정 함수 스테이지를 정의하는 모든 구조체 +* **파이프라인 레이아웃(Pipeline layout)**: 셰이더에서 참조하며 드로우 타임에 업데이트할 수 있는 유니폼 및 푸시 값 +* **렌더 패스(Render pass)**: 파이프라인 스테이지에서 참조하는 어태치먼트와 그 사용법 + +이 모든 것을 합치면 그래픽스 파이프라인의 기능이 완벽하게 정의됩니다. 따라서 이제 `create_graphics_pipeline` 함수의 끝부분에서 `vk::GraphicsPipelineCreateInfo` 구조체를 채워 넣기 시작할 수 있습니다. 단, 셰이더 모듈은 파이프라인 생성 중에 여전히 사용되므로 셰이더 모듈을 파괴하는 코드보다는 앞에 위치해야 합니다. + +`ash`에서는 C++처럼 구조체의 각 필드를 수동으로 할당하는 대신, 타입-세이프한 빌더(builder) 패턴을 사용하는 것이 일반적입니다. + +```rust +let pipeline_info = vk::GraphicsPipelineCreateInfo::builder() + .stages(shader_stages) ``` -We start by referencing the array of `VkPipelineShaderStageCreateInfo` structs. - -```c++ -pipelineInfo.pVertexInputState = &vertexInputInfo; -pipelineInfo.pInputAssemblyState = &inputAssembly; -pipelineInfo.pViewportState = &viewportState; -pipelineInfo.pRasterizationState = &rasterizer; -pipelineInfo.pMultisampleState = &multisampling; -pipelineInfo.pDepthStencilState = nullptr; // Optional -pipelineInfo.pColorBlendState = &colorBlending; -pipelineInfo.pDynamicState = &dynamicState; +먼저 `.stages()` 메서드를 사용하여 `vk::PipelineShaderStageCreateInfo` 구조체 슬라이스(`&[..]`)를 참조하는 것으로 시작합니다. + +```rust + .vertex_input_state(&vertex_input_info) + .input_assembly_state(&input_assembly) + .viewport_state(&viewport_state) + .rasterization_state(&rasterizer) + .multisample_state(&multisampling) + // .depth_stencil_state(&depth_stencil_info) // 선택 사항 + .color_blend_state(&color_blending) + .dynamic_state(&dynamic_state); ``` -Then we reference all of the structures describing the fixed-function stage. +그다음, 고정 함수 스테이지를 설명하는 모든 구조체를 참조합니다. `depth_stencil_state`와 같은 선택적 필드는 빌더에서 해당 메서드를 호출하지 않으면 자동으로 null 포인터로 설정되므로 코드가 더 간결해집니다. -```c++ -pipelineInfo.layout = pipelineLayout; +```rust + .layout(pipeline_layout) ``` -After that comes the pipeline layout, which is a Vulkan handle rather than a -struct pointer. +그다음은 파이프라인 레이아웃인데, 이것은 구조체 포인터가 아닌 Vulkan 핸들입니다. -```c++ -pipelineInfo.renderPass = renderPass; -pipelineInfo.subpass = 0; +```rust + .render_pass(render_pass) + .subpass(0) ``` -And finally we have the reference to the render pass and the index of the sub -pass where this graphics pipeline will be used. It is also possible to use other -render passes with this pipeline instead of this specific instance, but they -have to be *compatible* with `renderPass`. The requirements for compatibility -are described [here](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap8.html#renderpass-compatibility), -but we won't be using that feature in this tutorial. +마지막으로 렌더 패스와, 이 그래픽스 파이프라인이 사용될 서브패스의 인덱스에 대한 참조가 있습니다. 이 파이프라인을 이 특정 인스턴스 대신 다른 렌더 패스와 함께 사용하는 것도 가능하지만, 그 렌더 패스들은 `render_pass`와 *호환 가능(compatible)*해야 합니다. 호환성 요구 사항은 [여기](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap8.html#renderpass-compatibility)에 설명되어 있지만, 이 튜토리얼에서는 해당 기능을 사용하지 않을 것입니다. -```c++ -pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional -pipelineInfo.basePipelineIndex = -1; // Optional +```rust + .base_pipeline_handle(vk::Pipeline::null()) // 선택 사항 + .base_pipeline_index(-1); // 선택 사항 ``` -There are actually two more parameters: `basePipelineHandle` and -`basePipelineIndex`. Vulkan allows you to create a new graphics pipeline by -deriving from an existing pipeline. The idea of pipeline derivatives is that it -is less expensive to set up pipelines when they have much functionality in -common with an existing pipeline and switching between pipelines from the same -parent can also be done quicker. You can either specify the handle of an -existing pipeline with `basePipelineHandle` or reference another pipeline that -is about to be created by index with `basePipelineIndex`. Right now there is -only a single pipeline, so we'll simply specify a null handle and an invalid -index. These values are only used if the `VK_PIPELINE_CREATE_DERIVATIVE_BIT` -flag is also specified in the `flags` field of `VkGraphicsPipelineCreateInfo`. - -Now prepare for the final step by creating a class member to hold the -`VkPipeline` object: - -```c++ -VkPipeline graphicsPipeline; -``` +실제로는 두 개의 필드가 더 있습니다: `base_pipeline_handle`과 `base_pipeline_index`. Vulkan에서는 기존 파이프라인에서 파생하여 새로운 그래픽스 파이프라인을 생성할 수 있습니다. 파이프라인 파생(derivatives)의 개념은, 기존 파이프라인과 많은 기능이 공통될 때 파이프라인을 설정하는 비용이 저렴해지고, 동일한 부모에서 파생된 파이프라인 간의 전환도 더 빠르게 수행할 수 있다는 것입니다. `base_pipeline_handle`로 기존 파이프라인의 핸들을 지정하거나, `base_pipeline_index`로 지금 생성하려는 다른 파이프라인을 인덱스로 참조할 수 있습니다. 지금은 파이프라인이 하나뿐이므로, null 핸들(`vk::Pipeline::null()`)과 유효하지 않은 인덱스(-1)를 지정하겠습니다. 이 값들은 `vk::PipelineCreateFlags::DERIVATIVE` 플래그가 지정된 경우에만 사용됩니다. 빌더 패턴에서는 이들을 명시적으로 설정하지 않으면 기본값으로 설정됩니다. -And finally create the graphics pipeline: +이제 마지막 단계를 위해 `VkPipeline` 객체를 담을 구조체 필드를 준비합니다. -```c++ -if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) { - throw std::runtime_error("failed to create graphics pipeline!"); +```rust +struct HelloTriangleApplication { + // ... + graphics_pipeline: vk::Pipeline, + // ... } ``` -The `vkCreateGraphicsPipelines` function actually has more parameters than the -usual object creation functions in Vulkan. It is designed to take multiple -`VkGraphicsPipelineCreateInfo` objects and create multiple `VkPipeline` objects -in a single call. - -The second parameter, for which we've passed the `VK_NULL_HANDLE` argument, -references an optional `VkPipelineCache` object. A pipeline cache can be used to -store and reuse data relevant to pipeline creation across multiple calls to -`vkCreateGraphicsPipelines` and even across program executions if the cache is -stored to a file. This makes it possible to significantly speed up pipeline -creation at a later time. We'll get into this in the pipeline cache chapter. - -The graphics pipeline is required for all common drawing operations, so it -should also only be destroyed at the end of the program: - -```c++ -void cleanup() { - vkDestroyPipeline(device, graphicsPipeline, nullptr); - vkDestroyPipelineLayout(device, pipelineLayout, nullptr); - ... -} +그리고 마침내 그래픽스 파이프라인을 생성합니다: + +```rust +self.graphics_pipeline = unsafe { + device + .create_graphics_pipelines(vk::PipelineCache::null(), &[pipeline_info.build()], None) + .expect("Failed to create graphics pipeline!") +}[0]; ``` -Now run your program to confirm that all this hard work has resulted in a -successful pipeline creation! We are already getting quite close to seeing -something pop up on the screen. In the next couple of chapters we'll set up the -actual framebuffers from the swap chain images and prepare the drawing commands. +`ash`의 `create_graphics_pipelines` 함수는 Vulkan C API와 약간 다릅니다. 이 함수는 여러 개의 `vk::GraphicsPipelineCreateInfo` 객체를 받아 한 번의 호출로 여러 `vk::Pipeline` 객체를 생성하도록 설계되었습니다. + +* 첫 번째 인자(`vk::PipelineCache::null()`)는 선택적인 파이프라인 캐시 객체를 참조합니다. 파이프라인 캐시는 여러 `create_graphics_pipelines` 호출에 걸쳐 파이프라인 생성과 관련된 데이터를 저장하고 재사용하는 데 사용될 수 있으며, 캐시를 파일에 저장하면 프로그램 실행 간에도 재사용할 수 있습니다. 이 내용은 파이프라인 캐시 챕터에서 다룰 것입니다. +* 두 번째 인자는 생성 정보 구조체의 슬라이스입니다. 우리는 하나만 생성하므로, `.build()`로 빌더를 완료한 후 슬라이스(`&[..]`)로 감싸줍니다. +* 세 번째 인자는 할당자 콜백으로, 여기서는 `None`을 사용합니다. +* 이 함수는 `Result, vk::Result>`를 반환합니다. `expect()`를 사용해 에러를 처리하고, 우리는 파이프라인을 하나만 생성했으므로 반환된 `Vec`의 첫 번째 요소(`[0]`)를 가져옵니다. 이 작업은 `unsafe` 블록 안에서 수행해야 합니다. + +그래픽스 파이프라인은 모든 일반적인 드로잉 작업에 필요하므로 프로그램이 끝날 때 파괴되어야 합니다. Rust에서는 보통 `Drop` 트레잇 내에서 리소스 해제를 처리하지만, 이 튜토리얼의 구조를 따라 `cleanup` 함수에 추가하겠습니다. + +```rust +unsafe fn cleanup(&mut self) { + self.device.destroy_pipeline(self.graphics_pipeline, None); + self.device.destroy_pipeline_layout(self.pipeline_layout, None); + // ... +} +``` -[C++ code](/code/12_graphics_pipeline_complete.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +이제 프로그램을 실행하여 이 모든 노력이 성공적인 파이프라인 생성으로 이어졌는지 확인하세요! 이제 화면에 무언가 나타나는 것에 꽤 가까워졌습니다. 다음 몇 개의 챕터에서는 스왑 체인 이미지로부터 실제 프레임버퍼를 설정하고 드로잉 커맨드를 준비할 것입니다. \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md b/ko-rust/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md index bf7f84a7..91249215 100644 --- a/ko-rust/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md +++ b/ko-rust/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md @@ -1,107 +1,116 @@ -We've talked a lot about framebuffers in the past few chapters and we've set up -the render pass to expect a single framebuffer with the same format as the swap -chain images, but we haven't actually created any yet. - -The attachments specified during render pass creation are bound by wrapping them -into a `VkFramebuffer` object. A framebuffer object references all of the -`VkImageView` objects that represent the attachments. In our case that will be -only a single one: the color attachment. However, the image that we have to use -for the attachment depends on which image the swap chain returns when we retrieve one -for presentation. That means that we have to create a framebuffer for all of the -images in the swap chain and use the one that corresponds to the retrieved image -at drawing time. - -To that end, create another `std::vector` class member to hold the framebuffers: - -```c++ -std::vector swapChainFramebuffers; -``` +### 프레임버퍼 (Rust / Ash) -We'll create the objects for this array in a new function `createFramebuffers` -that is called from `initVulkan` right after creating the graphics pipeline: - -```c++ -void initVulkan() { - createInstance(); - setupDebugMessenger(); - createSurface(); - pickPhysicalDevice(); - createLogicalDevice(); - createSwapChain(); - createImageViews(); - createRenderPass(); - createGraphicsPipeline(); - createFramebuffers(); -} +지난 몇 장에 걸쳐 프레임버퍼에 대해 많이 이야기했고, 스왑 체인 이미지와 동일한 포맷을 가진 단일 프레임버퍼를 사용하도록 렌더 패스를 설정했지만, 아직 실제로 생성하지는 않았습니다. -... +렌더 패스를 생성할 때 지정한 첨부(attachment)들은 `vk::Framebuffer` 객체로 감싸서 바인딩됩니다. 프레임버퍼 객체는 첨부를 나타내는 모든 `vk::ImageView` 객체를 참조합니다. 우리의 경우에는 단 하나, 바로 색상 첨부(color attachment)입니다. 하지만 첨부에 사용해야 할 이미지는 우리가 프레젠테이션을 위해 스왑 체인에서 이미지를 가져올 때 어떤 이미지를 반환하는지에 따라 달라집니다. 이는 스왑 체인의 모든 이미지에 대해 프레임버퍼를 생성하고, 드로잉 시점에는 가져온 이미지에 해당하는 것을 사용해야 한다는 의미입니다. -void createFramebuffers() { +이를 위해, 프레임버퍼를 담을 `Vec` 구조체 필드를 추가합니다: +```rust +struct HelloTriangleApplication { + // ... + render_pass: vk::RenderPass, + pipeline_layout: vk::PipelineLayout, + graphics_pipeline: vk::Pipeline, + swapchain_framebuffers: Vec, + // ... } ``` -Start by resizing the container to hold all of the framebuffers: +이 벡터를 채우기 위한 객체들은 애플리케이션 생성자(`new`)에서 그래픽 파이프라인을 생성한 직후에 호출되는 새로운 함수 `create_framebuffers`에서 생성할 것입니다. + +```rust +impl HelloTriangleApplication { + pub fn new(window: &Window) -> Self { + // ... + let render_pass = Self::create_render_pass(&device, swapchain_image_format); + let (graphics_pipeline, pipeline_layout) = + Self::create_graphics_pipeline(&device, render_pass, swapchain_extent); + let swapchain_framebuffers = + Self::create_framebuffers(&device, render_pass, &swapchain_image_views, swapchain_extent); + // ... + } + + // ... -```c++ -void createFramebuffers() { - swapChainFramebuffers.resize(swapChainImageViews.size()); + fn create_framebuffers( + device: &ash::Device, + render_pass: vk::RenderPass, + image_views: &[vk::ImageView], + swapchain_extent: vk::Extent2D, + ) -> Vec { + // ... 구현 ... + } } ``` -We'll then iterate through the image views and create framebuffers from them: - -```c++ -for (size_t i = 0; i < swapChainImageViews.size(); i++) { - VkImageView attachments[] = { - swapChainImageViews[i] - }; - - VkFramebufferCreateInfo framebufferInfo{}; - framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - framebufferInfo.renderPass = renderPass; - framebufferInfo.attachmentCount = 1; - framebufferInfo.pAttachments = attachments; - framebufferInfo.width = swapChainExtent.width; - framebufferInfo.height = swapChainExtent.height; - framebufferInfo.layers = 1; - - if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &swapChainFramebuffers[i]) != VK_SUCCESS) { - throw std::runtime_error("failed to create framebuffer!"); +`create_framebuffers` 함수는 이미지 뷰를 순회하며 각각에 대한 프레임버퍼를 생성합니다. + +```rust +fn create_framebuffers( + device: &ash::Device, + render_pass: vk::RenderPass, + image_views: &[vk::ImageView], + swapchain_extent: vk::Extent2D, +) -> Vec { + let mut framebuffers = Vec::with_capacity(image_views.len()); + + for &image_view in image_views.iter() { + let attachments = [image_view]; + + let framebuffer_info = vk::FramebufferCreateInfo::builder() + .render_pass(render_pass) + .attachments(&attachments) + .width(swapchain_extent.width) + .height(swapchain_extent.height) + .layers(1); + + let framebuffer = unsafe { + device + .create_framebuffer(&framebuffer_info, None) + .expect("Failed to create Framebuffer!") + }; + framebuffers.push(framebuffer); } + + framebuffers } ``` -As you can see, creation of framebuffers is quite straightforward. We first need -to specify with which `renderPass` the framebuffer needs to be compatible. You -can only use a framebuffer with the render passes that it is compatible with, -which roughly means that they use the same number and type of attachments. +보시다시피, Ash의 빌더 패턴 덕분에 프레임버퍼 생성이 매우 간단합니다. +먼저 프레임버퍼가 어떤 `render_pass`와 호환되어야 하는지 지정해야 합니다. 프레임버퍼는 호환되는 렌더 패스와만 사용할 수 있는데, 이는 대략적으로 말해 동일한 수와 유형의 첨부를 사용한다는 것을 의미합니다. -The `attachmentCount` and `pAttachments` parameters specify the `VkImageView` -objects that should be bound to the respective attachment descriptions in -the render pass `pAttachment` array. +- `render_pass()`: 프레임버퍼가 호환되어야 할 렌더 패스를 지정합니다. +- `attachments()`: 렌더 패스의 `pAttachments` 배열에 있는 각 첨부 설명에 바인딩될 `vk::ImageView` 객체의 슬라이스를 지정합니다. +- `width()`와 `height()`: 이름에서 알 수 있듯이 명확하며, 스왑 체인의 크기(`extent`)에서 가져옵니다. +- `layers()`: 이미지 배열의 레이어 수를 나타냅니다. 우리의 스왑 체인 이미지는 단일 이미지이므로 레이어 수는 `1`입니다. -The `width` and `height` parameters are self-explanatory and `layers` refers to -the number of layers in image arrays. Our swap chain images are single images, -so the number of layers is `1`. +`device.create_framebuffer` 호출은 `unsafe` 블록 안에 있습니다. 이는 유효하지 않은 핸들이나 매개변수를 전달할 경우 Vulkan 드라이버가 정의되지 않은 동작을 일으킬 수 있기 때문이며, Rust 컴파일러는 이를 보장할 수 없습니다. -We should delete the framebuffers before the image views and render pass that -they are based on, but only after we've finished rendering: +생성된 프레임버퍼는 애플리케이션이 종료될 때 정리해야 합니다. Rust에서는 `Drop` 트레잇을 구현하여 이 작업을 자동으로 처리하는 것이 일반적입니다. 프레임버퍼는 그것들이 참조하는 이미지 뷰나 렌더 패스보다 먼저 파괴되어야 합니다. `Drop` 구현에서 파괴 순서를 명시적으로 제어하는 것이 좋습니다. -```c++ -void cleanup() { - for (auto framebuffer : swapChainFramebuffers) { - vkDestroyFramebuffer(device, framebuffer, nullptr); - } +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + // ... 다른 리소스 정리 ... + + for framebuffer in self.swapchain_framebuffers.iter() { + self.device.destroy_framebuffer(*framebuffer, None); + } + + self.device.destroy_pipeline(self.graphics_pipeline, None); + self.device.destroy_pipeline_layout(self.pipeline_layout, None); + self.device.destroy_render_pass(self.render_pass, None); - ... + for image_view in self.swapchain_image_views.iter() { + self.device.destroy_image_view(*image_view, None); + } + + // ... 나머지 리소스 정리 ... + } + } } ``` -We've now reached the milestone where we have all of the objects that are -required for rendering. In the next chapter we're going to write the first -actual drawing commands. - -[C++ code](/code/13_framebuffers.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +이제 우리는 렌더링에 필요한 모든 객체를 갖추는 중요한 단계에 도달했습니다. 다음 장에서는 첫 실제 드로잉 명령을 작성할 것입니다. \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md b/ko-rust/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md index 61a40b4f..42784aee 100644 --- a/ko-rust/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md +++ b/ko-rust/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md @@ -1,344 +1,274 @@ -Commands in Vulkan, like drawing operations and memory transfers, are not -executed directly using function calls. You have to record all of the operations -you want to perform in command buffer objects. The advantage of this is that when -we are ready to tell the Vulkan what we want to do, all of the commands are -submitted together and Vulkan can more efficiently process the commands since all -of them are available together. In addition, this allows command recording to -happen in multiple threads if so desired. - -## Command pools - -We have to create a command pool before we can create command buffers. Command -pools manage the memory that is used to store the buffers and command buffers -are allocated from them. Add a new class member to store a `VkCommandPool`: - -```c++ -VkCommandPool commandPool; -``` +Vulkan에서 그리기 연산이나 메모리 전송과 같은 커맨드(command)는 함수 호출을 통해 직접 실행되지 않습니다. 대신, 수행하려는 모든 작업을 커맨드 버퍼(command buffer) 객체에 기록(record)해야 합니다. 이 방식의 장점은 Vulkan에게 무엇을 할지 알려줄 준비가 되었을 때 모든 커맨드가 함께 제출된다는 것입니다. 그러면 Vulkan은 모든 커맨드를 한 번에 사용할 수 있으므로 더 효율적으로 처리할 수 있습니다. 또한, 원한다면 여러 스레드에서 커맨드 기록을 수행할 수도 있습니다. -Then create a new function `createCommandPool` and call it from `initVulkan` -after the framebuffers were created. - -```c++ -void initVulkan() { - createInstance(); - setupDebugMessenger(); - createSurface(); - pickPhysicalDevice(); - createLogicalDevice(); - createSwapChain(); - createImageViews(); - createRenderPass(); - createGraphicsPipeline(); - createFramebuffers(); - createCommandPool(); -} +## 커맨드 풀 (Command pools) -... +커맨드 버퍼를 생성하기 전에 먼저 커맨드 풀(command pool)을 생성해야 합니다. 커맨드 풀은 버퍼를 저장하는 데 사용되는 메모리를 관리하며, 커맨드 버퍼는 이 풀에서 할당됩니다. 메인 애플리케이션 `struct`에 `vk::CommandPool`을 저장할 새 필드를 추가합니다. -void createCommandPool() { +```rust +struct VulkanApp { + // ... + command_pool: vk::CommandPool, + // ... +} +``` +그런 다음 `create_command_pool`이라는 새 메서드를 만들고, `init_vulkan`에서 프레임버퍼가 생성된 후에 호출합니다. + +```rust +impl VulkanApp { + fn init_vulkan(&mut self) { + self.create_instance(); + self.setup_debug_messenger(); + self.create_surface(); + self.pick_physical_device(); + self.create_logical_device(); + self.create_swapchain(); + self.create_image_views(); + self.create_render_pass(); + self.create_graphics_pipeline(); + self.create_framebuffers(); + self.create_command_pool(); + } + + // ... + + fn create_command_pool(&mut self) { + // ... + } } ``` -Command pool creation only takes two parameters: +커맨드 풀 생성에는 `ash`의 빌더(builder) 패턴을 사용하는 것이 편리하고 안전합니다. -```c++ -QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice); +```rust +let queue_family_indices = self.find_queue_families(self.physical_device); -VkCommandPoolCreateInfo poolInfo{}; -poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; -poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; -poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value(); +let pool_info = vk::CommandPoolCreateInfo::builder() + .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER) + .queue_family_index(queue_family_indices.graphics_family.unwrap()); ``` -There are two possible flags for command pools: +`ash`의 빌더를 사용하면 `sType` 필드가 자동으로 채워져 코드가 더 깔끔해집니다. 플래그는 타입-세이프(type-safe) 열거형으로 제공됩니다. -* `VK_COMMAND_POOL_CREATE_TRANSIENT_BIT`: Hint that command buffers are -rerecorded with new commands very often (may change memory allocation behavior) -* `VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT`: Allow command buffers to be -rerecorded individually, without this flag they all have to be reset together +* `vk::CommandPoolCreateFlags::TRANSIENT`: 커맨드 버퍼가 새로운 커맨드로 매우 자주 다시 기록될 것임을 암시합니다 (메모리 할당 동작이 변경될 수 있음). +* `vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER`: 커맨드 버퍼를 개별적으로 다시 기록할 수 있도록 허용합니다. 이 플래그가 없으면 모든 커맨드 버퍼를 함께 리셋해야 합니다. -We will be recording a command buffer every frame, so we want to be able to -reset and rerecord over it. Thus, we need to set the -`VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT` flag bit for our command pool. +우리는 매 프레임마다 커맨드 버퍼를 기록할 것이므로, 이를 리셋하고 다시 기록할 수 있어야 합니다. 따라서 커맨드 풀에 `RESET_COMMAND_BUFFER` 플래그를 설정해야 합니다. -Command buffers are executed by submitting them on one of the device queues, -like the graphics and presentation queues we retrieved. Each command pool can -only allocate command buffers that are submitted on a single type of queue. -We're going to record commands for drawing, which is why we've chosen the -graphics queue family. +커맨드 버퍼는 우리가 가져온 그래픽스 및 프레젠테이션 큐와 같은 장치 큐 중 하나에 제출하여 실행됩니다. 각 커맨드 풀은 단일 유형의 큐에 제출되는 커맨드 버퍼만 할당할 수 있습니다. 우리는 그리기를 위한 커맨드를 기록할 것이므로 그래픽스 큐 패밀리를 선택했습니다. `Option` 타입의 큐 패밀리 인덱스는 `unwrap()`을 통해 값을 가져옵니다. - -```c++ -if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) { - throw std::runtime_error("failed to create command pool!"); -} +```rust +self.command_pool = unsafe { + self.device + .create_command_pool(&pool_info, None) + .expect("Failed to create command pool!") +}; ``` -Finish creating the command pool using the `vkCreateCommandPool` function. It -doesn't have any special parameters. Commands will be used throughout the -program to draw things on the screen, so the pool should only be destroyed at -the end: +`ash`에서 생성 함수는 `Device`나 `Instance`의 메서드로 제공됩니다. C++의 `nullptr`는 Rust의 `None`에 해당합니다. Vulkan API 호출은 드라이버와의 상호작용과 유효한 상태 유지를 프로그래머에게 위임하므로 `unsafe` 블록 안에서 호출해야 합니다. `ash` 함수는 `Result`를 반환하므로 `?` 연산자나 `expect`를 사용해 에러를 처리할 수 있습니다. -```c++ -void cleanup() { - vkDestroyCommandPool(device, commandPool, nullptr); +커맨드 풀은 프로그램이 끝날 때 파괴되어야 합니다. Rust에서는 `Drop` 트레잇을 구현하여 리소스 정리를 자동화하는 것이 일반적입니다. - ... +```rust +impl Drop for VulkanApp { + fn drop(&mut self) { + unsafe { + self.device.destroy_command_pool(self.command_pool, None); + // ... + } + } } ``` -## Command buffer allocation - -We can now start allocating command buffers. - -Create a `VkCommandBuffer` object as a class member. Command buffers -will be automatically freed when their command pool is destroyed, so we don't -need explicit cleanup. - -```c++ -VkCommandBuffer commandBuffer; -``` - -We'll now start working on a `createCommandBuffer` function to allocate a single -command buffer from the command pool. - -```c++ -void initVulkan() { - createInstance(); - setupDebugMessenger(); - createSurface(); - pickPhysicalDevice(); - createLogicalDevice(); - createSwapChain(); - createImageViews(); - createRenderPass(); - createGraphicsPipeline(); - createFramebuffers(); - createCommandPool(); - createCommandBuffer(); -} - -... +## 커맨드 버퍼 할당 -void createCommandBuffer() { +이제 커맨드 버퍼 할당을 시작할 수 있습니다. `vk::CommandBuffer` 객체를 `struct`의 필드로 추가합니다. 커맨드 버퍼는 커맨드 풀이 파괴될 때 자동으로 해제되므로, 명시적인 정리 코드가 필요하지 않습니다. +```rust +struct VulkanApp { + // ... + command_pool: vk::CommandPool, + command_buffer: vk::CommandBuffer, + // ... } ``` -Command buffers are allocated with the `vkAllocateCommandBuffers` function, -which takes a `VkCommandBufferAllocateInfo` struct as parameter that specifies -the command pool and number of buffers to allocate: - -```c++ -VkCommandBufferAllocateInfo allocInfo{}; -allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; -allocInfo.commandPool = commandPool; -allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; -allocInfo.commandBufferCount = 1; - -if (vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer) != VK_SUCCESS) { - throw std::runtime_error("failed to allocate command buffers!"); +이제 커맨드 풀에서 단일 커맨드 버퍼를 할당하는 `create_command_buffer` 메서드 작업을 시작하겠습니다. + +```rust +impl VulkanApp { + fn init_vulkan(&mut self) { + // ... + self.create_command_pool(); + self.create_command_buffer(); + } + + // ... + + fn create_command_buffer(&mut self) { + let alloc_info = vk::CommandBufferAllocateInfo::builder() + .command_pool(self.command_pool) + .level(vk::CommandBufferLevel::PRIMARY) + .command_buffer_count(1); + + self.command_buffer = unsafe { + self.device + .allocate_command_buffers(&alloc_info) + .expect("Failed to allocate command buffers!")[0] + }; + } } ``` -The `level` parameter specifies if the allocated command buffers are primary or -secondary command buffers. - -* `VK_COMMAND_BUFFER_LEVEL_PRIMARY`: Can be submitted to a queue for execution, -but cannot be called from other command buffers. -* `VK_COMMAND_BUFFER_LEVEL_SECONDARY`: Cannot be submitted directly, but can be -called from primary command buffers. +커맨드 버퍼는 `allocate_command_buffers` 메서드로 할당됩니다. 이 메서드는 `Vec`를 반환하므로, 하나만 할당했더라도 첫 번째 요소(`[0]`)를 가져와야 합니다. -We won't make use of the secondary command buffer functionality here, but you -can imagine that it's helpful to reuse common operations from primary command -buffers. +`level` 매개변수는 할당된 커맨드 버퍼가 주(primary) 커맨드 버퍼인지 보조(secondary) 커맨드 버퍼인지를 지정합니다. -Since we are only allocating one command buffer, the `commandBufferCount` parameter -is just one. +* `vk::CommandBufferLevel::PRIMARY`: 큐에 제출하여 실행할 수 있지만, 다른 커맨드 버퍼에서 호출될 수는 없습니다. +* `vk::CommandBufferLevel::SECONDARY`: 직접 제출할 수는 없지만, 주 커맨드 버퍼에서 호출될 수 있습니다. -## Command buffer recording +여기서는 보조 커맨드 버퍼 기능을 사용하지 않겠지만, 주 커맨드 버퍼에서 공통 작업을 재사용하는 데 유용하다는 것을 상상할 수 있습니다. -We'll now start working on the `recordCommandBuffer` function that writes the -commands we want to execute into a command buffer. The `VkCommandBuffer` used -will be passed in as a parameter, as well as the index of the current swapchain -image we want to write to. +## 커맨드 버퍼 기록 -```c++ -void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) { +이제 실행하려는 커맨드를 커맨드 버퍼에 작성하는 `record_command_buffer` 메서드 작업을 시작하겠습니다. 현재 스왑체인 이미지의 인덱스를 매개변수로 받습니다. +```rust +impl VulkanApp { + fn record_command_buffer(&self, image_index: u32) { + // ... + } } ``` -We always begin recording a command buffer by calling `vkBeginCommandBuffer` -with a small `VkCommandBufferBeginInfo` structure as argument that specifies -some details about the usage of this specific command buffer. +커맨드 버퍼 기록은 항상 `begin_command_buffer`를 호출하는 것으로 시작합니다. -```c++ -VkCommandBufferBeginInfo beginInfo{}; -beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; -beginInfo.flags = 0; // Optional -beginInfo.pInheritanceInfo = nullptr; // Optional +```rust +let begin_info = vk::CommandBufferBeginInfo::builder(); -if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) { - throw std::runtime_error("failed to begin recording command buffer!"); +unsafe { + self.device + .begin_command_buffer(self.command_buffer, &begin_info) + .expect("Failed to begin recording command buffer!"); } ``` -The `flags` parameter specifies how we're going to use the command buffer. The -following values are available: +`flags` 매개변수는 커맨드 버퍼 사용 방식을 지정합니다. `ash`의 빌더는 기본적으로 플래그를 0으로 설정합니다. -* `VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT`: The command buffer will be -rerecorded right after executing it once. -* `VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT`: This is a secondary -command buffer that will be entirely within a single render pass. -* `VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT`: The command buffer can be -resubmitted while it is also already pending execution. +* `vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT`: 커맨드 버퍼는 한 번 실행된 직후 다시 기록될 것입니다. +* `vk::CommandBufferUsageFlags::RENDER_PASS_CONTINUE`: 이것은 단일 렌더 패스 내에서만 사용될 보조 커맨드 버퍼입니다. +* `vk::CommandBufferUsageFlags::SIMULTANEOUS_USE`: 커맨드 버퍼가 이미 실행 대기 중인 상태에서도 다시 제출될 수 있습니다. -None of these flags are applicable for us right now. +지금 우리에게는 이 플래그들 중 어느 것도 해당되지 않습니다. 커맨드 버퍼가 이미 기록되었다면 `begin_command_buffer` 호출은 암시적으로 리셋합니다. -The `pInheritanceInfo` parameter is only relevant for secondary command buffers. -It specifies which state to inherit from the calling primary command buffers. +## 렌더 패스 시작하기 -If the command buffer was already recorded once, then a call to -`vkBeginCommandBuffer` will implicitly reset it. It's not possible to append -commands to a buffer at a later time. +그리기는 `cmd_begin_render_pass`로 렌더 패스를 시작하는 것으로 시작됩니다. -## Starting a render pass +```rust +let render_pass_info = { + let clear_color = vk::ClearValue { + color: vk::ClearColorValue { + float32: [0.0, 0.0, 0.0, 1.0], + }, + }; -Drawing starts by beginning the render pass with `vkCmdBeginRenderPass`. The -render pass is configured using some parameters in a `VkRenderPassBeginInfo` -struct. + let render_area = vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: self.swapchain_extent, + }; -```c++ -VkRenderPassBeginInfo renderPassInfo{}; -renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; -renderPassInfo.renderPass = renderPass; -renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex]; + vk::RenderPassBeginInfo::builder() + .render_pass(self.render_pass) + .framebuffer(self.swapchain_framebuffers[image_index as usize]) + .render_area(render_area) + .clear_values(&[clear_color]) +}; ``` -The first parameters are the render pass itself and the attachments to bind. We -created a framebuffer for each swap chain image where it is specified as a color -attachment. Thus we need to bind the framebuffer for the swapchain image we want -to draw to. Using the imageIndex parameter which was passed in, we can pick the -right framebuffer for the current swapchain image. +`ash`에서는 배열을 전달해야 하는 곳에 Rust의 슬라이스(`&[...]`)를 사용합니다. `image_index`를 사용하여 현재 스왑체인 이미지에 맞는 프레임버퍼를 선택합니다. 렌더 영역은 셰이더 로드 및 저장이 일어날 위치를 정의하며, 최상의 성능을 위해 어태치먼트 크기와 일치시키는 것이 좋습니다. 마지막으로, `VK_ATTACHMENT_LOAD_OP_CLEAR`에 사용할 소거 값을 검은색으로 설정했습니다. -```c++ -renderPassInfo.renderArea.offset = {0, 0}; -renderPassInfo.renderArea.extent = swapChainExtent; -``` - -The next two parameters define the size of the render area. The render area -defines where shader loads and stores will take place. The pixels outside this -region will have undefined values. It should match the size of the attachments -for best performance. - -```c++ -VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}}; -renderPassInfo.clearValueCount = 1; -renderPassInfo.pClearValues = &clearColor; -``` - -The last two parameters define the clear values to use for -`VK_ATTACHMENT_LOAD_OP_CLEAR`, which we used as load operation for the color -attachment. I've defined the clear color to simply be black with 100% opacity. - -```c++ -vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); +```rust +unsafe { + self.device.cmd_begin_render_pass( + self.command_buffer, + &render_pass_info, + vk::SubpassContents::INLINE, + ); +} ``` -The render pass can now begin. All of the functions that record commands can be -recognized by their `vkCmd` prefix. They all return `void`, so there will be no -error handling until we've finished recording. - -The first parameter for every command is always the command buffer to record the -command to. The second parameter specifies the details of the render pass we've -just provided. The final parameter controls how the drawing commands within the -render pass will be provided. It can have one of two values: +커맨드를 기록하는 모든 함수는 `cmd_` 접두사를 가지며 `Device`의 메서드입니다. 반환값이 없으므로 오류 처리가 필요 없습니다. 마지막 매개변수는 다음 두 값 중 하나를 가집니다: -* `VK_SUBPASS_CONTENTS_INLINE`: The render pass commands will be embedded in -the primary command buffer itself and no secondary command buffers will be -executed. -* `VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS`: The render pass commands will -be executed from secondary command buffers. +* `vk::SubpassContents::INLINE`: 렌더 패스 커맨드가 주 커맨드 버퍼 자체에 포함됩니다. +* `vk::SubpassContents::SECONDARY_COMMAND_BUFFERS`: 렌더 패스 커맨드가 보조 커맨드 버퍼에서 실행됩니다. -We will not be using secondary command buffers, so we'll go with the first -option. +우리는 보조 커맨드 버퍼를 사용하지 않으므로 `INLINE`을 선택합니다. -## Basic drawing commands +## 기본 드로잉 커맨드 -We can now bind the graphics pipeline: +이제 그래픽스 파이프라인을 바인딩합니다. -```c++ -vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); +```rust +unsafe { + self.device.cmd_bind_pipeline( + self.command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.graphics_pipeline, + ); +} ``` -The second parameter specifies if the pipeline object is a graphics or compute -pipeline. We've now told Vulkan which operations to execute in the graphics -pipeline and which attachment to use in the fragment shader. - -As noted in the [fixed functions chapter](../02_Graphics_pipeline_basics/02_Fixed_functions.md#dynamic-state), -we did specify viewport and scissor state for this pipeline to be dynamic. -So we need to set them in the command buffer before issuing our draw command: - -```c++ -VkViewport viewport{}; -viewport.x = 0.0f; -viewport.y = 0.0f; -viewport.width = static_cast(swapChainExtent.width); -viewport.height = static_cast(swapChainExtent.height); -viewport.minDepth = 0.0f; -viewport.maxDepth = 1.0f; -vkCmdSetViewport(commandBuffer, 0, 1, &viewport); - -VkRect2D scissor{}; -scissor.offset = {0, 0}; -scissor.extent = swapChainExtent; -vkCmdSetScissor(commandBuffer, 0, 1, &scissor); +파이프라인의 뷰포트와 시저 상태를 동적으로 지정했으므로, 드로우 커맨드 전에 이를 설정해야 합니다. + +```rust +unsafe { + let viewport = vk::Viewport { + x: 0.0, + y: 0.0, + width: self.swapchain_extent.width as f32, + height: self.swapchain_extent.height as f32, + min_depth: 0.0, + max_depth: 1.0, + }; + self.device.cmd_set_viewport(self.command_buffer, 0, &[viewport]); + + let scissor = vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: self.swapchain_extent, + }; + self.device.cmd_set_scissor(self.command_buffer, 0, &[scissor]); +} ``` +`ash`에서는 뷰포트와 시저 같은 단일 항목도 슬라이스(`&[...]`)로 전달해야 합니다. -Now we are ready to issue the draw command for the triangle: +이제 삼각형을 그리기 위한 드로우 커맨드를 실행합니다. -```c++ -vkCmdDraw(commandBuffer, 3, 1, 0, 0); +```rust +unsafe { + self.device.cmd_draw(self.command_buffer, 3, 1, 0, 0); +} ``` -The actual `vkCmdDraw` function is a bit anticlimactic, but it's so simple -because of all the information we specified in advance. It has the following -parameters, aside from the command buffer: - -* `vertexCount`: Even though we don't have a vertex buffer, we technically still -have 3 vertices to draw. -* `instanceCount`: Used for instanced rendering, use `1` if you're not doing -that. -* `firstVertex`: Used as an offset into the vertex buffer, defines the lowest -value of `gl_VertexIndex`. -* `firstInstance`: Used as an offset for instanced rendering, defines the lowest -value of `gl_InstanceIndex`. +`cmd_draw` 함수는 사전에 많은 정보를 지정했기 때문에 매우 간단합니다. -## Finishing up +* `vertex_count`: 정점 버퍼가 없지만, 3개의 정점을 그립니다. +* `instance_count`: 인스턴스 렌더링에 사용됩니다. (`1`은 미사용) +* `first_vertex`: `gl_VertexIndex`의 최솟값을 정의하는 오프셋입니다. +* `first_instance`: `gl_InstanceIndex`의 최솟값을 정의하는 오프셋입니다. -The render pass can now be ended: +## 마무리 -```c++ -vkCmdEndRenderPass(commandBuffer); -``` +렌더 패스를 종료하고 커맨드 버퍼 기록을 마칩니다. -And we've finished recording the command buffer: +```rust +unsafe { + self.device.cmd_end_render_pass(self.command_buffer); -```c++ -if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) { - throw std::runtime_error("failed to record command buffer!"); + self.device + .end_command_buffer(self.command_buffer) + .expect("Failed to record command buffer!"); } ``` - - -In the next chapter we'll write the code for the main loop, which will acquire -an image from the swap chain, record and execute a command buffer, then return the -finished image to the swap chain. - -[C++ code](/code/14_command_buffers.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +다음 챕터에서는 메인 루프 코드를 작성할 것입니다. 이 루프는 스왑 체인에서 이미지를 가져오고, 커맨드 버퍼를 기록 및 실행한 다음, 완성된 이미지를 스왑 체인으로 반환하는 작업을 수행합니다. \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md b/ko-rust/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md index 233c059d..df3e2704 100644 --- a/ko-rust/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md +++ b/ko-rust/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md @@ -1,577 +1,334 @@ - -This is the chapter where everything is going to come together. We're going to -write the `drawFrame` function that will be called from the main loop to put the -triangle on the screen. Let's start by creating the function and call it from -`mainLoop`: - -```c++ -void mainLoop() { - while (!glfwWindowShouldClose(window)) { - glfwPollEvents(); - drawFrame(); - } +이번 장에서는 모든 것을 하나로 합칠 시간입니다. 메인 루프에서 호출되어 삼각형을 화면에 그리는 `draw_frame` 함수를 작성할 것입니다. 먼저 함수를 만들고 `main_loop`에서 호출해 봅시다. + +```rust +fn main_loop(&mut self) { + self.event_loop + .run(move |event, _, control_flow| { + // ... 이벤트 처리 ... + match event { + // ... + Event::MainEventsCleared => { + self.window.request_redraw(); + } + Event::RedrawRequested(_) => { + self.draw_frame(); + } + // ... + } + }) } -... - -void drawFrame() { +// ... +impl HelloTriangleApplication { + fn draw_frame(&mut self) { + // 여기에 렌더링 로직을 작성합니다. + } } ``` +*참고: Rust에서는 이벤트 루프 기반으로 동작하므로, C++의 `while` 루프 대신 `winit`의 `RedrawRequested` 이벤트 핸들러 내에서 `draw_frame`을 호출하는 것이 일반적입니다.* -## Outline of a frame - -At a high level, rendering a frame in Vulkan consists of a common set of steps: +## 프레임의 개요 -* Wait for the previous frame to finish -* Acquire an image from the swap chain -* Record a command buffer which draws the scene onto that image -* Submit the recorded command buffer -* Present the swap chain image +높은 수준에서 Vulkan으로 프레임을 렌더링하는 것은 다음과 같은 공통된 단계로 구성됩니다. -While we will expand the drawing function in later chapters, for now this is the -core of our render loop. +* 이전 프레임이 끝나기를 기다립니다. +* 스왑 체인에서 이미지를 가져옵니다. +* 가져온 이미지에 장면을 그리는 커맨드 버퍼를 기록합니다. +* 기록된 커맨드 버퍼를 제출합니다. +* 스왑 체인 이미지를 제시(present)합니다. - +이후 장에서 드로잉 함수를 더 확장하겠지만, 지금으로서는 이것이 우리 렌더링 루프의 핵심입니다. -## Synchronization + - +## 동기화 -A core design philosophy in Vulkan is that synchronization of execution on -the GPU is explicit. The order of operations is up to us to define using various -synchronization primitives which tell the driver the order we want things to run -in. This means that many Vulkan API calls which start executing work on the GPU -are asynchronous, the functions will return before the operation has finished. + -In this chapter there are a number of events that we need to order explicitly -because they happen on the GPU, such as: +Vulkan의 핵심 설계 철학 중 하나는 GPU에서의 실행 동기화가 명시적이라는 것입니다. (이하 동기화에 대한 개념 설명은 C++ 버전과 동일하므로 생략하고, Rust/Ash 구현에 초점을 맞춥니다.) -* Acquire an image from the swap chain -* Execute commands that draw onto the acquired image -* Present that image to the screen for presentation, returning it to the swapchain +... (세마포어와 펜스에 대한 개념 설명) ... -Each of these events is set in motion using a single function call, but are all -executed asynchronously. The function calls will return before the operations -are actually finished and the order of execution is also undefined. That is -unfortunate, because each of the operations depends on the previous one -finishing. Thus we need to explore which primitives we can use to achieve -the desired ordering. +### 무엇을 선택해야 할까? -### Semaphores +우리에게는 두 가지 동기화 프리미티브가 있고, 마침 동기화를 적용할 두 곳이 있습니다. 스왑 체인 작업과 이전 프레임이 끝나기를 기다리는 것입니다. 스왑 체인 작업은 GPU에서 발생하므로 세마포어를 사용하고, 이전 프레임이 끝나기를 기다리는 작업은 CPU(호스트)가 기다려야 하므로 펜스를 사용합니다. 이는 GPU가 커맨드 버퍼를 사용하는 동안 CPU가 해당 커맨드 버퍼를 덮어쓰지 않도록 보장하기 위함입니다. -A semaphore is used to add order between queue operations. Queue operations -refer to the work we submit to a queue, either in a command buffer or from -within a function as we will see later. Examples of queues are the graphics -queue and the presentation queue. Semaphores are used both to order work inside -the same queue and between different queues. +## 동기화 객체 생성하기 -There happens to be two kinds of semaphores in Vulkan, binary and timeline. -Because only binary semaphores will be used in this tutorial, we will not -discuss timeline semaphores. Further mention of the term semaphore exclusively -refers to binary semaphores. +단일 프레임만 처리하는 대신, 여러 프레임이 동시에 처리 중(in-flight)일 수 있는 보다 일반적인 접근 방식을 사용하겠습니다. 이를 통해 GPU가 하나의 프레임을 렌더링하는 동안 CPU는 다음 프레임을 준비할 수 있어 성능이 향상됩니다. `MAX_FRAMES_IN_FLIGHT` 상수를 정의하고, 각 프레임에 대한 동기화 객체 세트를 생성합니다. -A semaphore is either unsignaled or signaled. It begins life as unsignaled. The -way we use a semaphore to order queue operations is by providing the same -semaphore as a 'signal' semaphore in one queue operation and as a 'wait' -semaphore in another queue operation. For example, lets say we have semaphore S -and queue operations A and B that we want to execute in order. What we tell -Vulkan is that operation A will 'signal' semaphore S when it finishes executing, -and operation B will 'wait' on semaphore S before it begins executing. When -operation A finishes, semaphore S will be signaled, while operation B wont -start until S is signaled. After operation B begins executing, semaphore S -is automatically reset back to being unsignaled, allowing it to be used again. +```rust +const MAX_FRAMES_IN_FLIGHT: usize = 2; -Pseudo-code of what was just described: -``` -VkCommandBuffer A, B = ... // record command buffers -VkSemaphore S = ... // create a semaphore - -// enqueue A, signal S when done - starts executing immediately -vkQueueSubmit(work: A, signal: S, wait: None) - -// enqueue B, wait on S to start -vkQueueSubmit(work: B, signal: None, wait: S) -``` - -Note that in this code snippet, both calls to `vkQueueSubmit()` return -immediately - the waiting only happens on the GPU. The CPU continues running -without blocking. To make the CPU wait, we need a different synchronization -primitive, which we will now describe. - -### Fences - -A fence has a similar purpose, in that it is used to synchronize execution, but -it is for ordering the execution on the CPU, otherwise known as the host. -Simply put, if the host needs to know when the GPU has finished something, we -use a fence. - -Similar to semaphores, fences are either in a signaled or unsignaled state. -Whenever we submit work to execute, we can attach a fence to that work. When -the work is finished, the fence will be signaled. Then we can make the host -wait for the fence to be signaled, guaranteeing that the work has finished -before the host continues. - -A concrete example is taking a screenshot. Say we have already done the -necessary work on the GPU. Now need to transfer the image from the GPU over -to the host and then save the memory to a file. We have command buffer A which -executes the transfer and fence F. We submit command buffer A with fence F, -then immediately tell the host to wait for F to signal. This causes the host to -block until command buffer A finishes execution. Thus we are safe to let the -host save the file to disk, as the memory transfer has completed. - -Pseudo-code for what was described: -``` -VkCommandBuffer A = ... // record command buffer with the transfer -VkFence F = ... // create the fence - -// enqueue A, start work immediately, signal F when done -vkQueueSubmit(work: A, fence: F) - -vkWaitForFence(F) // blocks execution until A has finished executing - -save_screenshot_to_disk() // can't run until the transfer has finished -``` - -Unlike the semaphore example, this example *does* block host execution. This -means the host won't do anything except wait until execution has finished. For -this case, we had to make sure the transfer was complete before we could save -the screenshot to disk. - -In general, it is preferable to not block the host unless necessary. We want to -feed the GPU and the host with useful work to do. Waiting on fences to signal -is not useful work. Thus we prefer semaphores, or other synchronization -primitives not yet covered, to synchronize our work. - -Fences must be reset manually to put them back into the unsignaled state. This -is because fences are used to control the execution of the host, and so the -host gets to decide when to reset the fence. Contrast this to semaphores which -are used to order work on the GPU without the host being involved. - -In summary, semaphores are used to specify the execution order of operations on -the GPU while fences are used to keep the CPU and GPU in sync with each-other. - -### What to choose? - -We have two synchronization primitives to use and conveniently two places to -apply synchronization: Swapchain operations and waiting for the previous frame -to finish. We want to use semaphores for swapchain operations because they -happen on the GPU, thus we don't want to make the host wait around if we can -help it. For waiting on the previous frame to finish, we want to use fences -for the opposite reason, because we need the host to wait. This is so we don't -draw more than one frame at a time. Because we re-record the command buffer -every frame, we cannot record the next frame's work to the command buffer -until the current frame has finished executing, as we don't want to overwrite -the current contents of the command buffer while the GPU is using it. - -## Creating the synchronization objects - -We'll need one semaphore to signal that an image has been acquired from the -swapchain and is ready for rendering, another one to signal that rendering has -finished and presentation can happen, and a fence to make sure only one frame -is rendering at a time. - -Create three class members to store these semaphore objects and fence object: - -```c++ -VkSemaphore imageAvailableSemaphore; -VkSemaphore renderFinishedSemaphore; -VkFence inFlightFence; -``` - -To create the semaphores, we'll add the last `create` function for this part of -the tutorial: `createSyncObjects`: - -```c++ -void initVulkan() { - createInstance(); - setupDebugMessenger(); - createSurface(); - pickPhysicalDevice(); - createLogicalDevice(); - createSwapChain(); - createImageViews(); - createRenderPass(); - createGraphicsPipeline(); - createFramebuffers(); - createCommandPool(); - createCommandBuffer(); - createSyncObjects(); +// AppData 구조체에 추가 +struct AppData { + // ... + image_available_semaphores: Vec, + render_finished_semaphores: Vec, + in_flight_fences: Vec, + // ... } -... - -void createSyncObjects() { - +// HelloTriangleApplication 구조체에 추가 +struct HelloTriangleApplication { + // ... + current_frame: usize, + // ... } ``` -Creating semaphores requires filling in the `VkSemaphoreCreateInfo`, but in the -current version of the API it doesn't actually have any required fields besides -`sType`: - -```c++ -void createSyncObjects() { - VkSemaphoreCreateInfo semaphoreInfo{}; - semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; +이제 동기화 객체를 생성하는 `create_sync_objects` 함수를 만들어 봅시다. + +```rust +// main.rs +impl HelloTriangleApplication { + pub fn new(event_loop: &EventLoop<()>) -> Self { + // ... + let command_buffers = + create_command_buffers(&device, &command_pool, &graphics_pipeline, &framebuffers, &render_pass, &app_data); + + let ( + image_available_semaphores, + render_finished_semaphores, + in_flight_fences, + ) = create_sync_objects(&device); + + let mut app_data = AppData { + // ... + image_available_semaphores, + render_finished_semaphores, + in_flight_fences, + }; + + Self { + // ... + app_data, + current_frame: 0, + } + } } -``` - -Future versions of the Vulkan API or extensions may add functionality for the -`flags` and `pNext` parameters like it does for the other structures. - -Creating a fence requires filling in the `VkFenceCreateInfo`: -```c++ -VkFenceCreateInfo fenceInfo{}; -fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; -``` - -Creating the semaphores and fence follows the familiar pattern with -`vkCreateSemaphore` & `vkCreateFence`: +// create.rs 또는 적절한 모듈 +fn create_sync_objects( + device: &ash::Device, +) -> ( + Vec, + Vec, + Vec, +) { + let mut image_available_semaphores = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); + let mut render_finished_semaphores = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); + let mut in_flight_fences = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); + + let semaphore_create_info = vk::SemaphoreCreateInfo::builder(); + + let fence_create_info = vk::FenceCreateInfo::builder() + .flags(vk::FenceCreateFlags::SIGNALED); // 첫 프레임에서 바로 통과하도록 신호된 상태로 생성 + + for _ in 0..MAX_FRAMES_IN_FLIGHT { + unsafe { + let image_available_semaphore = device + .create_semaphore(&semaphore_create_info, None) + .expect("Failed to create Semaphore Object!"); + let render_finished_semaphore = device + .create_semaphore(&semaphore_create_info, None) + .expect("Failed to create Semaphore Object!"); + let in_flight_fence = device + .create_fence(&fence_create_info, None) + .expect("Failed to create Fence Object!"); + + image_available_semaphores.push(image_available_semaphore); + render_finished_semaphores.push(render_finished_semaphore); + in_flight_fences.push(in_flight_fence); + } + } -```c++ -if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphore) != VK_SUCCESS || - vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphore) != VK_SUCCESS || - vkCreateFence(device, &fenceInfo, nullptr, &inFlightFence) != VK_SUCCESS) { - throw std::runtime_error("failed to create semaphores!"); + ( + image_available_semaphores, + render_finished_semaphores, + in_flight_fences, + ) } ``` - -The semaphores and fence should be cleaned up at the end of the program, when -all commands have finished and no more synchronization is necessary: - -```c++ -void cleanup() { - vkDestroySemaphore(device, imageAvailableSemaphore, nullptr); - vkDestroySemaphore(device, renderFinishedSemaphore, nullptr); - vkDestroyFence(device, inFlightFence, nullptr); -``` - -Onto the main drawing function! - -## Waiting for the previous frame - -At the start of the frame, we want to wait until the previous frame has -finished, so that the command buffer and semaphores are available to use. To do -that, we call `vkWaitForFences`: - -```c++ -void drawFrame() { - vkWaitForFences(device, 1, &inFlightFence, VK_TRUE, UINT64_MAX); +`ash`에서는 `vk::...CreateInfo::builder()` 패턴을 사용하여 구조체를 생성합니다. `vkCreate...` 함수 호출은 `unsafe` 블록 안에서 이루어지며, Rust의 `Result` 타입을 반환하므로 `expect`를 사용해 오류를 처리합니다. + +생성된 객체들은 `Drop` 트레잇 구현에서 정리해야 합니다. + +```rust +// main.rs +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + self.device.device_wait_idle().unwrap(); + + for i in 0..MAX_FRAMES_IN_FLIGHT { + self.device.destroy_semaphore(self.app_data.image_available_semaphores[i], None); + self.device.destroy_semaphore(self.app_data.render_finished_semaphores[i], None); + self.device.destroy_fence(self.app_data.in_flight_fences[i], None); + } + // ... 다른 리소스 정리 ... + } + } } ``` -The `vkWaitForFences` function takes an array of fences and waits on the host -for either any or all of the fences to be signaled before returning. The -`VK_TRUE` we pass here indicates that we want to wait for all fences, but in -the case of a single one it doesn't matter. This function also has a timeout -parameter that we set to the maximum value of a 64 bit unsigned integer, -`UINT64_MAX`, which effectively disables the timeout. - -After waiting, we need to manually reset the fence to the unsignaled state with -the `vkResetFences` call: -```c++ - vkResetFences(device, 1, &inFlightFence); -``` - -Before we can proceed, there is a slight hiccup in our design. On the first -frame we call `drawFrame()`, which immediately waits on `inFlightFence` to -be signaled. `inFlightFence` is only signaled after a frame has finished -rendering, yet since this is the first frame, there are no previous frames in -which to signal the fence! Thus `vkWaitForFences()` blocks indefinitely, -waiting on something which will never happen. - -Of the many solutions to this dilemma, there is a clever workaround built into -the API. Create the fence in the signaled state, so that the first call to -`vkWaitForFences()` returns immediately since the fence is already signaled. +## `draw_frame` 구현하기 -To do this, we add the `VK_FENCE_CREATE_SIGNALED_BIT` flag to the `VkFenceCreateInfo`: +### 이전 프레임 기다리기 -```c++ -void createSyncObjects() { - ... +프레임 시작 시, 현재 프레임에 할당된 펜스를 기다려 이전 작업이 완료되었는지 확인합니다. - VkFenceCreateInfo fenceInfo{}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - - ... +```rust +fn draw_frame(&mut self) { + let fence = self.app_data.in_flight_fences[self.current_frame]; + + unsafe { + self.device + .wait_for_fences(&[fence], true, u64::MAX) + .expect("Failed to wait for Fence!"); + } } ``` - -## Acquiring an image from the swap chain - -The next thing we need to do in the `drawFrame` function is acquire an image -from the swap chain. Recall that the swap chain is an extension feature, so we -must use a function with the `vk*KHR` naming convention: - -```c++ -void drawFrame() { - ... - - uint32_t imageIndex; - vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex); +`wait_for_fences`는 펜스 슬라이스(`&[fence]`)를 인자로 받습니다. `true`는 모든 펜스를 기다리겠다는 의미이며, 타임아웃은 `u64::MAX`로 설정하여 비활성화합니다. + +*참고: C++ 버전의 첫 프레임 교착 상태 문제는 펜스를 `vk::FenceCreateFlags::SIGNALED` 플래그와 함께 생성하여 해결했습니다. `wait_for_fences`가 첫 호출에서 즉시 반환될 것입니다.* + +### 스왑 체인에서 이미지 가져오기 + +다음으로 스왑 체인에서 렌더링할 이미지를 가져옵니다. 이 작업이 완료되면 `image_available_semaphores`가 신호를 받습니다. + +```rust +// draw_frame 함수 내 +let image_available_semaphore = self.app_data.image_available_semaphores[self.current_frame]; + +let (image_index, _is_suboptimal) = unsafe { + self.swapchain_loader + .acquire_next_image( + self.app_data.swapchain, + u64::MAX, + image_available_semaphore, + vk::Fence::null(), + ) + .expect("Failed to acquire next image.") +}; + +// 펜스를 기다린 후에는 재사용하기 전에 반드시 리셋해야 합니다. +unsafe { + self.device + .reset_fences(&[fence]) + .expect("Failed to reset Fence!"); } ``` +`acquire_next_image`는 이미지 인덱스와 스왑체인이 최적 상태가 아님을 나타내는 bool 값을 튜플로 반환합니다. 지금은 `_is_suboptimal` 값을 무시하지만, 창 크기 조절 등을 처리할 때 중요해집니다. 펜스는 이미지 획득 *후*에 리셋하여 CPU-GPU 병렬 실행을 극대화할 수 있습니다. -The first two parameters of `vkAcquireNextImageKHR` are the logical device and -the swap chain from which we wish to acquire an image. The third parameter -specifies a timeout in nanoseconds for an image to become available. Using the -maximum value of a 64 bit unsigned integer means we effectively disable the -timeout. - -The next two parameters specify synchronization objects that are to be signaled -when the presentation engine is finished using the image. That's the point in -time where we can start drawing to it. It is possible to specify a semaphore, -fence or both. We're going to use our `imageAvailableSemaphore` for that purpose -here. - -The last parameter specifies a variable to output the index of the swap chain -image that has become available. The index refers to the `VkImage` in our -`swapChainImages` array. We're going to use that index to pick the `VkFrameBuffer`. - -## Recording the command buffer - -With the imageIndex specifying the swap chain image to use in hand, we can now -record the command buffer. First, we call `vkResetCommandBuffer` on the command -buffer to make sure it is able to be recorded. - -```c++ -vkResetCommandBuffer(commandBuffer, 0); -``` - -The second parameter of `vkResetCommandBuffer` is a `VkCommandBufferResetFlagBits` -flag. Since we don't want to do anything special, we leave it as 0. - -Now call the function `recordCommandBuffer` to record the commands we want. - -```c++ -recordCommandBuffer(commandBuffer, imageIndex); -``` - -With a fully recorded command buffer, we can now submit it. +### 커맨드 버퍼 기록 및 제출 -## Submitting the command buffer +이제 이미지를 사용할 수 있으므로, 해당 이미지에 그리는 커맨드 버퍼를 다시 기록하고 제출합니다. -Queue submission and synchronization is configured through parameters in the -`VkSubmitInfo` structure. +```rust +// draw_frame 함수 내 +let command_buffer = self.command_buffers[image_index as usize]; -```c++ -VkSubmitInfo submitInfo{}; -submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - -VkSemaphore waitSemaphores[] = {imageAvailableSemaphore}; -VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; -submitInfo.waitSemaphoreCount = 1; -submitInfo.pWaitSemaphores = waitSemaphores; -submitInfo.pWaitDstStageMask = waitStages; -``` - -The first three parameters specify which semaphores to wait on before execution -begins and in which stage(s) of the pipeline to wait. We want to wait with -writing colors to the image until it's available, so we're specifying the stage -of the graphics pipeline that writes to the color attachment. That means that -theoretically the implementation can already start executing our vertex shader -and such while the image is not yet available. Each entry in the `waitStages` -array corresponds to the semaphore with the same index in `pWaitSemaphores`. - -```c++ -submitInfo.commandBufferCount = 1; -submitInfo.pCommandBuffers = &commandBuffer; -``` - -The next two parameters specify which command buffers to actually submit for -execution. We simply submit the single command buffer we have. - -```c++ -VkSemaphore signalSemaphores[] = {renderFinishedSemaphore}; -submitInfo.signalSemaphoreCount = 1; -submitInfo.pSignalSemaphores = signalSemaphores; -``` - -The `signalSemaphoreCount` and `pSignalSemaphores` parameters specify which -semaphores to signal once the command buffer(s) have finished execution. In our -case we're using the `renderFinishedSemaphore` for that purpose. - -```c++ -if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFence) != VK_SUCCESS) { - throw std::runtime_error("failed to submit draw command buffer!"); +unsafe { + self.device + .reset_command_buffer(command_buffer, vk::CommandBufferResetFlags::empty()) + .expect("Failed to reset Command Buffer!"); } -``` -We can now submit the command buffer to the graphics queue using -`vkQueueSubmit`. The function takes an array of `VkSubmitInfo` structures as -argument for efficiency when the workload is much larger. The last parameter -references an optional fence that will be signaled when the command buffers -finish execution. This allows us to know when it is safe for the command -buffer to be reused, thus we want to give it `inFlightFence`. Now on the next -frame, the CPU will wait for this command buffer to finish executing before it -records new commands into it. - -## Subpass dependencies - -Remember that the subpasses in a render pass automatically take care of image -layout transitions. These transitions are controlled by *subpass dependencies*, -which specify memory and execution dependencies between subpasses. We have only -a single subpass right now, but the operations right before and right after this -subpass also count as implicit "subpasses". - -There are two built-in dependencies that take care of the transition at the -start of the render pass and at the end of the render pass, but the former does -not occur at the right time. It assumes that the transition occurs at the start -of the pipeline, but we haven't acquired the image yet at that point! There are -two ways to deal with this problem. We could change the `waitStages` for the -`imageAvailableSemaphore` to `VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT` to ensure that -the render passes don't begin until the image is available, or we can make the -render pass wait for the `VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT` stage. -I've decided to go with the second option here, because it's a good excuse to -have a look at subpass dependencies and how they work. - -Subpass dependencies are specified in `VkSubpassDependency` structs. Go to the -`createRenderPass` function and add one: - -```c++ -VkSubpassDependency dependency{}; -dependency.srcSubpass = VK_SUBPASS_EXTERNAL; -dependency.dstSubpass = 0; -``` - -The first two fields specify the indices of the dependency and the dependent -subpass. The special value `VK_SUBPASS_EXTERNAL` refers to the implicit subpass -before or after the render pass depending on whether it is specified in -`srcSubpass` or `dstSubpass`. The index `0` refers to our subpass, which is the -first and only one. The `dstSubpass` must always be higher than `srcSubpass` to -prevent cycles in the dependency graph (unless one of the subpasses is -`VK_SUBPASS_EXTERNAL`). - -```c++ -dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; -dependency.srcAccessMask = 0; -``` - -The next two fields specify the operations to wait on and the stages in which -these operations occur. We need to wait for the swap chain to finish reading -from the image before we can access it. This can be accomplished by waiting on -the color attachment output stage itself. - -```c++ -dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; -dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; -``` - -The operations that should wait on this are in the color attachment stage and -involve the writing of the color attachment. These settings will -prevent the transition from happening until it's actually necessary (and -allowed): when we want to start writing colors to it. - -```c++ -renderPassInfo.dependencyCount = 1; -renderPassInfo.pDependencies = &dependency; +// 이전 장에서 만든 함수를 호출 +record_command_buffer( + &self.device, + command_buffer, + self.app_data.render_pass, + self.app_data.framebuffers[image_index as usize], + self.app_data.graphics_pipeline, + self.app_data.swapchain_extent, +); + +let wait_semaphores = [image_available_semaphore]; +let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT]; +let signal_semaphores = [self.app_data.render_finished_semaphores[self.current_frame]]; + +let submit_infos = [vk::SubmitInfo::builder() + .wait_semaphores(&wait_semaphores) + .wait_dst_stage_mask(&wait_stages) + .command_buffers(&[command_buffer]) + .signal_semaphores(&signal_semaphores) + .build()]; + +unsafe { + self.device + .queue_submit(self.graphics_queue, &submit_infos, fence) + .expect("Failed to submit draw command buffer!"); +} ``` +`ash`의 빌더 패턴을 사용하여 `VkSubmitInfo`를 간결하게 생성합니다. `queue_submit`의 마지막 인자로 `fence`를 전달하여, 이 작업이 끝나면 해당 펜스에 신호를 보내도록 합니다. -The `VkRenderPassCreateInfo` struct has two fields to specify an array of -dependencies. +### 서브패스 종속성 -## Presentation +이미지 레이아웃 전환을 올바른 시점에 수행하기 위해 서브패스 종속성을 설정해야 합니다. `acquire_next_image`가 완료된 후, 그리고 우리가 이미지에 색상을 쓰기 시작하기 전에 전환이 일어나야 합니다. `create_render_pass` 함수를 수정합니다. -The last step of drawing a frame is submitting the result back to the swap chain -to have it eventually show up on the screen. Presentation is configured through -a `VkPresentInfoKHR` structure at the end of the `drawFrame` function. +```rust +// create.rs 또는 적절한 모듈 +let dependency = vk::SubpassDependency::builder() + .src_subpass(vk::SUBPASS_EXTERNAL) + .dst_subpass(0) + .src_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT) + .src_access_mask(vk::AccessFlags::empty()) + .dst_stage_mask(vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT) + .dst_access_mask(vk::AccessFlags::COLOR_ATTACHMENT_WRITE) + .build(); -```c++ -VkPresentInfoKHR presentInfo{}; -presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; +let dependencies = [dependency]; -presentInfo.waitSemaphoreCount = 1; -presentInfo.pWaitSemaphores = signalSemaphores; +let render_pass_info = vk::RenderPassCreateInfo::builder() + .attachments(&attachments) + .subpasses(&subpasses) + .dependencies(&dependencies); // 종속성 추가 ``` +이 종속성은 외부(스왑체인 이미지 획득)에서 우리의 첫 번째 서브패스(인덱스 0)로의 전환을 제어합니다. `COLOR_ATTACHMENT_OUTPUT` 단계에서 쓰기 작업(`COLOR_ATTACHMENT_WRITE`)을 시작하기 직전에 전환이 발생하도록 보장합니다. -The first two parameters specify which semaphores to wait on before presentation -can happen, just like `VkSubmitInfo`. Since we want to wait on the command buffer -to finish execution, thus our triangle being drawn, we take the semaphores -which will be signalled and wait on them, thus we use `signalSemaphores`. +### 프레젠테이션 +마지막으로 렌더링된 이미지를 화면에 표시하기 위해 프레젠테이션 큐에 제출합니다. -```c++ -VkSwapchainKHR swapChains[] = {swapChain}; -presentInfo.swapchainCount = 1; -presentInfo.pSwapchains = swapChains; -presentInfo.pImageIndices = &imageIndex; -``` +```rust +// draw_frame 함수 내 +let swapchains = [self.app_data.swapchain]; +let image_indices = [image_index]; -The next two parameters specify the swap chains to present images to and the -index of the image for each swap chain. This will almost always be a single one. - -```c++ -presentInfo.pResults = nullptr; // Optional -``` +let present_info = vk::PresentInfoKHR::builder() + .wait_semaphores(&signal_semaphores) // 렌더링이 끝나면 신호를 받는 세마포어를 기다림 + .swapchains(&swapchains) + .image_indices(&image_indices); -There is one last optional parameter called `pResults`. It allows you to specify -an array of `VkResult` values to check for every individual swap chain if -presentation was successful. It's not necessary if you're only using a single -swap chain, because you can simply use the return value of the present function. +unsafe { + self.swapchain_loader + .queue_present(self.present_queue, &present_info) + .expect("Failed to present queue."); +} -```c++ -vkQueuePresentKHR(presentQueue, &presentInfo); +// 다음 프레임을 위해 프레임 인덱스를 업데이트 +self.current_frame = (self.current_frame + 1) % MAX_FRAMES_IN_FLIGHT; ``` +`queue_present`는 렌더링이 완료되었음을 알리는 `render_finished_semaphore`를 기다린 후 실행됩니다. 마지막으로 `current_frame` 인덱스를 순환시켜 다음 `draw_frame` 호출에서 다음 프레임의 동기화 객체 세트를 사용하도록 합니다. -The `vkQueuePresentKHR` function submits the request to present an image to the -swap chain. We'll add error handling for both `vkAcquireNextImageKHR` and -`vkQueuePresentKHR` in the next chapter, because their failure does not -necessarily mean that the program should terminate, unlike the functions we've -seen so far. - -If you did everything correctly up to this point, then you should now see -something resembling the following when you run your program: - -![](/images/triangle.png) - ->This colored triangle may look a bit different from the one you're used to seeing in graphics tutorials. That's because this tutorial lets the shader interpolate in linear color space and converts to sRGB color space afterwards. See [this blog post](https://medium.com/@heypete/hello-triangle-meet-swift-and-wide-color-6f9e246616d9) for a discussion of the difference. - -Yay! Unfortunately, you'll see that when validation layers are enabled, the -program crashes as soon as you close it. The messages printed to the terminal -from `debugCallback` tell us why: - -![](/images/semaphore_in_use.png) +### 유휴 상태 대기 -Remember that all of the operations in `drawFrame` are asynchronous. That means -that when we exit the loop in `mainLoop`, drawing and presentation operations -may still be going on. Cleaning up resources while that is happening is a bad -idea. +프로그램이 종료될 때, 모든 비동기 작업이 완료될 때까지 기다려야 리소스를 안전하게 해제할 수 있습니다. `main_loop`가 끝난 후와 `Drop` 구현의 시작 부분에서 `device_wait_idle`을 호출합니다. -To fix that problem, we should wait for the logical device to finish operations -before exiting `mainLoop` and destroying the window: - -```c++ -void mainLoop() { - while (!glfwWindowShouldClose(window)) { - glfwPollEvents(); - drawFrame(); - } - - vkDeviceWaitIdle(device); +```rust +// main_loop에서 이벤트 루프가 끝난 후 (실제로는 winit의 클로저 밖) +// 또는 Drop 구현에서 +unsafe { + self.device.device_wait_idle().unwrap(); } ``` +이제 프로그램을 실행하면 화면에 삼각형이 나타나고, 창을 닫아도 유효성 검사 오류 없이 깔끔하게 종료될 것입니다. -You can also wait for operations in a specific command queue to be finished with -`vkQueueWaitIdle`. These functions can be used as a very rudimentary way to -perform synchronization. You'll see that the program now exits without problems -when closing the window. - -## Conclusion - -A little over 900 lines of code later, we've finally gotten to the stage of seeing -something pop up on the screen! Bootstrapping a Vulkan program is definitely a -lot of work, but the take-away message is that Vulkan gives you an immense -amount of control through its explicitness. I recommend you to take some time -now to reread the code and build a mental model of the purpose of all of the -Vulkan objects in the program and how they relate to each other. We'll be -building on top of that knowledge to extend the functionality of the program -from this point on. +## 결론 -The next chapter will expand the render loop to handle multiple frames in flight. +상당한 양의 코드를 통해 드디어 화면에 무언가를 띄웠습니다! Vulkan의 부트스트래핑 과정은 복잡하지만, 그만큼 명시성을 통해 강력한 제어권을 얻을 수 있습니다. 지금까지 작성한 코드를 다시 살펴보며 각 Vulkan 객체의 역할과 상호 관계를 이해하는 시간을 갖는 것이 좋습니다. -[C++ code](/code/15_hello_triangle.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +다음 장에서는 여러 프레임을 동시에 처리하는 렌더링 루프를 더욱 견고하게 만들고 창 크기 변경과 같은 예외 상황을 처리하는 방법을 알아보겠습니다. \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md b/ko-rust/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md index e2345e31..c72e7c65 100644 --- a/ko-rust/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md +++ b/ko-rust/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md @@ -1,176 +1,182 @@ -## Frames in flight +## 동시에 여러 프레임 렌더링하기 (Frames in flight) -Right now our render loop has one glaring flaw. We are required to wait on the -previous frame to finish before we can start rendering the next which results -in unnecessary idling of the host. +현재 우리의 렌더 루프에는 한 가지 명백한 결함이 있습니다. 이전 프레임의 렌더링이 끝나기를 기다려야만 다음 프레임의 렌더링을 시작할 수 있다는 점인데, 이는 호스트(CPU)의 불필요한 유휴 상태를 유발합니다. -The way to fix this is to allow multiple frames to be *in-flight* at once, that -is to say, allow the rendering of one frame to not interfere with the recording -of the next. How do we do this? Any resource that is accessed and modified -during rendering must be duplicated. Thus, we need multiple command buffers, -semaphores, and fences. In later chapters we will also add multiple instances -of other resources, so we will see this concept reappear. +이 문제를 해결하는 방법은 여러 프레임을 동시에 *작업 중(in-flight)* 상태로 두는 것입니다. 즉, 한 프레임의 렌더링이 다음 프레임의 기록을 방해하지 않도록 하는 것입니다. 어떻게 이렇게 할 수 있을까요? 렌더링 중에 접근하고 수정하는 모든 리소스를 복제해야 합니다. 따라서 여러 개의 커맨드 버퍼, 세마포어, 펜스가 필요합니다. 이후 챕터에서는 다른 리소스들의 여러 인스턴스도 추가할 것이므로, 이 개념은 다시 등장하게 될 것입니다. -Start by adding a constant at the top of the program that defines how many -frames should be processed concurrently: +먼저 프로그램 상단에 동시에 처리할 프레임 수를 정의하는 상수를 추가합니다. Rust에서는 `usize` 타입을 사용하는 것이 일반적입니다. -```c++ -const int MAX_FRAMES_IN_FLIGHT = 2; +```rust +const MAX_FRAMES_IN_FLIGHT: usize = 2; ``` -We choose the number 2 because we don't want the CPU to get *too* far ahead of -the GPU. With 2 frames in flight, the CPU and the GPU can be working on their -own tasks at the same time. If the CPU finishes early, it will wait till the -GPU finishes rendering before submitting more work. With 3 or more frames in -flight, the CPU could get ahead of the GPU, adding frames of latency. -Generally, extra latency isn't desired. But giving the application control over -the number of frames in flight is another example of Vulkan being explicit. +2를 선택한 이유는 CPU가 GPU보다 *너무* 앞서 나가는 것을 원치 않기 때문입니다. 2개의 프레임이 동시 실행되면, CPU와 GPU가 동시에 각자의 작업을 처리할 수 있습니다. 만약 CPU가 먼저 작업을 마치면, GPU가 렌더링을 마칠 때까지 기다렸다가 다음 작업을 제출합니다. 3개 이상의 프레임을 사용하면 CPU가 GPU를 앞질러 지연 시간(latency)을 추가할 수 있습니다. 일반적으로 추가적인 지연 시간은 바람직하지 않습니다. 하지만 애플리케이션에 동시 실행 프레임 수를 제어할 수 있는 권한을 주는 것은 Vulkan의 명시적인(explicit) 특성을 보여주는 또 다른 예시입니다. -Each frame should have its own command buffer, set of semaphores, and fence. -Rename and then change them to be `std::vector`s of the objects: +각 프레임은 자체적인 커맨드 버퍼, 세마포어 집합, 펜스를 가져야 합니다. 기존 객체들을 `Vec`으로 변경합니다. `App` 구조체 내의 필드들을 다음과 같이 수정합니다. -```c++ -std::vector commandBuffers; - -... - -std::vector imageAvailableSemaphores; -std::vector renderFinishedSemaphores; -std::vector inFlightFences; +```rust +struct App { + // ... + command_buffers: Vec, + image_available_semaphores: Vec, + render_finished_semaphores: Vec, + in_flight_fences: Vec, + // ... +} ``` -Then we need to create multiple command buffers. Rename `createCommandBuffer` -to `createCommandBuffers`. Next we need to resize the command buffers vector -to the size of `MAX_FRAMES_IN_FLIGHT`, alter the `VkCommandBufferAllocateInfo` -to contain that many command buffers, and then change the destination to our -vector of command buffers: +다음으로 여러 개의 커맨드 버퍼를 생성해야 합니다. `create_command_buffers` 함수를 수정합니다. `ash`의 `allocate_command_buffers` 함수는 `Vec`를 반환하므로, 반환된 벡터를 바로 필드에 할당하면 됩니다. -```c++ -void createCommandBuffers() { - commandBuffers.resize(MAX_FRAMES_IN_FLIGHT); - ... - allocInfo.commandBufferCount = (uint32_t) commandBuffers.size(); +```rust +fn create_command_buffers(&mut self) { + let command_buffer_allocate_info = vk::CommandBufferAllocateInfo::builder() + .command_pool(self.command_pool) + .level(vk::CommandBufferLevel::PRIMARY) + .command_buffer_count(MAX_FRAMES_IN_FLIGHT as u32); - if (vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()) != VK_SUCCESS) { - throw std::runtime_error("failed to allocate command buffers!"); - } + self.command_buffers = unsafe { + self.device + .allocate_command_buffers(&command_buffer_allocate_info) + .expect("Failed to allocate Command Buffers!") + }; } ``` -The `createSyncObjects` function should be changed to create all of the objects: - -```c++ -void createSyncObjects() { - imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT); - renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT); - inFlightFences.resize(MAX_FRAMES_IN_FLIGHT); - - VkSemaphoreCreateInfo semaphoreInfo{}; - semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; - - VkFenceCreateInfo fenceInfo{}; - fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; - fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; - - for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { - if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS || - vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS || - vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) != VK_SUCCESS) { - - throw std::runtime_error("failed to create synchronization objects for a frame!"); +`create_sync_objects` 함수는 모든 동기화 객체들을 생성하도록 변경해야 합니다. Ash의 빌더 패턴을 사용하여 생성 정보를 만들고, 루프 내에서 객체들을 생성합니다. + +```rust +fn create_sync_objects(&mut self) { + self.image_available_semaphores = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); + self.render_finished_semaphores = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); + self.in_flight_fences = Vec::with_capacity(MAX_FRAMES_IN_FLIGHT); + + let semaphore_info = vk::SemaphoreCreateInfo::builder(); + let fence_info = vk::FenceCreateInfo::builder() + .flags(vk::FenceCreateFlags::SIGNALED); + + for _ in 0..MAX_FRAMES_IN_FLIGHT { + unsafe { + let image_available_semaphore = self + .device + .create_semaphore(&semaphore_info, None) + .expect("Failed to create Semaphore Object!"); + let render_finished_semaphore = self + .device + .create_semaphore(&semaphore_info, None) + .expect("Failed to create Semaphore Object!"); + let in_flight_fence = self + .device + .create_fence(&fence_info, None) + .expect("Failed to create Fence Object!"); + + self.image_available_semaphores.push(image_available_semaphore); + self.render_finished_semaphores.push(render_finished_semaphore); + self.in_flight_fences.push(in_flight_fence); } } } ``` -Similarly, they should also all be cleaned up: +마찬가지로, 이 객체들도 `cleanup`에서 모두 정리되어야 합니다. Rust의 `unsafe` 블록 안에서 파괴 함수를 호출합니다. -```c++ -void cleanup() { - for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { - vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr); - vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr); - vkDestroyFence(device, inFlightFences[i], nullptr); +```rust +unsafe fn cleanup(&mut self) { + for i in 0..MAX_FRAMES_IN_FLIGHT { + self.device.destroy_semaphore(self.render_finished_semaphores[i], None); + self.device.destroy_semaphore(self.image_available_semaphores[i], None); + self.device.destroy_fence(self.in_flight_fences[i], None); } - - ... + // ... } ``` -Remember, because command buffers are freed for us when we free the command -pool, there is nothing extra to do for command buffer cleanup. +커맨드 버퍼는 커맨드 풀이 해제될 때 자동으로 해제되므로, 커맨드 버퍼 정리를 위해 추가로 할 일은 없습니다. -To use the right objects every frame, we need to keep track of the current -frame. We will use a frame index for that purpose: +매 프레임마다 올바른 객체를 사용하기 위해, 현재 프레임을 추적해야 합니다. 이를 위해 `App` 구조체에 프레임 인덱스를 추가합니다. -```c++ -uint32_t currentFrame = 0; +```rust +struct App { + // ... + current_frame: usize, + // ... +} +// App::new()에서 초기화... +current_frame: 0, ``` -The `drawFrame` function can now be modified to use the right objects: - -```c++ -void drawFrame() { - vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); - vkResetFences(device, 1, &inFlightFences[currentFrame]); - - vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); +이제 `draw_frame` 함수를 올바른 객체들을 사용하도록 수정할 수 있습니다. `self.current_frame`을 사용하여 각 프레임에 맞는 동기화 객체와 커맨드 버퍼에 접근합니다. - ... +```rust +fn draw_frame(&mut self) { + let in_flight_fence = self.in_flight_fences[self.current_frame]; - vkResetCommandBuffer(commandBuffers[currentFrame], 0); - recordCommandBuffer(commandBuffers[currentFrame], imageIndex); + unsafe { + self.device + .wait_for_fences(&[in_flight_fence], true, u64::MAX) + .expect("Failed to wait for Fence!"); - ... - - submitInfo.pCommandBuffers = &commandBuffers[currentFrame]; - - ... - - VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]}; - - ... - - VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]}; + self.device + .reset_fences(&[in_flight_fence]) + .expect("Failed to reset Fence!"); + } - ... + let (image_index, _is_suboptimal) = unsafe { + self.swapchain_loader + .acquire_next_image( + self.swapchain, + u64::MAX, + self.image_available_semaphores[self.current_frame], + vk::Fence::null(), + ) + .expect("Failed to acquire next image.") + }; + + let command_buffer = self.command_buffers[self.current_frame]; + unsafe { + self.device + .reset_command_buffer(command_buffer, vk::CommandBufferResetFlags::empty()) + .expect("Failed to reset Command Buffer!"); + } - if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) { + self.record_command_buffer(command_buffer, image_index); + + let wait_semaphores = [self.image_available_semaphores[self.current_frame]]; + let wait_stages = [vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT]; + let signal_semaphores = [self.render_finished_semaphores[self.current_frame]]; + + let submit_infos = [vk::SubmitInfo::builder() + .wait_semaphores(&wait_semaphores) + .wait_dst_stage_mask(&wait_stages) + .command_buffers(&[command_buffer]) + .signal_semaphores(&signal_semaphores) + .build()]; + + unsafe { + self.device + .queue_submit(self.graphics_queue, &submit_infos, in_flight_fence) + .expect("Failed to submit draw command buffer!"); + } + + // ... Present KHR 로직 ... } ``` -Of course, we shouldn't forget to advance to the next frame every time: - -```c++ -void drawFrame() { - ... +물론, 매번 다음 프레임으로 넘어가는 것을 잊지 말아야 합니다. `draw_frame` 함수의 마지막 부분에 추가합니다. - currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +```rust +fn draw_frame(&mut self) { + // ... + + self.current_frame = (self.current_frame + 1) % MAX_FRAMES_IN_FLIGHT; } ``` -By using the modulo (%) operator, we ensure that the frame index loops around -after every `MAX_FRAMES_IN_FLIGHT` enqueued frames. - - - -We've now implemented all the needed synchronization to ensure that there are -no more than `MAX_FRAMES_IN_FLIGHT` frames of work enqueued and that these -frames are not stepping over eachother. Note that it is fine for other parts of -the code, like the final cleanup, to rely on more rough synchronization like -`vkDeviceWaitIdle`. You should decide on which approach to use based on -performance requirements. - -To learn more about synchronization through examples, have a look at [this extensive overview](https://github.com/KhronosGroup/Vulkan-Docs/wiki/Synchronization-Examples#swapchain-image-acquire-and-present) by Khronos. - +나머지(%) 연산자를 사용함으로써, 프레임 인덱스는 `MAX_FRAMES_IN_FLIGHT` 만큼의 프레임이 큐에 쌓인 후 다시 순환하게 됩니다. -In the next chapter we'll deal with one more small thing that is required for a -well-behaved Vulkan program. +이제 우리는 최대 `MAX_FRAMES_IN_FLIGHT`개의 프레임만 작업 큐에 쌓이도록 하고, 이 프레임들이 서로를 침범하지 않도록 하는 데 필요한 모든 동기화를 구현했습니다. 최종 정리(cleanup)와 같은 코드의 다른 부분에서는 `device.device_wait_idle()`처럼 더 단순한 동기화에 의존해도 괜찮다는 점에 유의하세요. 성능 요구사항에 따라 어떤 접근 방식을 사용할지 결정해야 합니다. +동기화에 대해 예제를 통해 더 배우고 싶다면, Khronos의 [이 광범위한 개요](https://github.com/KhronosGroup/Vulkan-Docs/wiki/Synchronization-Examples#swapchain-image-acquire-and-present)를 살펴보세요. -[C++ code](/code/16_frames_in_flight.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +다음 챕터에서는 잘 동작하는 Vulkan 프로그램을 위해 필요한 또 다른 작은 사항을 다룰 것입니다. \ No newline at end of file diff --git a/ko-rust/03_Drawing_a_triangle/04_Swap_chain_recreation.md b/ko-rust/03_Drawing_a_triangle/04_Swap_chain_recreation.md index ce58528b..8042c7e6 100644 --- a/ko-rust/03_Drawing_a_triangle/04_Swap_chain_recreation.md +++ b/ko-rust/03_Drawing_a_triangle/04_Swap_chain_recreation.md @@ -1,280 +1,282 @@ -## Introduction +## 소개 -The application we have now successfully draws a triangle, but there are some -circumstances that it isn't handling properly yet. It is possible for the window -surface to change such that the swap chain is no longer compatible with it. One -of the reasons that could cause this to happen is the size of the window -changing. We have to catch these events and recreate the swap chain. +지금까지 우리가 만든 애플리케이션은 성공적으로 삼각형을 그리지만, 아직 제대로 처리하지 못하는 몇 가지 상황이 있습니다. 윈도우 서피스가 변경되어 스왑 체인이 더 이상 호환되지 않는 경우가 발생할 수 있습니다. 이런 상황이 발생하는 원인 중 하나는 윈도우 크기가 변경되는 것입니다. 우리는 이러한 이벤트를 감지하고 스왑 체인을 다시 만들어야 합니다. -## Recreating the swap chain +## 스왑 체인 재구성 -Create a new `recreateSwapChain` function that calls `createSwapChain` and all -of the creation functions for the objects that depend on the swap chain or the -window size. +`recreate_swapchain`이라는 새로운 함수를 만들어, `create_swapchain`과 스왑 체인 또는 윈도우 크기에 의존하는 모든 객체들의 생성 함수를 호출하도록 합시다. -```c++ -void recreateSwapChain() { - vkDeviceWaitIdle(device); +```rust +fn recreate_swapchain(&mut self) -> Result<(), Box> { + unsafe { + self.device.device_wait_idle()?; + } + + self.create_swapchain()?; + self.create_image_views()?; + self.create_framebuffers()?; - createSwapChain(); - createImageViews(); - createFramebuffers(); + Ok(()) } ``` -We first call `vkDeviceWaitIdle`, because just like in the last chapter, we -shouldn't touch resources that may still be in use. Obviously, we'll have to recreate -the swap chain itself. The image views need to be recreated because they are based -directly on the swap chain images. Finally, the framebuffers directly depend on the -swap chain images, and thus must be recreated as well. +먼저 `device_wait_idle`을 호출하는데, 이는 이전 장에서와 마찬가지로 아직 사용 중일 수 있는 리소스에 접근해서는 안 되기 때문입니다. 이 함수는 `unsafe` 블록 안에서 호출해야 합니다. 당연히 스왑 체인 자체를 다시 만들어야 합니다. 이미지 뷰는 스왑 체인 이미지에 직접 기반하므로 다시 만들어야 합니다. 마지막으로, 프레임버퍼는 스왑 체인 이미지에 직접 의존하므로 다시 만들어야 합니다. + +이러한 객체들의 이전 버전이 재생성되기 전에 확실히 정리되도록, 일부 정리 코드를 별도의 함수로 옮겨 `recreate_swapchain` 함수에서 호출하도록 합시다. 이 함수를 `cleanup_swapchain`이라고 부르겠습니다. -To make sure that the old versions of these objects are cleaned up before -recreating them, we should move some of the cleanup code to a separate function -that we can call from the `recreateSwapChain` function. Let's call it -`cleanupSwapChain`: +```rust +fn cleanup_swapchain(&mut self) { + unsafe { + self.framebuffers + .iter() + .for_each(|&framebuffer| self.device.destroy_framebuffer(framebuffer, None)); -```c++ -void cleanupSwapChain() { + self.swapchain_image_views + .iter() + .for_each(|&view| self.device.destroy_image_view(view, None)); + self.swapchain_loader + .destroy_swapchain(self.swapchain, None); + } } -void recreateSwapChain() { - vkDeviceWaitIdle(device); +fn recreate_swapchain(&mut self) -> Result<(), Box> { + unsafe { + self.device.device_wait_idle()?; + } - cleanupSwapChain(); + self.cleanup_swapchain(); - createSwapChain(); - createImageViews(); - createFramebuffers(); + self.create_swapchain()?; + self.create_image_views()?; + self.create_framebuffers()?; + + Ok(()) } ``` -Note that we don't recreate the renderpass here for simplicity. In theory it can be possible for the swap chain image format to change during an applications' lifetime, e.g. when moving a window from a standard range to a high dynamic range monitor. This may require the application to recreate the renderpass to make sure the change between dynamic ranges is properly reflected. - -We'll move the cleanup code of all objects that are recreated as part of a swap -chain refresh from `cleanup` to `cleanupSwapChain`: +여기서는 간단하게 하기 위해 렌더 패스를 다시 만들지 않는다는 점에 유의하세요. 이론적으로는 애플리케이션 실행 중에 스왑 체인 이미지 포맷이 변경될 수 있습니다. 예를 들어, 표준 다이나믹 레인지(SDR) 모니터에서 하이 다이나믹 레인지(HDR) 모니터로 창을 옮기는 경우가 그렇습니다. 이 경우 다이나믹 레인지 간의 변경이 올바르게 반영되도록 애플리케이션이 렌더 패스를 다시 만들어야 할 수도 있습니다. -```c++ -void cleanupSwapChain() { - for (auto framebuffer : swapChainFramebuffers) { - vkDestroyFramebuffer(device, framebuffer, nullptr); - } +스왑 체인 갱신의 일부로 재생성되는 모든 객체들의 정리 코드를 `cleanup` (Rust에서는 `Drop` 트레이트 구현)에서 `cleanup_swapchain`으로 옮기겠습니다. Rust에서는 리소스 해제를 위해 `Drop` 트레이트를 구현하는 것이 일반적입니다. - for (auto imageView : swapChainImageViews) { - vkDestroyImageView(device, imageView, nullptr); - } +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + self.device.device_wait_idle().unwrap(); - vkDestroySwapchainKHR(device, swapChain, nullptr); -} + self.cleanup_swapchain(); -void cleanup() { - cleanupSwapChain(); + self.device.destroy_pipeline(self.graphics_pipeline, None); + self.device + .destroy_pipeline_layout(self.pipeline_layout, None); - vkDestroyPipeline(device, graphicsPipeline, nullptr); - vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + self.device.destroy_render_pass(self.render_pass, None); - vkDestroyRenderPass(device, renderPass, nullptr); + for i in 0..MAX_FRAMES_IN_FLIGHT { + self.device + .destroy_semaphore(self.render_finished_semaphores[i], None); + self.device + .destroy_semaphore(self.image_available_semaphores[i], None); + self.device.destroy_fence(self.in_flight_fences[i], None); + } - for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { - vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr); - vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr); - vkDestroyFence(device, inFlightFences[i], nullptr); - } + self.device.destroy_command_pool(self.command_pool, None); - vkDestroyCommandPool(device, commandPool, nullptr); + self.device.destroy_device(None); - vkDestroyDevice(device, nullptr); + if VALIDATION.is_enable { + self.debug_utils_loader + .destroy_debug_utils_messenger(self.debug_messenger, None); + } - if (enableValidationLayers) { - DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr); + self.surface_loader.destroy_surface(self.surface, None); + self.instance.destroy_instance(None); + } } - - vkDestroySurfaceKHR(instance, surface, nullptr); - vkDestroyInstance(instance, nullptr); - - glfwDestroyWindow(window); - - glfwTerminate(); } ``` +`choose_swap_extent`에서는 이미 새로운 윈도우 해상도를 조회하여 스왑 체인 이미지가 (새로운) 올바른 크기를 갖도록 하고 있으므로, `choose_swap_extent`를 수정할 필요는 없습니다 (스왑 체인을 만들 때 이미 서피스의 해상도를 픽셀 단위로 얻기 위해 `window.get_framebuffer_size()`를 사용해야 했던 것을 기억하세요). -Note that in `chooseSwapExtent` we already query the new window resolution to -make sure that the swap chain images have the (new) right size, so there's no -need to modify `chooseSwapExtent` (remember that we already had to use -`glfwGetFramebufferSize` to get the resolution of the surface in pixels when -creating the swap chain). - -That's all it takes to recreate the swap chain! However, the disadvantage of -this approach is that we need to stop all rendering before creating the new swap -chain. It is possible to create a new swap chain while drawing commands on an -image from the old swap chain are still in-flight. You need to pass the previous -swap chain to the `oldSwapChain` field in the `VkSwapchainCreateInfoKHR` struct -and destroy the old swap chain as soon as you've finished using it. - -## Suboptimal or out-of-date swap chain - -Now we just need to figure out when swap chain recreation is necessary and call -our new `recreateSwapChain` function. Luckily, Vulkan will usually just tell us that the swap chain is no longer adequate during presentation. The `vkAcquireNextImageKHR` and -`vkQueuePresentKHR` functions can return the following special values to -indicate this. - -* `VK_ERROR_OUT_OF_DATE_KHR`: The swap chain has become incompatible with the -surface and can no longer be used for rendering. Usually happens after a window resize. -* `VK_SUBOPTIMAL_KHR`: The swap chain can still be used to successfully present -to the surface, but the surface properties are no longer matched exactly. - -```c++ -VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); - -if (result == VK_ERROR_OUT_OF_DATE_KHR) { - recreateSwapChain(); - return; -} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { - throw std::runtime_error("failed to acquire swap chain image!"); -} -``` +이것만으로도 스왑 체인을 재구성할 수 있습니다! 하지만 이 방법의 단점은 새로운 스왑 체인을 만들기 전에 모든 렌더링을 중단해야 한다는 것입니다. 이전 스왑 체인의 이미지에 대한 그리기 명령이 아직 실행 중인 상태에서 새로운 스왑 체인을 만드는 것도 가능합니다. 그러려면 `vk::SwapchainCreateInfoKHR` 구조체의 `old_swapchain` 필드에 이전 스왑 체인을 전달하고, 이전 스왑 체인 사용이 끝나는 즉시 파괴해야 합니다. -If the swap chain turns out to be out of date when attempting to acquire an -image, then it is no longer possible to present to it. Therefore we should -immediately recreate the swap chain and try again in the next `drawFrame` call. +## 준최적(Suboptimal) 또는 오래된(out-of-date) 스왑 체인 -You could also decide to do that if the swap chain is suboptimal, but I've -chosen to proceed anyway in that case because we've already acquired an image. -Both `VK_SUCCESS` and `VK_SUBOPTIMAL_KHR` are considered "success" return codes. +이제 스왑 체인 재구성이 언제 필요한지 파악하고 새로운 `recreate_swapchain` 함수를 호출하기만 하면 됩니다. 다행히도 Vulkan은 보통 프레젠테이션 중에 스왑 체인이 더 이상 적합하지 않다고 알려줍니다. Ash 라이브러리는 `acquire_next_image`와 `queue_present` 함수에서 `Result` 타입을 반환하여 이를 명확하게 처리합니다. -```c++ -result = vkQueuePresentKHR(presentQueue, &presentInfo); +* `Err(vk::Result::ERROR_OUT_OF_DATE_KHR)`: 스왑 체인이 서피스와 호환되지 않게 되어 더 이상 렌더링에 사용할 수 없습니다. 보통 윈도우 리사이즈 후에 발생합니다. +* `Ok((_image_index, true))`: `acquire_next_image`에서 반환되는 튜플의 두 번째 값이 `true`이면 스왑 체인이 준최적(suboptimal) 상태임을 의미합니다. `queue_present`에서는 `Ok(true)`가 준최적 상태를 나타냅니다. -if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { - recreateSwapChain(); -} else if (result != VK_SUCCESS) { - throw std::runtime_error("failed to present swap chain image!"); -} +```rust +let result = unsafe { + self.swapchain_loader.acquire_next_image( + self.swapchain, + u64::MAX, + self.image_available_semaphores[self.current_frame], + vk::Fence::null(), + ) +}; -currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +let image_index = match result { + Ok((image_index, _is_suboptimal)) => image_index, + Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => { + self.recreate_swapchain()?; + return Ok(()); // 다음 프레임에서 다시 시도 + } + Err(error) => return Err(error.into()), +}; ``` -The `vkQueuePresentKHR` function returns the same values with the same meaning. -In this case we will also recreate the swap chain if it is suboptimal, because -we want the best possible result. - -## Fixing a deadlock - -If we try to run the code now, it is possible to encounter a deadlock. -Debugging the code, we find that the application reaches `vkWaitForFences` but -never continues past it. This is because when `vkAcquireNextImageKHR` returns -`VK_ERROR_OUT_OF_DATE_KHR`, we recreate the swapchain and then return from -`drawFrame`. But before that happens, the current frame's fence was waited upon -and reset. Since we return immediately, no work is submitted for execution and -the fence will never be signaled, causing `vkWaitForFences` to halt forever. - -There is a simple fix thankfully. Delay resetting the fence until after we -know for sure we will be submitting work with it. Thus, if we return early, the -fence is still signaled and `vkWaitForFences` wont deadlock the next time we -use the same fence object. - -The beginning of `drawFrame` should now look like this: -```c++ -vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); - -uint32_t imageIndex; -VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); - -if (result == VK_ERROR_OUT_OF_DATE_KHR) { - recreateSwapChain(); - return; -} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { - throw std::runtime_error("failed to acquire swap chain image!"); -} - -// Only reset the fence if we are submitting work -vkResetFences(device, 1, &inFlightFences[currentFrame]); -``` +`acquire_next_image`가 `ERROR_OUT_OF_DATE_KHR` 오류를 반환하면 더 이상 현재 스왑 체인으로 프레젠테이션을 할 수 없습니다. 따라서 즉시 스왑 체인을 재구성하고 `draw_frame`을 종료하여 다음 루프에서 다시 시도해야 합니다. Ash의 `acquire_next_image`는 성공 시 `(u32, bool)` 튜플을 반환하는데, 두 번째 `bool` 값은 준최적 여부를 나타냅니다. 일단은 이미지를 성공적으로 획득했으므로 준최적 상태는 무시하고 진행합니다. -## Handling resizes explicitly +```rust +let present_info = vk::PresentInfoKHR::builder() + // ... + ; -Although many drivers and platforms trigger `VK_ERROR_OUT_OF_DATE_KHR` automatically after a window resize, it is not guaranteed to happen. That's why we'll add some extra code to also handle resizes explicitly. First add a new member variable that flags that a resize has happened: +let result = unsafe { self.swapchain_loader.queue_present(self.present_queue, &present_info) }; -```c++ -std::vector inFlightFences; +let is_resized = match result { + Ok(true) | Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => { + self.framebuffer_resized = false; + self.recreate_swapchain()?; + } + Err(e) => return Err(e.into()), + _ => {} +}; -bool framebufferResized = false; +self.current_frame = (self.current_frame + 1) % MAX_FRAMES_IN_FLIGHT; ``` -The `drawFrame` function should then be modified to also check for this flag: +`queue_present` 함수도 `Result`를 반환합니다. `Ok(true)`는 준최적 상태를, `Err(vk::Result::ERROR_OUT_OF_DATE_KHR)`는 오래된 상태를 의미합니다. 두 경우 모두 최상의 결과를 위해 스왑 체인을 재구성합니다. -```c++ -if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) { - framebufferResized = false; - recreateSwapChain(); -} else if (result != VK_SUCCESS) { - ... -} -``` +## 데드락 해결하기 + +지금 코드를 실행하면 데드락이 발생할 수 있습니다. 코드를 디버깅해보면, 애플리케이션이 `wait_for_fences`에 도달한 후 더 이상 진행하지 못하고 멈추는 것을 발견할 수 있습니다. 이는 `acquire_next_image`가 `ERROR_OUT_OF_DATE_KHR`를 반환할 때, 우리가 스왑 체인을 재구성한 후 `draw_frame`에서 즉시 반환하기 때문입니다. 하지만 그 전에, 현재 프레임의 펜스는 대기 상태에 들어간 후 리셋되었습니다. 우리가 즉시 반환하므로 아무 작업도 제출되지 않고, 따라서 펜스는 절대 신호를 받지 못하게 되어 `wait_for_fences`가 영원히 멈추게 됩니다. -It is important to do this after `vkQueuePresentKHR` to ensure that the semaphores are in a consistent state, otherwise a signaled semaphore may never be properly waited upon. Now to actually detect resizes we can use the `glfwSetFramebufferSizeCallback` function in the GLFW framework to set up a callback: +다행히 간단한 해결책이 있습니다. 펜스를 리셋하는 것을, 우리가 확실히 작업을 제출할 것이라는 것을 안 이후로 미루는 것입니다. 이렇게 하면, 우리가 일찍 반환하더라도 펜스는 여전히 신호를 받은 상태(signaled)로 남아있어, 다음에 같은 펜스 객체를 사용할 때 `wait_for_fences`가 데드락을 일으키지 않을 것입니다. -```c++ -void initWindow() { - glfwInit(); +이제 `draw_frame` 함수의 시작 부분은 다음과 같아야 합니다: +```rust +unsafe { + self.device.wait_for_fences( + &[self.in_flight_fences[self.current_frame]], + true, + u64::MAX, + )?; +} - glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); +let result = unsafe { + self.swapchain_loader.acquire_next_image( + self.swapchain, + u64::MAX, + self.image_available_semaphores[self.current_frame], + vk::Fence::null(), + ) +}; + +let image_index = match result { + Ok((image_index, _is_suboptimal)) => image_index, + Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => { + self.recreate_swapchain()?; + return Ok(()); // 다음 프레임에서 다시 시도 + } + Err(error) => return Err(error.into()), +}; - window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); - glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); +// 작업을 제출할 때만 펜스를 리셋합니다. +unsafe { + self.device + .reset_fences(&[self.in_flight_fences[self.current_frame]])?; } +``` +## 명시적으로 리사이즈 처리하기 -static void framebufferResizeCallback(GLFWwindow* window, int width, int height) { +많은 드라이버와 플랫폼이 윈도우 리사이즈 후 자동으로 `ERROR_OUT_OF_DATE_KHR`를 발생시키지만, 이것이 보장되지는 않습니다. 그래서 우리는 리사이즈를 명시적으로 처리하는 코드를 추가할 것입니다. 먼저 구조체에 리사이즈가 발생했음을 알리는 플래그 멤버 변수를 추가합니다: +```rust +struct HelloTriangleApplication { + // ... + in_flight_fences: Vec, + framebuffer_resized: bool, + // ... } ``` -The reason that we're creating a `static` function as a callback is because GLFW does not know how to properly call a member function with the right `this` pointer to our `HelloTriangleApplication` instance. +`draw_frame` 함수를 이 플래그도 확인하도록 수정해야 합니다. `queue_present` 후의 로직에 이 플래그를 함께 검사합니다. -However, we do get a reference to the `GLFWwindow` in the callback and there is another GLFW function that allows you to store an arbitrary pointer inside of it: `glfwSetWindowUserPointer`: +```rust +let present_result = unsafe { self.swapchain_loader.queue_present(self.present_queue, &present_info) }; -```c++ -window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); -glfwSetWindowUserPointer(window, this); -glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); -``` +let suboptimal = match present_result { + Ok(suboptimal) => suboptimal, + Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => true, // 오래된 경우도 리사이즈로 취급 + Err(e) => return Err(e.into()), +}; -This value can now be retrieved from within the callback with `glfwGetWindowUserPointer` to properly set the flag: +if suboptimal || self.framebuffer_resized { + self.framebuffer_resized = false; + self.recreate_swapchain()?; +} +``` -```c++ -static void framebufferResizeCallback(GLFWwindow* window, int width, int height) { - auto app = reinterpret_cast(glfwGetWindowUserPointer(window)); - app->framebufferResized = true; +이제 실제로 리사이즈를 감지하기 위해 `glfw` 크레이트를 사용하여 콜백을 설정할 수 있습니다. Rust에서 FFI(Foreign Function Interface)를 통해 C 라이브러리 콜백을 다루는 것은 `unsafe` 코드를 필요로 합니다. + +```rust +// init_window 함수 내에서 +// 'self'에 대한 포인터를 윈도우의 user data로 저장합니다. +self.window.set_user_data(self as *mut _ as *mut c_void); +self.window.set_framebuffer_size_callback(framebuffer_resize_callback); + +// ... + +// 애플리케이션 구조체 밖의 static 함수 +extern "C" fn framebuffer_resize_callback( + window: *mut glfw::ffi::GLFWwindow, + _width: i32, + _height: i32, +) { + unsafe { + let app_ptr = glfw::ffi::glfwGetWindowUserPointer(window) as *mut HelloTriangleApplication; + if !app_ptr.is_null() { + (*app_ptr).framebuffer_resized = true; + } + } } ``` -Now try to run the program and resize the window to see if the framebuffer is indeed resized properly with the window. +C++ 예제와 마찬가지로, `glfw`는 Rust의 멤버 함수를 직접 호출하는 방법을 모르기 때문에, `static` 함수(Rust에서는 `extern "C" fn`)를 사용합니다. `set_user_data`를 통해 `self`의 포인터를 윈도우에 저장하고, 콜백 함수 내에서 `glfwGetWindowUserPointer`로 다시 가져와 애플리케이션의 `framebuffer_resized` 플래그를 설정합니다. 이 과정은 `unsafe` 블록 안에서 수행되어야 합니다. + +이제 프로그램을 실행하고 윈도우 크기를 조절하여 프레임버퍼가 윈도우에 맞게 올바르게 리사이즈되는지 확인해 보세요. -## Handling minimization +## 창 최소화 처리하기 -There is another case where a swap chain may become out of date and that is a special kind of window resizing: window minimization. This case is special because it will result in a frame buffer size of `0`. In this tutorial we will handle that by pausing until the window is in the foreground again by extending the `recreateSwapChain` function: +스왑 체인이 오래될 수 있는 또 다른 경우는 특별한 종류의 윈도우 리사이즈인 창 최소화입니다. 이 경우는 프레임버퍼 크기가 `(0, 0)`이 되기 때문에 특별합니다. 이 튜토리얼에서는 윈도우가 다시 전경에 올 때까지 일시 중지하는 방식으로 이 문제를 처리할 것입니다. `recreate_swapchain` 함수를 다음과 같이 확장합니다: -```c++ -void recreateSwapChain() { - int width = 0, height = 0; - glfwGetFramebufferSize(window, &width, &height); - while (width == 0 || height == 0) { - glfwGetFramebufferSize(window, &width, &height); - glfwWaitEvents(); +```rust +fn recreate_swapchain(&mut self) -> Result<(), Box> { + let (mut width, mut height) = self.window.get_framebuffer_size(); + while width == 0 || height == 0 { + (width, height) = self.window.get_framebuffer_size(); + self.glfw.wait_events(); } - vkDeviceWaitIdle(device); + unsafe { + self.device.device_wait_idle()?; + } + + self.cleanup_swapchain(); + self.create_swapchain()?; + self.create_image_views()?; + self.create_framebuffers()?; - ... + Ok(()) } ``` +초기 `get_framebuffer_size` 호출은 이미 크기가 올바르고 `wait_events`가 기다릴 것이 없는 경우를 처리합니다. -The initial call to `glfwGetFramebufferSize` handles the case where the size is already correct and `glfwWaitEvents` would have nothing to wait on. - -Congratulations, you've now finished your very first well-behaved Vulkan -program! In the next chapter we're going to get rid of the hardcoded vertices in -the vertex shader and actually use a vertex buffer. +축하합니다, 여러분은 이제 최초의 잘 동작하는(well-behaved) Rust-Vulkan 프로그램을 완성했습니다! 다음 장에서는 버텍스 셰이더에 하드코딩된 정점들을 제거하고 실제로 정점 버퍼(vertex buffer)를 사용할 것입니다. -[C++ code](/code/17_swap_chain_recreation.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +[Rust 코드](https://github.com/vulkan-tutorial-rs/vulkan-tutorial-rs-code-new/blob/main/src/17_swap_chain_recreation.rs) / +[버텍스 셰이더](/code/09_shader_base.vert) / +[프래그먼트 셰이더](/code/09_shader_base.frag) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/00_Setup/00_Base_code.md b/ko/03_Drawing_a_triangle/00_Setup/00_Base_code.md index df26c6ac..6ccf987b 100644 --- a/ko/03_Drawing_a_triangle/00_Setup/00_Base_code.md +++ b/ko/03_Drawing_a_triangle/00_Setup/00_Base_code.md @@ -1,8 +1,6 @@ -## General structure +## 일반적인 구조 -In the previous chapter you've created a Vulkan project with all of the proper -configuration and tested it with the sample code. In this chapter we're starting -from scratch with the following code: +이전 장에서 여러분은 모든 설정을 마친 Vulkan 프로젝트를 만들고 예제 코드로 테스트했습니다. 이번 장에서는 다음 코드를 가지고 처음부터 시작합니다. ```c++ #include @@ -47,73 +45,32 @@ int main() { } ``` -We first include the Vulkan header from the LunarG SDK, which provides the -functions, structures and enumerations. The `stdexcept` and `iostream` headers -are included for reporting and propagating errors. The `cstdlib` -header provides the `EXIT_SUCCESS` and `EXIT_FAILURE` macros. - -The program itself is wrapped into a class where we'll store the Vulkan objects -as private class members and add functions to initiate each of them, which will -be called from the `initVulkan` function. Once everything has been prepared, we -enter the main loop to start rendering frames. We'll fill in the `mainLoop` -function to include a loop that iterates until the window is closed in a moment. -Once the window is closed and `mainLoop` returns, we'll make sure to deallocate -the resources we've used in the `cleanup` function. - -If any kind of fatal error occurs during execution then we'll throw a -`std::runtime_error` exception with a descriptive message, which will propagate -back to the `main` function and be printed to the command prompt. To handle -a variety of standard exception types as well, we catch the more general `std::exception`. One example of an error that we will deal with soon is finding -out that a certain required extension is not supported. - -Roughly every chapter that follows after this one will add one new function that -will be called from `initVulkan` and one or more new Vulkan objects to the -private class members that need to be freed at the end in `cleanup`. - -## Resource management - -Just like each chunk of memory allocated with `malloc` requires a call to -`free`, every Vulkan object that we create needs to be explicitly destroyed when -we no longer need it. In C++ it is possible to perform automatic resource -management using [RAII](https://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization) -or smart pointers provided in the `` header. However, I've chosen to be -explicit about allocation and deallocation of Vulkan objects in this tutorial. -After all, Vulkan's niche is to be explicit about every operation to avoid -mistakes, so it's good to be explicit about the lifetime of objects to learn how -the API works. - -After following this tutorial, you could implement automatic resource management -by writing C++ classes that acquire Vulkan objects in their constructor and -release them in their destructor, or by providing a custom deleter to either -`std::unique_ptr` or `std::shared_ptr`, depending on your ownership requirements. -RAII is the recommended model for larger Vulkan programs, but -for learning purposes it's always good to know what's going on behind the -scenes. - -Vulkan objects are either created directly with functions like `vkCreateXXX`, or -allocated through another object with functions like `vkAllocateXXX`. After -making sure that an object is no longer used anywhere, you need to destroy it -with the counterparts `vkDestroyXXX` and `vkFreeXXX`. The parameters for these -functions generally vary for different types of objects, but there is one -parameter that they all share: `pAllocator`. This is an optional parameter that -allows you to specify callbacks for a custom memory allocator. We will ignore -this parameter in the tutorial and always pass `nullptr` as argument. - -## Integrating GLFW - -Vulkan works perfectly fine without creating a window if you want to use it for -off-screen rendering, but it's a lot more exciting to actually show something! -First replace the `#include ` line with +먼저 LunarG SDK의 Vulkan 헤더를 포함하여 함수, 구조체, 열거형을 가져옵니다. `stdexcept`와 `iostream` 헤더는 오류를 보고하고 전파하는 데 사용됩니다. `cstdlib` 헤더는 `EXIT_SUCCESS`와 `EXIT_FAILURE` 매크로를 제공합니다. + +프로그램 자체는 클래스로 감싸져 있습니다. Vulkan 객체들을 비공개 클래스 멤버로 저장하고, 각 객체를 초기화하는 함수들을 추가하여 `initVulkan` 함수에서 호출할 것입니다. 모든 준비가 끝나면 메인 루프에 진입하여 프레임 렌더링을 시작합니다. 잠시 후에 창이 닫힐 때까지 반복하는 루프를 `mainLoop` 함수에 채워 넣을 것입니다. 창이 닫히고 `mainLoop`가 반환되면, `cleanup` 함수에서 사용했던 리소스들을 반드시 할당 해제할 것입니다. + +실행 중 치명적인 오류가 발생하면, 설명적인 메시지와 함께 `std::runtime_error` 예외를 던질 것입니다. 이 예외는 `main` 함수로 전파되어 명령 프롬프트에 출력됩니다. 다양한 표준 예외 타입을 처리하기 위해 더 일반적인 `std::exception`을 잡습니다. 곧 다룰 오류의 한 예는 특정 필수 익스텐션이 지원되지 않는다는 것을 발견하는 경우입니다. + +이 장 이후의 거의 모든 장에서는 `initVulkan`에서 호출될 새로운 함수 하나와, `cleanup`에서 마지막에 해제해야 할 하나 이상의 새로운 Vulkan 객체를 비공개 클래스 멤버에 추가할 것입니다. + +## 리소스 관리 + +`malloc`으로 할당된 모든 메모리 덩어리에 `free` 호출이 필요한 것처럼, 우리가 생성하는 모든 Vulkan 객체는 더 이상 필요하지 않을 때 명시적으로 파괴되어야 합니다. C++에서는 [RAII(Resource Acquisition Is Initialization)](https://ko.wikipedia.org/wiki/RAII)나 `` 헤더에 제공된 스마트 포인터를 사용하여 자동 리소스 관리를 수행할 수 있습니다. 하지만, 저는 이 튜토리얼에서 Vulkan 객체의 할당과 해제를 명시적으로 다루기로 했습니다. 결국 Vulkan의 장점은 실수를 피하기 위해 모든 작업을 명시적으로 하는 것이므로, API가 어떻게 작동하는지 배우기 위해 객체의 수명 주기를 명시적으로 다루는 것이 좋습니다. + +이 튜토리얼을 마친 후에는 생성자에서 Vulkan 객체를 획득하고 소멸자에서 해제하는 C++ 클래스를 작성하거나, 소유권 요구사항에 따라 `std::unique_ptr` 또는 `std::shared_ptr`에 사용자 정의 삭제자(deleter)를 제공하여 자동 리소스 관리를 구현할 수 있습니다. RAII는 더 큰 Vulkan 프로그램에 권장되는 모델이지만, 학습 목적상 내부적으로 어떤 일이 일어나는지 아는 것은 항상 좋습니다. + +Vulkan 객체는 `vkCreateXXX`와 같은 함수로 직접 생성되거나, `vkAllocateXXX`와 같은 함수를 통해 다른 객체를 통해 할당됩니다. 객체가 더 이상 어디에서도 사용되지 않는 것을 확인한 후에는, 그에 상응하는 `vkDestroyXXX`와 `vkFreeXXX`로 파괴해야 합니다. 이 함수들의 매개변수는 일반적으로 객체 유형에 따라 다르지만, 모두가 공유하는 하나의 매개변수가 있습니다: `pAllocator`. 이것은 사용자 정의 메모리 할당자를 위한 콜백을 지정할 수 있는 선택적 매개변수입니다. 이 튜토리얼에서는 이 매개변수를 무시하고 항상 인자로 `nullptr`을 전달할 것입니다. + +## GLFW 통합 + +Vulkan은 오프스크린 렌더링에 사용하려는 경우 창을 생성하지 않고도 완벽하게 작동하지만, 실제로 무언가를 보여주는 것이 훨씬 더 흥미롭습니다! 먼저 `#include ` 라인을 다음으로 교체하세요. ```c++ #define GLFW_INCLUDE_VULKAN #include ``` -That way GLFW will include its own definitions and automatically load the Vulkan -header with it. Add a `initWindow` function and add a call to it from the `run` -function before the other calls. We'll use that function to initialize GLFW and -create a window. +이렇게 하면 GLFW가 자체 정의를 포함하고 Vulkan 헤더를 자동으로 함께 로드합니다. `initWindow` 함수를 추가하고 `run` 함수에서 다른 호출들보다 먼저 호출하도록 추가하세요. 이 함수를 사용하여 GLFW를 초기화하고 창을 생성할 것입니다. ```c++ void run() { @@ -129,49 +86,40 @@ private: } ``` -The very first call in `initWindow` should be `glfwInit()`, which initializes -the GLFW library. Because GLFW was originally designed to create an OpenGL -context, we need to tell it to not create an OpenGL context with a subsequent -call: +`initWindow`의 가장 첫 번째 호출은 GLFW 라이브러리를 초기화하는 `glfwInit()`이어야 합니다. GLFW는 원래 OpenGL 컨텍스트를 생성하도록 설계되었기 때문에, 다음 호출을 통해 OpenGL 컨텍스트를 생성하지 않도록 알려줘야 합니다. ```c++ glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); ``` -Because handling resized windows takes special care that we'll look into later, -disable it for now with another window hint call: +크기가 조절된 창을 처리하는 것은 나중에 다룰 특별한 주의가 필요하기 때문에, 지금은 다른 윈도우 힌트 호출로 비활성화합니다. ```c++ glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); ``` -All that's left now is creating the actual window. Add a `GLFWwindow* window;` -private class member to store a reference to it and initialize the window with: +이제 남은 것은 실제 창을 만드는 것뿐입니다. 창에 대한 참조를 저장할 `GLFWwindow* window;` 비공개 클래스 멤버를 추가하고 다음 코드로 창을 초기화하세요. ```c++ window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr); ``` -The first three parameters specify the width, height and title of the window. -The fourth parameter allows you to optionally specify a monitor to open the -window on and the last parameter is only relevant to OpenGL. +처음 세 매개변수는 창의 너비, 높이, 제목을 지정합니다. 네 번째 매개변수는 창을 열 모니터를 선택적으로 지정할 수 있게 하고, 마지막 매개변수는 OpenGL에만 관련이 있습니다. -It's a good idea to use constants instead of hardcoded width and height numbers -because we'll be referring to these values a couple of times in the future. I've -added the following lines above the `HelloTriangleApplication` class definition: +너비와 높이를 하드코딩된 숫자 대신 상수로 사용하는 것이 좋습니다. 앞으로 이 값들을 여러 번 참조할 것이기 때문입니다. 저는 `HelloTriangleApplication` 클래스 정의 위에 다음 줄을 추가했습니다. ```c++ const uint32_t WIDTH = 800; const uint32_t HEIGHT = 600; ``` -and replaced the window creation call with +그리고 창 생성 호출을 다음과 같이 바꿨습니다. ```c++ window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); ``` -You should now have a `initWindow` function that looks like this: +이제 `initWindow` 함수는 다음과 같아야 합니다. ```c++ void initWindow() { @@ -184,8 +132,7 @@ void initWindow() { } ``` -To keep the application running until either an error occurs or the window is -closed, we need to add an event loop to the `mainLoop` function as follows: +오류가 발생하거나 창이 닫힐 때까지 애플리케이션을 계속 실행하려면, `mainLoop` 함수에 다음과 같이 이벤트 루프를 추가해야 합니다. ```c++ void mainLoop() { @@ -195,12 +142,9 @@ void mainLoop() { } ``` -This code should be fairly self-explanatory. It loops and checks for events like -pressing the X button until the window has been closed by the user. This is also -the loop where we'll later call a function to render a single frame. +이 코드는 상당히 자명해야 합니다. 사용자가 창을 닫을 때까지 X 버튼 누르기와 같은 이벤트를 확인하며 반복합니다. 이곳은 나중에 단일 프레임을 렌더링하는 함수를 호출할 루프이기도 합니다. -Once the window is closed, we need to clean up resources by destroying it and -terminating GLFW itself. This will be our first `cleanup` code: +창이 닫히면, 창을 파괴하고 GLFW 자체를 종료하여 리소스를 정리해야 합니다. 이것이 우리의 첫 번째 `cleanup` 코드가 될 것입니다. ```c++ void cleanup() { @@ -210,8 +154,6 @@ void cleanup() { } ``` -When you run the program now you should see a window titled `Vulkan` show up -until the application is terminated by closing the window. Now that we have the -skeleton for the Vulkan application, let's [create the first Vulkan object](!en/Drawing_a_triangle/Setup/Instance)! +이제 프로그램을 실행하면 "Vulkan"이라는 제목의 창이 나타나고, 창을 닫아 애플리케이션이 종료될 때까지 유지됩니다. 이제 Vulkan 애플리케이션의 골격을 갖추었으니, [첫 번째 Vulkan 객체 생성하기](!ko/Drawing_a_triangle/Setup/Instance)로 넘어갑시다! -[C++ code](/code/00_base_code.cpp) +[C++ 코드](/code/00_base_code.cpp) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/00_Setup/01_Instance.md b/ko/03_Drawing_a_triangle/00_Setup/01_Instance.md index d9744a1c..48042682 100644 --- a/ko/03_Drawing_a_triangle/00_Setup/01_Instance.md +++ b/ko/03_Drawing_a_triangle/00_Setup/01_Instance.md @@ -1,12 +1,8 @@ -## Creating an instance +## 인스턴스 생성 -The very first thing you need to do is initialize the Vulkan library by creating -an *instance*. The instance is the connection between your application and the -Vulkan library and creating it involves specifying some details about your -application to the driver. +가장 먼저 해야 할 일은 *인스턴스(instance)*를 생성하여 Vulkan 라이브러리를 초기화하는 것입니다. 인스턴스는 애플리케이션과 Vulkan 라이브러리 간의 연결고리이며, 인스턴스를 생성하는 과정에는 애플리케이션에 대한 몇 가지 세부 정보를 드라이버에 지정하는 작업이 포함됩니다. -Start by adding a `createInstance` function and invoking it in the -`initVulkan` function. +먼저 `createInstance` 함수를 추가하고 `initVulkan` 함수에서 호출합니다. ```c++ void initVulkan() { @@ -14,18 +10,14 @@ void initVulkan() { } ``` -Additionally add a data member to hold the handle to the instance: +또한 인스턴스 핸들을 저장할 데이터 멤버를 추가합니다. ```c++ private: VkInstance instance; ``` -Now, to create an instance we'll first have to fill in a struct with some -information about our application. This data is technically optional, but it may -provide some useful information to the driver in order to optimize our specific -application (e.g. because it uses a well-known graphics engine with -certain special behavior). This struct is called `VkApplicationInfo`: +이제 인스턴스를 생성하기 위해 먼저 애플리케이션에 대한 정보가 담긴 구조체를 채워야 합니다. 이 데이터는 기술적으로는 선택 사항이지만, 드라이버가 우리의 특정 애플리케이션을 최적화하는 데 유용한 정보를 제공할 수 있습니다(예: 특정 특수 동작을 하는 잘 알려진 그래픽 엔진을 사용하는 경우). 이 구조체는 `VkApplicationInfo`라고 합니다. ```c++ void createInstance() { @@ -39,17 +31,9 @@ void createInstance() { } ``` -As mentioned before, many structs in Vulkan require you to explicitly specify -the type in the `sType` member. This is also one of the many structs with a -`pNext` member that can point to extension information in the future. We're -using value initialization here to leave it as `nullptr`. +앞서 언급했듯이, Vulkan의 많은 구조체는 `sType` 멤버에 명시적으로 타입을 지정해야 합니다. 이 구조체는 또한 나중에 확장 정보를 가리킬 수 있는 `pNext` 멤버를 가진 많은 구조체 중 하나입니다. 여기서는 값 초기화(value initialization)를 사용하여 `nullptr`로 남겨둡니다. -A lot of information in Vulkan is passed through structs instead of function -parameters and we'll have to fill in one more struct to provide sufficient -information for creating an instance. This next struct is not optional and tells -the Vulkan driver which global extensions and validation layers we want to use. -Global here means that they apply to the entire program and not a specific -device, which will become clear in the next few chapters. +Vulkan에서는 많은 정보가 함수 매개변수 대신 구조체를 통해 전달되며, 인스턴스 생성을 위한 충분한 정보를 제공하기 위해 또 다른 구조체를 채워야 합니다. 이 다음 구조체는 선택 사항이 아니며, 우리가 사용하려는 전역(global) 확장과 유효성 검사 레이어를 Vulkan 드라이버에 알려줍니다. 여기서 전역이라는 의미는 특정 장치가 아닌 프로그램 전체에 적용된다는 뜻이며, 이는 다음 몇 장에서 명확해질 것입니다. ```c++ VkInstanceCreateInfo createInfo{}; @@ -57,11 +41,7 @@ createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; createInfo.pApplicationInfo = &appInfo; ``` -The first two parameters are straightforward. The next two layers specify the -desired global extensions. As mentioned in the overview chapter, Vulkan is a -platform agnostic API, which means that you need an extension to interface with -the window system. GLFW has a handy built-in function that returns the -extension(s) it needs to do that which we can pass to the struct: +처음 두 매개변수는 간단합니다. 다음 두 멤버는 원하는 전역 확장을 지정합니다. 개요 장에서 언급했듯이, Vulkan은 플랫폼에 구애받지 않는(platform agnostic) API이므로, 창 시스템과 상호작용하려면 확장이 필요합니다. GLFW에는 이를 위해 필요한 확장을 반환하는 편리한 내장 함수가 있으며, 이 함수가 반환하는 값을 구조체에 전달할 수 있습니다. ```c++ uint32_t glfwExtensionCount = 0; @@ -73,33 +53,25 @@ createInfo.enabledExtensionCount = glfwExtensionCount; createInfo.ppEnabledExtensionNames = glfwExtensions; ``` -The last two members of the struct determine the global validation layers to -enable. We'll talk about these more in-depth in the next chapter, so just leave -these empty for now. +구조체의 마지막 두 멤버는 활성화할 전역 유효성 검사 레이어를 결정합니다. 이에 대해서는 다음 장에서 더 자세히 다룰 것이므로 지금은 비워둡니다. ```c++ createInfo.enabledLayerCount = 0; ``` -We've now specified everything Vulkan needs to create an instance and we can -finally issue the `vkCreateInstance` call: +이제 Vulkan이 인스턴스를 생성하는 데 필요한 모든 것을 지정했으므로, 마침내 `vkCreateInstance` 호출을 실행할 수 있습니다. ```c++ VkResult result = vkCreateInstance(&createInfo, nullptr, &instance); ``` -As you'll see, the general pattern that object creation function parameters in -Vulkan follow is: +보시다시피, Vulkan에서 객체 생성 함수의 매개변수는 일반적으로 다음과 같은 패턴을 따릅니다: -* Pointer to struct with creation info -* Pointer to custom allocator callbacks, always `nullptr` in this tutorial -* Pointer to the variable that stores the handle to the new object +* 생성 정보가 담긴 구조체에 대한 포인터 +* 사용자 정의 할당자 콜백에 대한 포인터, 이 튜토리얼에서는 항상 `nullptr` +* 새 객체의 핸들을 저장할 변수에 대한 포인터 -If everything went well then the handle to the instance was stored in the -`VkInstance` class member. Nearly all Vulkan functions return a value of type -`VkResult` that is either `VK_SUCCESS` or an error code. To check if the -instance was created successfully, we don't need to store the result and can -just use a check for the success value instead: +모든 것이 순조롭게 진행되었다면 인스턴스 핸들이 `VkInstance` 클래스 멤버에 저장되었을 것입니다. 거의 모든 Vulkan 함수는 `VK_SUCCESS` 또는 오류 코드인 `VkResult` 타입의 값을 반환합니다. 인스턴스가 성공적으로 생성되었는지 확인하기 위해 결과를 저장할 필요 없이 성공 값에 대한 검사만 사용하면 됩니다. ```c++ if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { @@ -107,18 +79,14 @@ if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { } ``` -Now run the program to make sure that the instance is created successfully. +이제 프로그램을 실행하여 인스턴스가 성공적으로 생성되는지 확인하세요. -## Encountered VK_ERROR_INCOMPATIBLE_DRIVER: -If using MacOS with the latest MoltenVK sdk, you may get `VK_ERROR_INCOMPATIBLE_DRIVER` -returned from `vkCreateInstance`. According to the [Getting Start Notes](https://vulkan.lunarg.com/doc/sdk/1.3.216.0/mac/getting_started.html). Beginning with the 1.3.216 Vulkan SDK, the `VK_KHR_PORTABILITY_subset` -extension is mandatory. +## VK_ERROR_INCOMPATIBLE_DRIVER 오류 발생 시: +최신 MoltenVK SDK를 사용하는 macOS에서 `vkCreateInstance`가 `VK_ERROR_INCOMPATIBLE_DRIVER`를 반환할 수 있습니다. [시작하기 노트](https://vulkan.lunarg.com/doc/sdk/1.3.216.0/mac/getting_started.html)에 따르면, 1.3.216 Vulkan SDK부터 `VK_KHR_PORTABILITY_subset` 확장이 필수로 요구됩니다. -To get over this error, first add the `VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR` bit -to `VkInstanceCreateInfo` struct's flags, then add `VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME` -to instance enabled extension list. +이 오류를 해결하려면, 먼저 `VkInstanceCreateInfo` 구조체의 `flags`에 `VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR` 비트를 추가한 다음, 인스턴스 활성화 확장 목록에 `VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME`을 추가해야 합니다. -Typically the code could be like this: +일반적으로 코드는 다음과 같습니다. ```c++ ... @@ -140,45 +108,32 @@ if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { } ``` -## Checking for extension support +## 확장 지원 여부 확인 -If you look at the `vkCreateInstance` documentation then you'll see that one of -the possible error codes is `VK_ERROR_EXTENSION_NOT_PRESENT`. We could simply -specify the extensions we require and terminate if that error code comes back. -That makes sense for essential extensions like the window system interface, but -what if we want to check for optional functionality? +`vkCreateInstance` 문서를 보면 가능한 오류 코드 중 하나가 `VK_ERROR_EXTENSION_NOT_PRESENT`임을 알 수 있습니다. 우리는 단순히 필요한 확장을 지정하고 해당 오류 코드가 반환되면 프로그램을 종료할 수 있습니다. 이는 창 시스템 인터페이스와 같은 필수적인 확장에는 합리적이지만, 선택적 기능에 대한 지원 여부를 확인하고 싶다면 어떻게 해야 할까요? -To retrieve a list of supported extensions before creating an instance, there's -the `vkEnumerateInstanceExtensionProperties` function. It takes a pointer to a -variable that stores the number of extensions and an array of -`VkExtensionProperties` to store details of the extensions. It also takes an -optional first parameter that allows us to filter extensions by a specific -validation layer, which we'll ignore for now. +인스턴스를 생성하기 전에 지원되는 확장 목록을 가져오려면 `vkEnumerateInstanceExtensionProperties` 함수가 있습니다. 이 함수는 확장의 수를 저장할 변수에 대한 포인터와 확장의 세부 정보를 저장할 `VkExtensionProperties` 배열을 매개변수로 받습니다. 또한 특정 유효성 검사 레이어로 확장을 필터링할 수 있는 선택적 첫 번째 매개변수가 있지만, 지금은 무시하겠습니다. -To allocate an array to hold the extension details we first need to know how -many there are. You can request just the number of extensions by leaving the -latter parameter empty: +확장 세부 정보를 담을 배열을 할당하려면 먼저 몇 개가 있는지 알아야 합니다. 뒤쪽 매개변수를 비워두면 확장의 수만 요청할 수 있습니다. ```c++ uint32_t extensionCount = 0; vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); ``` -Now allocate an array to hold the extension details (`include `): +이제 확장 세부 정보를 담을 배열을 할당합니다(`#include ` 필요). ```c++ std::vector extensions(extensionCount); ``` -Finally we can query the extension details: +마지막으로 확장 세부 정보를 쿼리할 수 있습니다. ```c++ vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data()); ``` -Each `VkExtensionProperties` struct contains the name and version of an -extension. We can list them with a simple for loop (`\t` is a tab for -indentation): +각 `VkExtensionProperties` 구조체에는 확장의 이름과 버전이 포함됩니다. 간단한 for 루프로 목록을 출력할 수 있습니다(`\t`는 들여쓰기를 위한 탭입니다). ```c++ std::cout << "available extensions:\n"; @@ -188,16 +143,11 @@ for (const auto& extension : extensions) { } ``` -You can add this code to the `createInstance` function if you'd like to provide -some details about the Vulkan support. As a challenge, try to create a function -that checks if all of the extensions returned by -`glfwGetRequiredInstanceExtensions` are included in the supported extensions -list. +Vulkan 지원에 대한 세부 정보를 제공하고 싶다면 이 코드를 `createInstance` 함수에 추가할 수 있습니다. 도전 과제로, `glfwGetRequiredInstanceExtensions`가 반환한 모든 확장이 지원되는 확장 목록에 포함되어 있는지 확인하는 함수를 만들어 보세요. -## Cleaning up +## 정리 -The `VkInstance` should be only destroyed right before the program exits. It can -be destroyed in `cleanup` with the `vkDestroyInstance` function: +`VkInstance`는 프로그램이 종료되기 직전에만 파괴되어야 합니다. `cleanup` 함수에서 `vkDestroyInstance` 함수를 사용하여 파괴할 수 있습니다. ```c++ void cleanup() { @@ -209,13 +159,8 @@ void cleanup() { } ``` -The parameters for the `vkDestroyInstance` function are straightforward. As -mentioned in the previous chapter, the allocation and deallocation functions -in Vulkan have an optional allocator callback that we'll ignore by passing -`nullptr` to it. All of the other Vulkan resources that we'll create in the -following chapters should be cleaned up before the instance is destroyed. +`vkDestroyInstance` 함수의 매개변수는 간단합니다. 이전 장에서 언급했듯이, Vulkan의 할당 및 해제 함수에는 선택적인 할당자 콜백이 있는데, 우리는 `nullptr`를 전달하여 이를 무시할 것입니다. 다음 장에서 생성할 다른 모든 Vulkan 리소스는 인스턴스가 파괴되기 전에 정리해야 합니다. -Before continuing with the more complex steps after instance creation, it's time -to evaluate our debugging options by checking out [validation layers](!en/Drawing_a_triangle/Setup/Validation_layers). +인스턴스 생성 후의 더 복잡한 단계로 넘어가기 전에, [유효성 검사 레이어](!ko/Drawing_a_triangle/Setup/Validation_layers)를 살펴봄으로써 디버깅 옵션을 평가해 볼 시간입니다. -[C++ code](/code/01_instance_creation.cpp) +[C++ 코드](/code/01_instance_creation.cpp) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md b/ko/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md index 569a0178..14632969 100644 --- a/ko/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md +++ b/ko/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md @@ -1,27 +1,16 @@ -## What are validation layers? - -The Vulkan API is designed around the idea of minimal driver overhead and one of -the manifestations of that goal is that there is very limited error checking in -the API by default. Even mistakes as simple as setting enumerations to incorrect -values or passing null pointers to required parameters are generally not -explicitly handled and will simply result in crashes or undefined behavior. -Because Vulkan requires you to be very explicit about everything you're doing, -it's easy to make many small mistakes like using a new GPU feature and -forgetting to request it at logical device creation time. - -However, that doesn't mean that these checks can't be added to the API. Vulkan -introduces an elegant system for this known as *validation layers*. Validation -layers are optional components that hook into Vulkan function calls to apply -additional operations. Common operations in validation layers are: - -* Checking the values of parameters against the specification to detect misuse -* Tracking creation and destruction of objects to find resource leaks -* Checking thread safety by tracking the threads that calls originate from -* Logging every call and its parameters to the standard output -* Tracing Vulkan calls for profiling and replaying - -Here's an example of what the implementation of a function in a diagnostics -validation layer could look like: +## 밸리데이션 레이어란 무엇인가? + +Vulkan API는 최소한의 드라이버 오버헤드를 목표로 설계되었으며, 이 목표가 드러나는 부분 중 하나는 API에 기본적으로 내장된 오류 검사가 매우 제한적이라는 점입니다. 열거형(enum) 값을 잘못 설정하거나 필수 파라미터에 null 포인터를 전달하는 것과 같은 간단한 실수조차도 일반적으로 명시적으로 처리되지 않으며, 크래시나 정의되지 않은 동작(undefined behavior)으로 이어질 뿐입니다. Vulkan은 개발자가 수행하는 모든 작업을 매우 명시적으로 지정해야 하므로, 논리 장치(logical device)를 생성할 때 새로운 GPU 기능을 사용하면서 해당 기능 사용을 요청하는 것을 잊는 등 많은 사소한 실수를 하기 쉽습니다. + +하지만 이러한 검사를 API에 추가할 수 없다는 의미는 아닙니다. Vulkan은 이를 위해 *밸리데이션 레이어*라는 멋진 시스템을 도입했습니다. 밸리데이션 레이어는 Vulkan 함수 호출에 끼어들어(hook into) 추가적인 작업을 적용하는 선택적 컴포넌트입니다. 밸리데이션 레이어의 일반적인 작업은 다음과 같습니다. + +* 사양에 명시된 값과 파라미터 값을 비교하여 오용을 감지 +* 객체의 생성 및 소멸을 추적하여 리소스 누수(resource leak)를 발견 +* 호출이 발생한 스레드를 추적하여 스레드 안전성(thread safety)을 검사 +* 모든 호출과 그 파라미터를 표준 출력으로 로깅 +* 프로파일링 및 재현(replaying)을 위해 Vulkan 호출을 추적 + +진단용 밸리데이션 레이어에서 함수가 어떻게 구현될 수 있는지 보여주는 예시는 다음과 같습니다. ```c++ VkResult vkCreateInstance( @@ -30,7 +19,7 @@ VkResult vkCreateInstance( VkInstance* instance) { if (pCreateInfo == nullptr || instance == nullptr) { - log("Null pointer passed to required parameter!"); + log("필수 파라미터에 null 포인터가 전달되었습니다!"); return VK_ERROR_INITIALIZATION_FAILED; } @@ -38,42 +27,19 @@ VkResult vkCreateInstance( } ``` -These validation layers can be freely stacked to include all the debugging -functionality that you're interested in. You can simply enable validation layers -for debug builds and completely disable them for release builds, which gives you -the best of both worlds! - -Vulkan does not come with any validation layers built-in, but the LunarG Vulkan -SDK provides a nice set of layers that check for common errors. They're also -completely [open source](https://github.com/KhronosGroup/Vulkan-ValidationLayers), -so you can check which kind of mistakes they check for and contribute. Using the -validation layers is the best way to avoid your application breaking on -different drivers by accidentally relying on undefined behavior. - -Validation layers can only be used if they have been installed onto the system. -For example, the LunarG validation layers are only available on PCs with the -Vulkan SDK installed. - -There were formerly two different types of validation layers in Vulkan: instance -and device specific. The idea was that instance layers would only check -calls related to global Vulkan objects like instances, and device specific layers -would only check calls related to a specific GPU. Device specific layers have now been -deprecated, which means that instance validation layers apply to all Vulkan -calls. The specification document still recommends that you enable validation -layers at device level as well for compatibility, which is required by some -implementations. We'll simply specify the same layers as the instance at logical -device level, which we'll see [later on](!en/Drawing_a_triangle/Setup/Logical_device_and_queues). - -## Using validation layers - -In this section we'll see how to enable the standard diagnostics layers provided -by the Vulkan SDK. Just like extensions, validation layers need to be enabled by -specifying their name. All of the useful standard validation is bundled into a layer included in the SDK that is known as `VK_LAYER_KHRONOS_validation`. - -Let's first add two configuration variables to the program to specify the layers -to enable and whether to enable them or not. I've chosen to base that value on -whether the program is being compiled in debug mode or not. The `NDEBUG` macro -is part of the C++ standard and means "not debug". +이러한 밸리데이션 레이어들은 원하는 모든 디버깅 기능을 포함하도록 자유롭게 쌓아서(stacked) 사용할 수 있습니다. 디버그 빌드에서는 밸리데이션 레이어를 활성화하고 릴리즈 빌드에서는 완전히 비활성화하면, 두 가지 장점을 모두 누릴 수 있습니다! + +Vulkan은 내장된 밸리데이션 레이어를 제공하지 않지만, LunarG Vulkan SDK는 일반적인 오류를 검사하는 훌륭한 레이어 세트를 제공합니다. 이 레이어들은 완전히 [오픈 소스](https://github.com/KhronosGroup/Vulkan-ValidationLayers)이므로, 어떤 종류의 실수를 검사하는지 확인하고 기여할 수도 있습니다. 밸리데이션 레이어를 사용하는 것은 실수로 정의되지 않은 동작에 의존하여 애플리케이션이 다른 드라이버에서 깨지는 것을 방지하는 가장 좋은 방법입니다. + +밸리데이션 레이어는 시스템에 설치된 경우에만 사용할 수 있습니다. 예를 들어, LunarG 밸리데이션 레이어는 Vulkan SDK가 설치된 PC에서만 사용할 수 있습니다. + +이전에는 Vulkan에 인스턴스(instance)와 장치(device) 한정이라는 두 가지 유형의 밸리데이션 레이어가 있었습니다. 인스턴스 레이어는 인스턴스와 같은 전역 Vulkan 객체와 관련된 호출만 확인하고, 장치 한정 레이어는 특정 GPU와 관련된 호출만 확인한다는 개념이었습니다. 장치 한정 레이어는 현재 사용이 중단(deprecated)되었으며, 이는 인스턴스 밸리데이션 레이어가 모든 Vulkan 호출에 적용된다는 것을 의미합니다. 그럼에도 사양 문서에서는 여전히 일부 구현에서 요구하는 호환성을 위해 장치 수준에서도 밸리데이션 레이어를 활성화할 것을 권장합니다. 우리는 인스턴스 수준에서 지정한 것과 동일한 레이어를 논리 장치 수준에서도 지정할 것입니다. 이는 [나중에](!ko/Drawing_a_triangle/Setup/Logical_device_and_queues) 살펴볼 것입니다. + +## 밸리데이션 레이어 사용하기 + +이 섹션에서는 Vulkan SDK에서 제공하는 표준 진단 레이어를 활성화하는 방법을 살펴보겠습니다. 확장(extension)과 마찬가지로, 밸리데이션 레이어는 그 이름을 지정하여 활성화해야 합니다. 모든 유용한 표준 밸리데이션은 SDK에 포함된 `VK_LAYER_KHRONOS_validation`이라는 레이어에 번들로 제공됩니다. + +먼저 프로그램에 두 개의 설정 변수를 추가하여 활성화할 레이어와 활성화 여부를 지정합시다. 저는 프로그램이 디버그 모드로 컴파일되는지에 따라 이 값을 결정하기로 했습니다. `NDEBUG` 매크로는 C++ 표준의 일부로 "not debug"를 의미합니다. ```c++ const uint32_t WIDTH = 800; @@ -90,11 +56,7 @@ const std::vector validationLayers = { #endif ``` -We'll add a new function `checkValidationLayerSupport` that checks if all of -the requested layers are available. First list all of the available layers -using the `vkEnumerateInstanceLayerProperties` function. Its usage is identical -to that of `vkEnumerateInstanceExtensionProperties` which was discussed in the -instance creation chapter. +요청한 모든 레이어가 사용 가능한지 확인하는 `checkValidationLayerSupport`라는 새 함수를 추가하겠습니다. 먼저 `vkEnumerateInstanceLayerProperties` 함수를 사용하여 사용 가능한 모든 레이어를 나열합니다. 이 함수의 사용법은 인스턴스 생성 챕터에서 다룬 `vkEnumerateInstanceExtensionProperties`와 동일합니다. ```c++ bool checkValidationLayerSupport() { @@ -108,8 +70,7 @@ bool checkValidationLayerSupport() { } ``` -Next, check if all of the layers in `validationLayers` exist in the -`availableLayers` list. You may need to include `` for `strcmp`. +다음으로, `validationLayers`에 있는 모든 레이어가 `availableLayers` 목록에 존재하는지 확인합니다. `strcmp`를 사용하기 위해 `` 헤더가 필요할 수 있습니다. ```c++ for (const char* layerName : validationLayers) { @@ -130,23 +91,21 @@ for (const char* layerName : validationLayers) { return true; ``` -We can now use this function in `createInstance`: +이제 이 함수를 `createInstance`에서 사용할 수 있습니다. ```c++ void createInstance() { if (enableValidationLayers && !checkValidationLayerSupport()) { - throw std::runtime_error("validation layers requested, but not available!"); + throw std::runtime_error("요청한 밸리데이션 레이어를 사용할 수 없습니다!"); } ... } ``` -Now run the program in debug mode and ensure that the error does not occur. If -it does, then have a look at the FAQ. +이제 디버그 모드로 프로그램을 실행하여 오류가 발생하지 않는지 확인하세요. 만약 오류가 발생한다면 FAQ를 확인해 보세요. -Finally, modify the `VkInstanceCreateInfo` struct instantiation to include the -validation layer names if they are enabled: +마지막으로, `VkInstanceCreateInfo` 구조체 인스턴스화 부분을 수정하여 밸리데이션 레이어가 활성화된 경우 레이어 이름을 포함하도록 합니다. ```c++ if (enableValidationLayers) { @@ -157,18 +116,15 @@ if (enableValidationLayers) { } ``` -If the check was successful then `vkCreateInstance` should not ever return a -`VK_ERROR_LAYER_NOT_PRESENT` error, but you should run the program to make sure. +검사가 성공했다면 `vkCreateInstance`는 `VK_ERROR_LAYER_NOT_PRESENT` 오류를 반환하지 않아야 하지만, 프로그램을 실행하여 확실히 확인해야 합니다. -## Message callback +## 메시지 콜백 -The validation layers will print debug messages to the standard output by default, but we can also handle them ourselves by providing an explicit callback in our program. This will also allow you to decide which kind of messages you would like to see, because not all are necessarily (fatal) errors. If you don't want to do that right now then you may skip to the last section in this chapter. +밸리데이션 레이어는 기본적으로 디버그 메시지를 표준 출력으로 인쇄하지만, 프로그램에 명시적인 콜백을 제공하여 직접 처리할 수도 있습니다. 이를 통해 어떤 종류의 메시지를 보고 싶은지 결정할 수 있습니다. 모든 메시지가 반드시 (치명적인) 오류는 아니기 때문입니다. 지금 당장 이 작업을 하고 싶지 않다면 이 챕터의 마지막 섹션으로 건너뛰어도 좋습니다. -To set up a callback in the program to handle messages and the associated details, we have to set up a debug messenger with a callback using the `VK_EXT_debug_utils` extension. +메시지와 관련 세부 정보를 처리할 콜백을 프로그램에 설정하려면 `VK_EXT_debug_utils` 확장을 사용하여 디버그 메신저와 콜백을 설정해야 합니다. -We'll first create a `getRequiredExtensions` function that will return the -required list of extensions based on whether validation layers are enabled or -not: +먼저 밸리데이션 레이어 활성화 여부에 따라 필요한 확장 목록을 반환하는 `getRequiredExtensions` 함수를 만들겠습니다. ```c++ std::vector getRequiredExtensions() { @@ -186,12 +142,9 @@ std::vector getRequiredExtensions() { } ``` -The extensions specified by GLFW are always required, but the debug messenger -extension is conditionally added. Note that I've used the -`VK_EXT_DEBUG_UTILS_EXTENSION_NAME` macro here which is equal to the literal -string "VK_EXT_debug_utils". Using this macro lets you avoid typos. +GLFW가 지정한 확장은 항상 필요하지만, 디버그 메신저 확장은 조건부로 추가됩니다. 여기서 저는 리터럴 문자열 "VK_EXT_debug_utils"와 동일한 `VK_EXT_DEBUG_UTILS_EXTENSION_NAME` 매크로를 사용했습니다. 이 매크로를 사용하면 오타를 방지할 수 있습니다. -We can now use this function in `createInstance`: +이제 이 함수를 `createInstance`에서 사용할 수 있습니다. ```c++ auto extensions = getRequiredExtensions(); @@ -199,15 +152,9 @@ createInfo.enabledExtensionCount = static_cast(extensions.size()); createInfo.ppEnabledExtensionNames = extensions.data(); ``` -Run the program to make sure you don't receive a -`VK_ERROR_EXTENSION_NOT_PRESENT` error. We don't really need to check for the -existence of this extension, because it should be implied by the availability of -the validation layers. +프로그램을 실행하여 `VK_ERROR_EXTENSION_NOT_PRESENT` 오류가 발생하지 않는지 확인하세요. 이 확장의 존재 여부를 굳이 확인할 필요는 없습니다. 밸리데이션 레이어가 사용 가능하다면 이 확장도 당연히 사용 가능해야 하기 때문입니다. -Now let's see what a debug callback function looks like. Add a new static member -function called `debugCallback` with the `PFN_vkDebugUtilsMessengerCallbackEXT` -prototype. The `VKAPI_ATTR` and `VKAPI_CALL` ensure that the function has the -right signature for Vulkan to call it. +이제 디버그 콜백 함수가 어떻게 생겼는지 봅시다. `PFN_vkDebugUtilsMessengerCallbackEXT` 프로토타입을 가진 `debugCallback`이라는 새 정적 멤버 함수를 추가합니다. `VKAPI_ATTR`과 `VKAPI_CALL`은 함수가 Vulkan이 호출할 수 있는 올바른 시그니처를 갖도록 보장합니다. ```c++ static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( @@ -216,58 +163,50 @@ static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) { - std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl; + std::cerr << "밸리데이션 레이어: " << pCallbackData->pMessage << std::endl; return VK_FALSE; } ``` -The first parameter specifies the severity of the message, which is one of the following flags: +첫 번째 파라미터는 메시지의 심각도(severity)를 지정하며, 다음 플래그 중 하나입니다: -* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT`: Diagnostic message -* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT`: Informational message like the creation of a resource -* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT`: Message about behavior that is not necessarily an error, but very likely a bug in your application -* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT`: Message about behavior that is invalid and may cause crashes +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT`: 진단 메시지 +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT`: 리소스 생성과 같은 정보성 메시지 +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT`: 반드시 오류는 아니지만 애플리케이션의 버그일 가능성이 매우 높은 동작에 대한 메시지 +* `VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT`: 유효하지 않으며 크래시를 유발할 수 있는 동작에 대한 메시지 -The values of this enumeration are set up in such a way that you can use a comparison operation to check if a message is equal or worse compared to some level of severity, for example: +이 열거형의 값들은 비교 연산을 사용하여 메시지가 특정 심각도 수준과 같거나 더 나쁜지 확인할 수 있도록 설정되어 있습니다. 예를 들면 다음과 같습니다: ```c++ if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { - // Message is important enough to show + // 메시지가 표시할 만큼 중요함 } ``` -The `messageType` parameter can have the following values: +`messageType` 파라미터는 다음 값을 가질 수 있습니다: -* `VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT`: Some event has happened that is unrelated to the specification or performance -* `VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT`: Something has happened that violates the specification or indicates a possible mistake -* `VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT`: Potential non-optimal use of Vulkan +* `VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT`: 사양이나 성능과 관련 없는 어떤 이벤트가 발생함 +* `VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT`: 사양을 위반하거나 가능한 실수를 나타내는 어떤 일이 발생함 +* `VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT`: Vulkan의 잠재적인 비최적 사용 -The `pCallbackData` parameter refers to a `VkDebugUtilsMessengerCallbackDataEXT` struct containing the details of the message itself, with the most important members being: +`pCallbackData` 파라미터는 메시지 자체의 세부 정보를 포함하는 `VkDebugUtilsMessengerCallbackDataEXT` 구조체를 가리키며, 가장 중요한 멤버는 다음과 같습니다: -* `pMessage`: The debug message as a null-terminated string -* `pObjects`: Array of Vulkan object handles related to the message -* `objectCount`: Number of objects in array +* `pMessage`: null로 끝나는 문자열 형태의 디버그 메시지 +* `pObjects`: 메시지와 관련된 Vulkan 객체 핸들 배열 +* `objectCount`: 배열에 있는 객체의 수 -Finally, the `pUserData` parameter contains a pointer that was specified during the setup of the callback and allows you to pass your own data to it. +마지막으로, `pUserData` 파라미터는 콜백 설정 중에 지정된 포인터를 포함하며, 이를 통해 자신의 데이터를 콜백에 전달할 수 있습니다. -The callback returns a boolean that indicates if the Vulkan call that triggered -the validation layer message should be aborted. If the callback returns true, -then the call is aborted with the `VK_ERROR_VALIDATION_FAILED_EXT` error. This -is normally only used to test the validation layers themselves, so you should -always return `VK_FALSE`. +콜백은 밸리데이션 레이어 메시지를 촉발한 Vulkan 호출을 중단해야 하는지를 나타내는 불리언 값을 반환합니다. 콜백이 true를 반환하면 해당 호출은 `VK_ERROR_VALIDATION_FAILED_EXT` 오류와 함께 중단됩니다. 이는 보통 밸리데이션 레이어 자체를 테스트하는 데만 사용되므로, 항상 `VK_FALSE`를 반환해야 합니다. -All that remains now is telling Vulkan about the callback function. Perhaps -somewhat surprisingly, even the debug callback in Vulkan is managed with a -handle that needs to be explicitly created and destroyed. Such a callback is part of a *debug messenger* and you can have as many of them as you want. Add a class member for -this handle right under `instance`: +이제 남은 것은 Vulkan에 콜백 함수에 대해 알리는 것뿐입니다. 아마도 놀랍게도 Vulkan의 디버그 콜백조차도 명시적으로 생성하고 소멸해야 하는 핸들로 관리됩니다. 이러한 콜백은 *디버그 메신저*의 일부이며, 원하는 만큼 많이 가질 수 있습니다. `instance` 바로 아래에 이 핸들을 위한 클래스 멤버를 추가합니다: ```c++ VkDebugUtilsMessengerEXT debugMessenger; ``` -Now add a function `setupDebugMessenger` to be called from `initVulkan` right -after `createInstance`: +이제 `createInstance` 직후 `initVulkan`에서 호출할 `setupDebugMessenger` 함수를 추가합니다. ```c++ void initVulkan() { @@ -281,7 +220,7 @@ void setupDebugMessenger() { } ``` -We'll need to fill in a structure with details about the messenger and its callback: +메신저와 그 콜백에 대한 세부 정보로 구조체를 채워야 합니다: ```c++ VkDebugUtilsMessengerCreateInfoEXT createInfo{}; @@ -289,23 +228,18 @@ createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; createInfo.pfnUserCallback = debugCallback; -createInfo.pUserData = nullptr; // Optional +createInfo.pUserData = nullptr; // 선택 사항 ``` -The `messageSeverity` field allows you to specify all the types of severities you would like your callback to be called for. I've specified all types except for `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT` here to receive notifications about possible problems while leaving out verbose general debug info. +`messageSeverity` 필드를 사용하면 콜백이 호출되기를 원하는 모든 심각도 유형을 지정할 수 있습니다. 여기서는 상세한 일반 디버그 정보를 제외하고 가능한 문제에 대한 알림을 받기 위해 `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT`를 제외한 모든 유형을 지정했습니다. -Similarly the `messageType` field lets you filter which types of messages your callback is notified about. I've simply enabled all types here. You can always disable some if they're not useful to you. +마찬가지로 `messageType` 필드를 사용하면 콜백이 알림을 받을 메시지 유형을 필터링할 수 있습니다. 여기서는 단순히 모든 유형을 활성화했습니다. 유용하지 않은 유형이 있다면 언제든지 비활성화할 수 있습니다. -Finally, the `pfnUserCallback` field specifies the pointer to the callback function. You can optionally pass a pointer to the `pUserData` field which will be passed along to the callback function via the `pUserData` parameter. You could use this to pass a pointer to the `HelloTriangleApplication` class, for example. +마지막으로 `pfnUserCallback` 필드는 콜백 함수에 대한 포인터를 지정합니다. 선택적으로 `pUserData` 필드에 포인터를 전달할 수 있으며, 이 포인터는 `pUserData` 파라미터를 통해 콜백 함수로 전달됩니다. 예를 들어, 이를 사용하여 `HelloTriangleApplication` 클래스에 대한 포인터를 전달할 수 있습니다. -Note that there are many more ways to configure validation layer messages and debug callbacks, but this is a good setup to get started with for this tutorial. See the [extension specification](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap50.html#VK_EXT_debug_utils) for more info about the possibilities. +밸리데이션 레이어 메시지와 디버그 콜백을 구성하는 더 많은 방법이 있지만, 이 튜토리얼을 시작하기에는 이 설정이 좋습니다. 가능한 설정에 대한 자세한 내용은 [확장 사양](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap50.html#VK_EXT_debug_utils)을 참조하세요. -This struct should be passed to the `vkCreateDebugUtilsMessengerEXT` function to -create the `VkDebugUtilsMessengerEXT` object. Unfortunately, because this -function is an extension function, it is not automatically loaded. We have to -look up its address ourselves using `vkGetInstanceProcAddr`. We're going to -create our own proxy function that handles this in the background. I've added it -right above the `HelloTriangleApplication` class definition. +이 구조체는 `vkCreateDebugUtilsMessengerEXT` 함수에 전달되어 `VkDebugUtilsMessengerEXT` 객체를 생성해야 합니다. 안타깝게도 이 함수는 확장 함수이므로 자동으로 로드되지 않습니다. `vkGetInstanceProcAddr`를 사용하여 주소를 직접 찾아야 합니다. 이 작업을 백그라운드에서 처리하는 프록시 함수를 직접 만들겠습니다. `HelloTriangleApplication` 클래스 정의 바로 위에 추가했습니다. ```c++ VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) { @@ -318,27 +252,19 @@ VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMes } ``` -The `vkGetInstanceProcAddr` function will return `nullptr` if the function -couldn't be loaded. We can now call this function to create the extension -object if it's available: +`vkGetInstanceProcAddr` 함수는 함수를 로드할 수 없는 경우 `nullptr`를 반환합니다. 이제 이 함수를 호출하여 확장 객체를 생성할 수 있습니다. ```c++ if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { - throw std::runtime_error("failed to set up debug messenger!"); + throw std::runtime_error("디버그 메신저 설정에 실패했습니다!"); } ``` -The second to last parameter is again the optional allocator callback that we -set to `nullptr`, other than that the parameters are fairly straightforward. -Since the debug messenger is specific to our Vulkan instance and its layers, it -needs to be explicitly specified as first argument. You will also see this -pattern with other *child* objects later on. +끝에서 두 번째 파라미터는 다시 선택적 할당자 콜백으로, 여기서는 `nullptr`로 설정했습니다. 그 외의 파라미터는 매우 간단합니다. 디버그 메신저는 우리의 Vulkan 인스턴스와 그 레이어에 한정적이므로, 첫 번째 인자로 명시적으로 지정해야 합니다. 나중에 다른 *자식* 객체에서도 이 패턴을 보게 될 것입니다. -The `VkDebugUtilsMessengerEXT` object also needs to be cleaned up with a call to -`vkDestroyDebugUtilsMessengerEXT`. Similarly to `vkCreateDebugUtilsMessengerEXT` -the function needs to be explicitly loaded. +`VkDebugUtilsMessengerEXT` 객체는 `vkDestroyDebugUtilsMessengerEXT` 호출로 정리해야 합니다. `vkCreateDebugUtilsMessengerEXT`와 마찬가지로 이 함수도 명시적으로 로드해야 합니다. -Create another proxy function right below `CreateDebugUtilsMessengerEXT`: +`CreateDebugUtilsMessengerEXT` 바로 아래에 또 다른 프록시 함수를 만듭니다. ```c++ void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) { @@ -349,8 +275,7 @@ void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT } ``` -Make sure that this function is either a static class function or a function -outside the class. We can then call it in the `cleanup` function: +이 함수가 정적 클래스 함수이거나 클래스 외부의 함수인지 확인하세요. 그런 다음 `cleanup` 함수에서 호출할 수 있습니다. ```c++ void cleanup() { @@ -366,11 +291,11 @@ void cleanup() { } ``` -## Debugging instance creation and destruction +## 인스턴스 생성 및 소멸 디버깅하기 -Although we've now added debugging with validation layers to the program we're not covering everything quite yet. The `vkCreateDebugUtilsMessengerEXT` call requires a valid instance to have been created and `vkDestroyDebugUtilsMessengerEXT` must be called before the instance is destroyed. This currently leaves us unable to debug any issues in the `vkCreateInstance` and `vkDestroyInstance` calls. +지금까지 프로그램에 밸리데이션 레이어를 사용한 디버깅을 추가했지만 아직 모든 것을 다루지는 못했습니다. `vkCreateDebugUtilsMessengerEXT` 호출은 유효한 인스턴스가 생성되어 있어야 하고, `vkDestroyDebugUtilsMessengerEXT`는 인스턴스가 소멸되기 전에 호출되어야 합니다. 이로 인해 현재로서는 `vkCreateInstance` 및 `vkDestroyInstance` 호출의 문제를 디버깅할 수 없습니다. -However, if you closely read the [extension documentation](https://github.com/KhronosGroup/Vulkan-Docs/blob/main/appendices/VK_EXT_debug_utils.adoc#examples), you'll see that there is a way to create a separate debug utils messenger specifically for those two function calls. It requires you to simply pass a pointer to a `VkDebugUtilsMessengerCreateInfoEXT` struct in the `pNext` extension field of `VkInstanceCreateInfo`. First extract population of the messenger create info into a separate function: +하지만 [확장 문서](https://github.com/KhronosGroup/Vulkan-Docs/blob/main/appendices/VK_EXT_debug_utils.adoc#examples)를 자세히 읽어보면, 이 두 함수 호출을 위해 별도의 디버그 유틸리티 메신저를 만드는 방법이 있다는 것을 알 수 있습니다. `VkInstanceCreateInfo`의 `pNext` 확장 필드에 `VkDebugUtilsMessengerCreateInfoEXT` 구조체에 대한 포인터를 전달하기만 하면 됩니다. 먼저 메신저 생성 정보 채우기를 별도의 함수로 추출합니다. ```c++ void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) { @@ -390,12 +315,12 @@ void setupDebugMessenger() { populateDebugMessengerCreateInfo(createInfo); if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { - throw std::runtime_error("failed to set up debug messenger!"); + throw std::runtime_error("디버그 메신저 설정에 실패했습니다!"); } } ``` -We can now re-use this in the `createInstance` function: +이제 이 함수를 `createInstance` 함수에서 재사용할 수 있습니다. ```c++ void createInstance() { @@ -421,38 +346,29 @@ void createInstance() { } if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { - throw std::runtime_error("failed to create instance!"); + throw std::runtime_error("인스턴스 생성에 실패했습니다!"); } } ``` -The `debugCreateInfo` variable is placed outside the if statement to ensure that it is not destroyed before the `vkCreateInstance` call. By creating an additional debug messenger this way it will automatically be used during `vkCreateInstance` and `vkDestroyInstance` and cleaned up after that. +`debugCreateInfo` 변수는 `vkCreateInstance` 호출 전에 소멸되지 않도록 `if` 문 밖에 위치합니다. 이런 식으로 추가 디버그 메신저를 생성하면 `vkCreateInstance`와 `vkDestroyInstance` 동안 자동으로 사용되고 그 이후에 정리됩니다. -## Testing +## 테스트하기 -Now let's intentionally make a mistake to see the validation layers in action. Temporarily remove the call to `DestroyDebugUtilsMessengerEXT` in the `cleanup` function and run your program. Once it exits you should see something like this: +이제 의도적으로 실수를 만들어 밸리데이션 레이어가 작동하는 것을 확인해 봅시다. `cleanup` 함수에서 `DestroyDebugUtilsMessengerEXT` 호출을 일시적으로 제거하고 프로그램을 실행하세요. 프로그램이 종료되면 다음과 같은 메시지를 보게 될 것입니다. ![](/images/validation_layer_test.png) ->If you don't see any messages then [check your installation](https://vulkan.lunarg.com/doc/view/1.2.131.1/windows/getting_started.html#user-content-verify-the-installation). +> 메시지가 보이지 않는다면 [설치를 확인](https://vulkan.lunarg.com/doc/view/1.2.131.1/windows/getting_started.html#user-content-verify-the-installation)하세요. -If you want to see which call triggered a message, you can add a breakpoint to the message callback and look at the stack trace. +어떤 호출이 메시지를 유발했는지 확인하고 싶다면, 메시지 콜백에 중단점(breakpoint)을 설정하고 스택 추적을 살펴보면 됩니다. -## Configuration +## 설정 -There are a lot more settings for the behavior of validation layers than just -the flags specified in the `VkDebugUtilsMessengerCreateInfoEXT` struct. Browse -to the Vulkan SDK and go to the `Config` directory. There you will find a -`vk_layer_settings.txt` file that explains how to configure the layers. +`VkDebugUtilsMessengerCreateInfoEXT` 구조체에 지정된 플래그 외에도 밸리데이션 레이어의 동작에 대한 설정은 훨씬 더 많습니다. Vulkan SDK로 이동하여 `Config` 디렉토리로 가보세요. 그곳에서 레이어를 구성하는 방법을 설명하는 `vk_layer_settings.txt` 파일을 찾을 수 있습니다. -To configure the layer settings for your own application, copy the file to the -`Debug` and `Release` directories of your project and follow the instructions to -set the desired behavior. However, for the remainder of this tutorial I'll -assume that you're using the default settings. +자신의 애플리케이션에 대한 레이어 설정을 구성하려면, 이 파일을 프로젝트의 `Debug` 및 `Release` 디렉토리에 복사하고 지침에 따라 원하는 동작을 설정하세요. 하지만 이 튜토리얼의 나머지 부분에서는 기본 설정을 사용한다고 가정하겠습니다. -Throughout this tutorial I'll be making a couple of intentional mistakes to show -you how helpful the validation layers are with catching them and to teach you -how important it is to know exactly what you're doing with Vulkan. Now it's time -to look at [Vulkan devices in the system](!en/Drawing_a_triangle/Setup/Physical_devices_and_queue_families). +이 튜토리얼 전반에 걸쳐, 밸리데이션 레이어가 실수를 잡아내는 데 얼마나 도움이 되는지 보여주고 Vulkan으로 무엇을 하고 있는지 정확히 아는 것이 얼마나 중요한지 가르치기 위해 몇 가지 의도적인 실수를 할 것입니다. 이제 시스템의 [Vulkan 장치](!ko/Drawing_a_triangle/Setup/Physical_devices_and_queue_families)에 대해 알아볼 시간입니다. -[C++ code](/code/02_validation_layers.cpp) +[C++ 코드](/code/02_validation_layers.cpp) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md b/ko/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md index 5761b9bc..31fb7dc0 100644 --- a/ko/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md +++ b/ko/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md @@ -1,12 +1,8 @@ -## Selecting a physical device +## 물리 디바이스 선택하기 -After initializing the Vulkan library through a VkInstance we need to look for -and select a graphics card in the system that supports the features we need. In -fact we can select any number of graphics cards and use them simultaneously, but -in this tutorial we'll stick to the first graphics card that suits our needs. +`VkInstance`를 통해 벌칸 라이브러리를 초기화한 후에는, 시스템에서 우리가 필요로 하는 기능을 지원하는 그래픽 카드를 찾아 선택해야 합니다. 사실 여러 개의 그래픽 카드를 선택하여 동시에 사용할 수도 있지만, 이 튜토리얼에서는 우리에게 필요한 첫 번째 그래픽 카드만 사용하겠습니다. -We'll add a function `pickPhysicalDevice` and add a call to it in the -`initVulkan` function. +`pickPhysicalDevice` 함수를 추가하고 `initVulkan` 함수에서 호출하도록 하겠습니다. ```c++ void initVulkan() { @@ -20,24 +16,20 @@ void pickPhysicalDevice() { } ``` -The graphics card that we'll end up selecting will be stored in a -VkPhysicalDevice handle that is added as a new class member. This object will be -implicitly destroyed when the VkInstance is destroyed, so we won't need to do -anything new in the `cleanup` function. +최종적으로 선택할 그래픽 카드는 `VkPhysicalDevice` 핸들에 저장되며, 이 핸들을 새로운 클래스 멤버로 추가합니다. 이 객체는 `VkInstance`가 소멸될 때 암시적으로 함께 소멸되므로, `cleanup` 함수에서 따로 처리할 필요는 없습니다. ```c++ VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; ``` -Listing the graphics cards is very similar to listing extensions and starts with -querying just the number. +그래픽 카드를 나열하는 것은 확장을 나열하는 것과 매우 유사하며, 먼저 개수만 쿼리하는 것으로 시작합니다. ```c++ uint32_t deviceCount = 0; vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr); ``` -If there are 0 devices with Vulkan support then there is no point going further. +만약 벌칸을 지원하는 디바이스가 하나도 없다면 더 이상 진행할 의미가 없습니다. ```c++ if (deviceCount == 0) { @@ -45,17 +37,14 @@ if (deviceCount == 0) { } ``` -Otherwise we can now allocate an array to hold all of the VkPhysicalDevice -handles. +벌칸 지원 디바이스가 있다면, 모든 `VkPhysicalDevice` 핸들을 담을 배열을 할당할 수 있습니다. ```c++ std::vector devices(deviceCount); vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()); ``` -Now we need to evaluate each of them and check if they are suitable for the -operations we want to perform, because not all graphics cards are created equal. -For that we'll introduce a new function: +이제 각 디바이스를 평가하여 우리가 수행하려는 작업에 적합한지 확인해야 합니다. 모든 그래픽 카드가 동일하게 만들어지지는 않기 때문입니다. 이를 위해 새로운 함수를 도입하겠습니다. ```c++ bool isDeviceSuitable(VkPhysicalDevice device) { @@ -63,8 +52,7 @@ bool isDeviceSuitable(VkPhysicalDevice device) { } ``` -And we'll check if any of the physical devices meet the requirements that we'll -add to that function. +그리고 물리 디바이스 중 어느 것이든 이 함수에 추가할 요구사항을 충족하는지 확인할 것입니다. ```c++ for (const auto& device : devices) { @@ -79,36 +67,27 @@ if (physicalDevice == VK_NULL_HANDLE) { } ``` -The next section will introduce the first requirements that we'll check for in -the `isDeviceSuitable` function. As we'll start using more Vulkan features in -the later chapters we will also extend this function to include more checks. +다음 섹션에서는 `isDeviceSuitable` 함수에서 확인할 첫 번째 요구사항을 소개할 것입니다. 이후 챕터에서 더 많은 벌칸 기능을 사용하게 되면서, 이 함수에 더 많은 검사를 추가하여 확장할 것입니다. -## Base device suitability checks +## 기본적인 디바이스 적합성 검사 -To evaluate the suitability of a device we can start by querying for some -details. Basic device properties like the name, type and supported Vulkan -version can be queried using vkGetPhysicalDeviceProperties. +디바이스의 적합성을 평가하기 위해 몇 가지 세부 정보를 쿼리하는 것으로 시작할 수 있습니다. 이름, 타입, 지원하는 벌칸 버전과 같은 기본적인 디바이스 속성은 `vkGetPhysicalDeviceProperties`를 사용하여 쿼리할 수 있습니다. ```c++ VkPhysicalDeviceProperties deviceProperties; vkGetPhysicalDeviceProperties(device, &deviceProperties); ``` -The support for optional features like texture compression, 64 bit floats and -multi viewport rendering (useful for VR) can be queried using -vkGetPhysicalDeviceFeatures: +텍스처 압축, 64비트 부동소수점, 다중 뷰포트 렌더링(VR에 유용함)과 같은 선택적 기능에 대한 지원 여부는 `vkGetPhysicalDeviceFeatures`를 사용하여 쿼리할 수 있습니다. ```c++ VkPhysicalDeviceFeatures deviceFeatures; vkGetPhysicalDeviceFeatures(device, &deviceFeatures); ``` -There are more details that can be queried from devices that we'll discuss later -concerning device memory and queue families (see the next section). +디바이스 메모리 및 큐 패밀리(다음 섹션 참조)에 관해 나중에 논의할 더 많은 세부 정보가 있습니다. -As an example, let's say we consider our application only usable for dedicated -graphics cards that support geometry shaders. Then the `isDeviceSuitable` -function would look like this: +예를 들어, 우리 애플리케이션이 지오메트리 셰이더를 지원하는 외장 그래픽 카드에서만 사용 가능하다고 가정해 봅시다. 그렇다면 `isDeviceSuitable` 함수는 다음과 같이 보일 것입니다. ```c++ bool isDeviceSuitable(VkPhysicalDevice device) { @@ -122,11 +101,7 @@ bool isDeviceSuitable(VkPhysicalDevice device) { } ``` -Instead of just checking if a device is suitable or not and going with the first -one, you could also give each device a score and pick the highest one. That way -you could favor a dedicated graphics card by giving it a higher score, but fall -back to an integrated GPU if that's the only available one. You could implement -something like that as follows: +디바이스가 적합한지 아닌지만 확인하고 첫 번째 것을 사용하는 대신, 각 디바이스에 점수를 매겨 가장 높은 점수를 받은 디바이스를 선택할 수도 있습니다. 이런 방식을 사용하면 외장 그래픽 카드에 더 높은 점수를 주어 선호하되, 사용 가능한 유일한 GPU가 내장 GPU일 경우 차선책으로 선택할 수 있습니다. 다음과 같이 구현할 수 있습니다. ```c++ #include @@ -136,7 +111,7 @@ something like that as follows: void pickPhysicalDevice() { ... - // Use an ordered map to automatically sort candidates by increasing score + // 순서가 있는 맵을 사용하여 후보들을 점수 오름차순으로 자동 정렬 std::multimap candidates; for (const auto& device : devices) { @@ -144,7 +119,7 @@ void pickPhysicalDevice() { candidates.insert(std::make_pair(score, device)); } - // Check if the best candidate is suitable at all + // 가장 좋은 후보가 사용 가능한지 확인 if (candidates.rbegin()->first > 0) { physicalDevice = candidates.rbegin()->second; } else { @@ -157,15 +132,15 @@ int rateDeviceSuitability(VkPhysicalDevice device) { int score = 0; - // Discrete GPUs have a significant performance advantage + // 외장 GPU는 상당한 성능 이점을 가짐 if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { score += 1000; } - // Maximum possible size of textures affects graphics quality + // 텍스처의 최대 크기는 그래픽 품질에 영향을 줌 score += deviceProperties.limits.maxImageDimension2D; - // Application can't function without geometry shaders + // 애플리케이션은 지오메트리 셰이더 없이는 작동할 수 없음 if (!deviceFeatures.geometryShader) { return 0; } @@ -174,12 +149,9 @@ int rateDeviceSuitability(VkPhysicalDevice device) { } ``` -You don't need to implement all that for this tutorial, but it's to give you an -idea of how you could design your device selection process. Of course you can -also just display the names of the choices and allow the user to select. +이 튜토리얼에서 이 모든 것을 구현할 필요는 없지만, 디바이스 선택 프로세스를 어떻게 설계할 수 있는지에 대한 아이디어를 제공하기 위한 것입니다. 물론, 선택 가능한 디바이스 목록을 보여주고 사용자가 선택하게 할 수도 있습니다. -Because we're just starting out, Vulkan support is the only thing we need and -therefore we'll settle for just any GPU: +우리는 이제 막 시작하는 단계이므로, 벌칸 지원 여부만이 유일한 요구사항입니다. 따라서 어떤 GPU든 상관없이 사용하겠습니다. ```c++ bool isDeviceSuitable(VkPhysicalDevice device) { @@ -187,32 +159,23 @@ bool isDeviceSuitable(VkPhysicalDevice device) { } ``` -In the next section we'll discuss the first real required feature to check for. +다음 섹션에서는 확인해야 할 첫 번째 실질적인 필수 기능에 대해 논의할 것입니다. -## Queue families +## 큐 패밀리 (Queue families) -It has been briefly touched upon before that almost every operation in Vulkan, -anything from drawing to uploading textures, requires commands to be submitted -to a queue. There are different types of queues that originate from different -*queue families* and each family of queues allows only a subset of commands. For -example, there could be a queue family that only allows processing of compute -commands or one that only allows memory transfer related commands. +이전에도 잠시 언급했듯이, 드로잉부터 텍스처 업로드에 이르기까지 벌칸의 거의 모든 작업은 명령(command)을 큐(queue)에 제출해야 합니다. 큐에는 여러 종류가 있으며, 이들은 각기 다른 *큐 패밀리(queue families)*에서 비롯됩니다. 각 큐 패밀리는 특정 종류의 명령만 허용합니다. 예를 들어, 컴퓨트(compute) 명령만 처리하는 큐 패밀리가 있을 수 있고, 메모리 전송 관련 명령만 처리하는 큐 패밀리가 있을 수도 있습니다. -We need to check which queue families are supported by the device and which one -of these supports the commands that we want to use. For that purpose we'll add a -new function `findQueueFamilies` that looks for all the queue families we need. +우리는 디바이스가 어떤 큐 패밀리를 지원하는지, 그리고 그중 어떤 큐 패밀리가 우리가 사용하려는 명령을 지원하는지 확인해야 합니다. 이를 위해, 우리가 필요로 하는 모든 큐 패밀리를 찾는 새로운 함수 `findQueueFamilies`를 추가하겠습니다. -Right now we are only going to look for a queue that supports graphics commands, -so the function could look like this: +지금은 그래픽 명령을 지원하는 큐만 찾을 것이므로, 함수는 다음과 같을 수 있습니다. ```c++ uint32_t findQueueFamilies(VkPhysicalDevice device) { - // Logic to find graphics queue family + // 그래픽 큐 패밀리를 찾는 로직 } ``` -However, in one of the next chapters we're already going to look for yet another -queue, so it's better to prepare for that and bundle the indices into a struct: +하지만 다음 챕터 중 하나에서 곧바로 다른 큐를 찾게 될 것이므로, 미리 대비하여 인덱스들을 구조체로 묶는 것이 좋습니다. ```c++ struct QueueFamilyIndices { @@ -221,21 +184,14 @@ struct QueueFamilyIndices { QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { QueueFamilyIndices indices; - // Logic to find queue family indices to populate struct with + // 구조체를 채우기 위해 큐 패밀리 인덱스를 찾는 로직 return indices; } ``` -But what if a queue family is not available? We could throw an exception in -`findQueueFamilies`, but this function is not really the right place to make -decisions about device suitability. For example, we may *prefer* devices with a -dedicated transfer queue family, but not require it. Therefore we need some way -of indicating whether a particular queue family was found. +그런데 만약 큐 패밀리를 찾을 수 없다면 어떻게 해야 할까요? `findQueueFamilies` 함수에서 예외를 던질 수도 있지만, 이 함수는 디바이스 적합성을 결정하기에 적절한 위치가 아닙니다. 예를 들어, 우리는 전용 전송(transfer) 큐 패밀리를 가진 디바이스를 *선호*할 수는 있지만, 필수 조건으로 삼고 싶지는 않을 수 있습니다. 따라서 특정 큐 패밀리가 발견되었는지 여부를 나타낼 방법이 필요합니다. -It's not really possible to use a magic value to indicate the nonexistence of a -queue family, since any value of `uint32_t` could in theory be a valid queue -family index including `0`. Luckily C++17 introduced a data structure to -distinguish between the case of a value existing or not: +큐 패밀리가 존재하지 않음을 나타내기 위해 특별한 값(magic value)을 사용하는 것은 사실상 불가능합니다. `0`을 포함한 모든 `uint32_t` 값이 이론적으로 유효한 큐 패밀리 인덱스가 될 수 있기 때문입니다. 다행히 C++17에서는 값이 존재하는 경우와 그렇지 않은 경우를 구별하기 위한 데이터 구조를 도입했습니다. ```c++ #include @@ -251,9 +207,7 @@ graphicsFamily = 0; std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // true ``` -`std::optional` is a wrapper that contains no value until you assign something -to it. At any point you can query if it contains a value or not by calling its -`has_value()` member function. That means that we can change the logic to: +`std::optional`은 값을 할당하기 전까지는 아무 값도 포함하지 않는 래퍼(wrapper)입니다. 언제든지 `has_value()` 멤버 함수를 호출하여 값이 포함되어 있는지 여부를 쿼리할 수 있습니다. 이를 통해 로직을 다음과 같이 변경할 수 있습니다. ```c++ #include @@ -266,12 +220,12 @@ struct QueueFamilyIndices { QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { QueueFamilyIndices indices; - // Assign index to queue families that could be found + // 찾은 큐 패밀리에 인덱스 할당 return indices; } ``` -We can now begin to actually implement `findQueueFamilies`: +이제 `findQueueFamilies`를 실제로 구현해 보겠습니다. ```c++ QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { @@ -283,8 +237,7 @@ QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { } ``` -The process of retrieving the list of queue families is exactly what you expect -and uses `vkGetPhysicalDeviceQueueFamilyProperties`: +큐 패밀리 목록을 가져오는 과정은 예상대로이며, `vkGetPhysicalDeviceQueueFamilyProperties`를 사용합니다. ```c++ uint32_t queueFamilyCount = 0; @@ -294,10 +247,7 @@ std::vector queueFamilies(queueFamilyCount); vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data()); ``` -The VkQueueFamilyProperties struct contains some details about the queue family, -including the type of operations that are supported and the number of queues -that can be created based on that family. We need to find at least one queue -family that supports `VK_QUEUE_GRAPHICS_BIT`. +`VkQueueFamilyProperties` 구조체는 지원되는 작업 유형 및 해당 패밀리를 기반으로 생성할 수 있는 큐의 수를 포함하여 큐 패밀리에 대한 일부 세부 정보를 담고 있습니다. 우리는 `VK_QUEUE_GRAPHICS_BIT`를 지원하는 큐 패밀리를 최소 하나 이상 찾아야 합니다. ```c++ int i = 0; @@ -310,9 +260,7 @@ for (const auto& queueFamily : queueFamilies) { } ``` -Now that we have this fancy queue family lookup function, we can use it as a -check in the `isDeviceSuitable` function to ensure that the device can process -the commands we want to use: +이제 이 멋진 큐 패밀리 조회 함수를 `isDeviceSuitable` 함수에서 검사 항목으로 사용하여, 디바이스가 우리가 사용하려는 명령을 처리할 수 있는지 확인할 수 있습니다. ```c++ bool isDeviceSuitable(VkPhysicalDevice device) { @@ -322,8 +270,7 @@ bool isDeviceSuitable(VkPhysicalDevice device) { } ``` -To make this a little bit more convenient, we'll also add a generic check to the -struct itself: +이를 좀 더 편리하게 만들기 위해, 구조체 자체에 일반적인 검사 함수를 추가하겠습니다. ```c++ struct QueueFamilyIndices { @@ -343,7 +290,7 @@ bool isDeviceSuitable(VkPhysicalDevice device) { } ``` -We can now also use this for an early exit from `findQueueFamilies`: +이제 이 함수를 `findQueueFamilies`에서 조기 탈출하는 데에도 사용할 수 있습니다. ```c++ for (const auto& queueFamily : queueFamilies) { @@ -357,8 +304,6 @@ for (const auto& queueFamily : queueFamilies) { } ``` -Great, that's all we need for now to find the right physical device! The next -step is to [create a logical device](!en/Drawing_a_triangle/Setup/Logical_device_and_queues) -to interface with it. +좋습니다, 이것으로 적절한 물리 디바이스를 찾는 데 필요한 모든 작업이 끝났습니다! 다음 단계는 [논리 디바이스를 생성하여](!ko/Drawing_a_triangle/Setup/Logical_device_and_queues) 물리 디바이스와 상호작용하는 것입니다. -[C++ code](/code/03_physical_device_selection.cpp) +[C++ 코드](/code/03_physical_device_selection.cpp) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md b/ko/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md index f2677d08..6cbeae88 100644 --- a/ko/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md +++ b/ko/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md @@ -1,19 +1,14 @@ -## Introduction +## 도입 -After selecting a physical device to use we need to set up a *logical device* to -interface with it. The logical device creation process is similar to the -instance creation process and describes the features we want to use. We also -need to specify which queues to create now that we've queried which queue -families are available. You can even create multiple logical devices from the -same physical device if you have varying requirements. +사용할 물리 장치를 선택한 후에는, 이와 상호작용하기 위한 *논리 장치*를 설정해야 합니다. 논리 장치 생성 과정은 인스턴스 생성 과정과 유사하며, 우리가 사용하고자 하는 기능들을 기술합니다. 또한, 어떤 큐 패밀리를 사용할 수 있는지 질의했으므로 이제 어떤 큐를 생성할지 명시해야 합니다. 요구 사항이 다양하다면 동일한 물리 장치에서 여러 개의 논리 장치를 생성할 수도 있습니다. -Start by adding a new class member to store the logical device handle in. +먼저 클래스 멤버를 새로 추가하여 논리 장치 핸들을 저장하도록 합시다. ```c++ VkDevice device; ``` -Next, add a `createLogicalDevice` function that is called from `initVulkan`. +다음으로, `initVulkan`에서 호출될 `createLogicalDevice` 함수를 추가합니다. ```c++ void initVulkan() { @@ -28,12 +23,9 @@ void createLogicalDevice() { } ``` -## Specifying the queues to be created +## 생성할 큐 명시하기 -The creation of a logical device involves specifying a bunch of details in -structs again, of which the first one will be `VkDeviceQueueCreateInfo`. This -structure describes the number of queues we want for a single queue family. -Right now we're only interested in a queue with graphics capabilities. +논리 장치 생성 과정에는 여러 구조체에 상세 정보를 지정하는 작업이 포함됩니다. 그중 첫 번째는 `VkDeviceQueueCreateInfo`입니다. 이 구조체는 단일 큐 패밀리에 대해 우리가 원하는 큐의 개수를 기술합니다. 지금 당장은 그래픽스 기능이 있는 큐에만 관심이 있습니다. ```c++ QueueFamilyIndices indices = findQueueFamilies(physicalDevice); @@ -44,44 +36,33 @@ queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value(); queueCreateInfo.queueCount = 1; ``` -The currently available drivers will only allow you to create a small number of -queues for each queue family and you don't really need more than one. That's -because you can create all of the command buffers on multiple threads and then -submit them all at once on the main thread with a single low-overhead call. +현재 사용 가능한 드라이버들은 각 큐 패밀리마다 소수의 큐만 생성하도록 허용하며, 실제로 하나보다 더 많이 필요한 경우는 거의 없습니다. 그 이유는 여러 스레드에서 모든 커맨드 버퍼를 생성한 다음, 메인 스레드에서 단 한 번의 오버헤드가 적은 호출로 모두 제출할 수 있기 때문입니다. -Vulkan lets you assign priorities to queues to influence the scheduling of -command buffer execution using floating point numbers between `0.0` and `1.0`. -This is required even if there is only a single queue: +Vulkan에서는 `0.0`에서 `1.0` 사이의 부동 소수점 숫자를 사용하여 큐에 우선순위를 할당하고 커맨드 버퍼 실행 스케줄링에 영향을 줄 수 있습니다. 큐가 하나만 있는 경우에도 이 설정은 필수입니다. ```c++ float queuePriority = 1.0f; queueCreateInfo.pQueuePriorities = &queuePriority; ``` -## Specifying used device features +## 사용할 장치 기능 명시하기 -The next information to specify is the set of device features that we'll be -using. These are the features that we queried support for with -`vkGetPhysicalDeviceFeatures` in the previous chapter, like geometry shaders. -Right now we don't need anything special, so we can simply define it and leave -everything to `VK_FALSE`. We'll come back to this structure once we're about to -start doing more interesting things with Vulkan. +다음으로 명시할 정보는 우리가 사용할 장치 기능의 집합입니다. 이는 이전 장에서 `vkGetPhysicalDeviceFeatures`로 지원 여부를 질의했던 지오메트리 셰이더와 같은 기능들입니다. 지금 당장은 특별한 기능이 필요 없으므로, 구조체를 정의하고 모든 값을 `VK_FALSE`로 두면 됩니다. Vulkan으로 더 흥미로운 작업을 시작할 때 이 구조체로 다시 돌아올 것입니다. ```c++ VkPhysicalDeviceFeatures deviceFeatures{}; ``` -## Creating the logical device +## 논리 장치 생성하기 -With the previous two structures in place, we can start filling in the main -`VkDeviceCreateInfo` structure. +앞선 두 구조체가 준비되었으니, 이제 메인 `VkDeviceCreateInfo` 구조체를 채워나갈 수 있습니다. ```c++ VkDeviceCreateInfo createInfo{}; createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; ``` -First add pointers to the queue creation info and device features structs: +먼저 큐 생성 정보와 장치 기능 구조체를 가리키는 포인터를 추가합니다. ```c++ createInfo.pQueueCreateInfos = &queueCreateInfo; @@ -90,17 +71,11 @@ createInfo.queueCreateInfoCount = 1; createInfo.pEnabledFeatures = &deviceFeatures; ``` -The remainder of the information bears a resemblance to the -`VkInstanceCreateInfo` struct and requires you to specify extensions and -validation layers. The difference is that these are device specific this time. +나머지 정보는 `VkInstanceCreateInfo` 구조체와 유사하며, 확장과 유효성 검사 레이어를 명시해야 합니다. 차이점은 이번에는 이것들이 장치에 한정된다는 점입니다. -An example of a device specific extension is `VK_KHR_swapchain`, which allows -you to present rendered images from that device to windows. It is possible that -there are Vulkan devices in the system that lack this ability, for example -because they only support compute operations. We will come back to this -extension in the swap chain chapter. +장치별 확장의 한 예로 `VK_KHR_swapchain`이 있습니다. 이 확장은 해당 장치에서 렌더링된 이미지를 창에 표시할 수 있게 해줍니다. 시스템에 있는 Vulkan 장치 중에는 이 기능이 없는 경우도 있을 수 있는데, 예를 들어 연산 작업만 지원하는 장치가 그렇습니다. 이 확장에 대해서는 스왑 체인 장에서 다시 다룰 것입니다. -Previous implementations of Vulkan made a distinction between instance and device specific validation layers, but this is [no longer the case](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap40.html#extendingvulkan-layers-devicelayerdeprecation). That means that the `enabledLayerCount` and `ppEnabledLayerNames` fields of `VkDeviceCreateInfo` are ignored by up-to-date implementations. However, it is still a good idea to set them anyway to be compatible with older implementations: +이전 Vulkan 구현에서는 인스턴스와 장치별 유효성 검사 레이어를 구분했지만, [이제는 그렇지 않습니다](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap40.html#extendingvulkan-layers-devicelayerdeprecation). 이는 최신 구현에서 `VkDeviceCreateInfo`의 `enabledLayerCount`와 `ppEnabledLayerNames` 필드가 무시된다는 의미입니다. 하지만 구버전 구현과의 호환성을 위해 여전히 설정해두는 것이 좋습니다. ```c++ createInfo.enabledExtensionCount = 0; @@ -113,10 +88,9 @@ if (enableValidationLayers) { } ``` -We won't need any device specific extensions for now. +지금은 장치별 확장이 필요하지 않습니다. -That's it, we're now ready to instantiate the logical device with a call to the -appropriately named `vkCreateDevice` function. +이제 모든 준비가 끝났습니다. 이름에 걸맞게 `vkCreateDevice` 함수를 호출하여 논리 장치를 인스턴스화할 준비가 되었습니다. ```c++ if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) { @@ -124,13 +98,9 @@ if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) } ``` -The parameters are the physical device to interface with, the queue and usage -info we just specified, the optional allocation callbacks pointer and a pointer -to a variable to store the logical device handle in. Similarly to the instance -creation function, this call can return errors based on enabling non-existent -extensions or specifying the desired usage of unsupported features. +파라미터는 각각 상호작용할 물리 장치, 방금 명시한 큐 및 사용 정보, 선택적인 할당 콜백 포인터, 그리고 논리 장치 핸들을 저장할 변수를 가리키는 포인터입니다. 인스턴스 생성 함수와 유사하게, 이 호출은 존재하지 않는 확장을 활성화하거나 지원되지 않는 기능의 사용을 명시하는 경우 오류를 반환할 수 있습니다. -The device should be destroyed in `cleanup` with the `vkDestroyDevice` function: +장치는 `cleanup`에서 `vkDestroyDevice` 함수로 소멸시켜야 합니다. ```c++ void cleanup() { @@ -139,33 +109,24 @@ void cleanup() { } ``` -Logical devices don't interact directly with instances, which is why it's not -included as a parameter. +논리 장치는 인스턴스와 직접적으로 상호작용하지 않기 때문에, 파라미터로 포함되지 않습니다. -## Retrieving queue handles +## 큐 핸들 가져오기 -The queues are automatically created along with the logical device, but we don't -have a handle to interface with them yet. First add a class member to store a -handle to the graphics queue: +큐는 논리 장치와 함께 자동으로 생성되지만, 아직 큐와 상호작용할 핸들을 가지고 있지는 않습니다. 먼저 클래스 멤버를 추가하여 그래픽스 큐의 핸들을 저장하도록 합시다. ```c++ VkQueue graphicsQueue; ``` -Device queues are implicitly cleaned up when the device is destroyed, so we -don't need to do anything in `cleanup`. +장치 큐는 장치가 소멸될 때 암시적으로 정리되므로, `cleanup`에서 따로 처리할 필요가 없습니다. -We can use the `vkGetDeviceQueue` function to retrieve queue handles for each -queue family. The parameters are the logical device, queue family, queue index -and a pointer to the variable to store the queue handle in. Because we're only -creating a single queue from this family, we'll simply use index `0`. +`vkGetDeviceQueue` 함수를 사용하여 각 큐 패밀리에 대한 큐 핸들을 가져올 수 있습니다. 파라미터는 논리 장치, 큐 패밀리, 큐 인덱스, 그리고 큐 핸들을 저장할 변수를 가리키는 포인터입니다. 이 패밀리에서는 큐를 하나만 생성하므로, 인덱스는 간단히 `0`을 사용합니다. ```c++ vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue); ``` -With the logical device and queue handles we can now actually start using the -graphics card to do things! In the next few chapters we'll set up the resources -to present results to the window system. +이제 논리 장치와 큐 핸들을 사용해 그래픽 카드로 무언가를 실제로 시작할 수 있습니다! 다음 몇 개의 장에 걸쳐, 결과를 창 시스템에 표시하기 위한 리소스를 설정해 보겠습니다. -[C++ code](/code/04_logical_device.cpp) +[C++ 코드](/code/04_logical_device.cpp) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md b/ko/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md index 966a8946..ce23effa 100644 --- a/ko/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md +++ b/ko/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md @@ -1,48 +1,22 @@ -Since Vulkan is a platform agnostic API, it can not interface directly with the -window system on its own. To establish the connection between Vulkan and the -window system to present results to the screen, we need to use the WSI (Window -System Integration) extensions. In this chapter we'll discuss the first one, -which is `VK_KHR_surface`. It exposes a `VkSurfaceKHR` object that represents an -abstract type of surface to present rendered images to. The surface in our -program will be backed by the window that we've already opened with GLFW. - -The `VK_KHR_surface` extension is an instance level extension and we've actually -already enabled it, because it's included in the list returned by -`glfwGetRequiredInstanceExtensions`. The list also includes some other WSI -extensions that we'll use in the next couple of chapters. - -The window surface needs to be created right after the instance creation, -because it can actually influence the physical device selection. The reason we -postponed this is because window surfaces are part of the larger topic of -render targets and presentation for which the explanation would have cluttered -the basic setup. It should also be noted that window surfaces are an entirely -optional component in Vulkan, if you just need off-screen rendering. Vulkan -allows you to do that without hacks like creating an invisible window -(necessary for OpenGL). - -## Window surface creation - -Start by adding a `surface` class member right below the debug callback. +Vulkan은 플랫폼에 종속되지 않는 API이므로, 그 자체만으로는 윈도우 시스템과 직접 상호작용할 수 없습니다. Vulkan과 윈도우 시스템을 연결하여 렌더링 결과를 화면에 표시하려면 WSI(Window System Integration) 확장을 사용해야 합니다. 이번 장에서는 그 첫 번째 확장인 `VK_KHR_surface`에 대해 논의하겠습니다. 이 확장은 렌더링된 이미지를 표시할 추상적인 유형의 표면을 나타내는 `VkSurfaceKHR` 객체를 제공합니다. 우리 프로그램의 서피스는 이미 GLFW로 열어 둔 창을 기반으로 생성될 것입니다. + +`VK_KHR_surface` 확장은 인스턴스 수준 확장(instance level extension)이며, `glfwGetRequiredInstanceExtensions`가 반환하는 목록에 포함되어 있으므로 우리는 이미 이 확장을 활성화했습니다. 이 목록에는 다음 몇 장에서 사용할 다른 WSI 확장들도 포함되어 있습니다. + +윈도우 서피스는 인스턴스 생성 직후에 만들어야 합니다. 왜냐하면 서피스는 물리 장치 선택에 영향을 줄 수 있기 때문입니다. 이 작업을 뒤로 미룬 이유는 윈도우 서피스가 렌더 타겟 및 프레젠테이션이라는 더 큰 주제의 일부이며, 이를 기본 설정 과정에서 설명하면 내용이 복잡해지기 때문입니다. 또한, 오프스크린 렌더링(off-screen rendering)만 필요한 경우 윈도우 서피스는 전적으로 선택적인 구성 요소라는 점에 유의해야 합니다. Vulkan을 사용하면 OpenGL에서 필요했던 보이지 않는 창을 만드는 것과 같은 꼼수 없이도 오프스크린 렌더링이 가능합니다. + +## 윈도우 서피스 생성하기 + +먼저 디버그 콜백 바로 아래에 `surface` 클래스 멤버를 추가합니다. ```c++ VkSurfaceKHR surface; ``` -Although the `VkSurfaceKHR` object and its usage is platform agnostic, its -creation isn't because it depends on window system details. For example, it -needs the `HWND` and `HMODULE` handles on Windows. Therefore there is a -platform-specific addition to the extension, which on Windows is called -`VK_KHR_win32_surface` and is also automatically included in the list from -`glfwGetRequiredInstanceExtensions`. +`VkSurfaceKHR` 객체와 그 사용법은 플랫폼에 독립적이지만, 그 생성 과정은 윈도우 시스템의 세부 사항에 의존하기 때문에 플랫폼 종속적입니다. 예를 들어, Windows에서는 `HWND`와 `HMODULE` 핸들이 필요합니다. 따라서 플랫폼별 추가 확장이 있으며, Windows에서는 이를 `VK_KHR_win32_surface`라고 부릅니다. 이 확장 역시 `glfwGetRequiredInstanceExtensions`가 반환하는 목록에 자동으로 포함됩니다. -I will demonstrate how this platform specific extension can be used to create a -surface on Windows, but we won't actually use it in this tutorial. It doesn't -make any sense to use a library like GLFW and then proceed to use -platform-specific code anyway. GLFW actually has `glfwCreateWindowSurface` that -handles the platform differences for us. Still, it's good to see what it does -behind the scenes before we start relying on it. +이 플랫폼별 확장을 사용하여 Windows에서 서피스를 만드는 방법을 보여드리겠지만, 이 튜토리얼에서 실제로 사용하지는 않을 것입니다. GLFW와 같은 라이브러리를 사용하면서 플랫폼 종속적인 코드를 사용하는 것은 이치에 맞지 않기 때문입니다. 사실 GLFW에는 `glfwCreateWindowSurface`라는 함수가 있어 플랫폼 간의 차이점을 알아서 처리해줍니다. 그럼에도 불구하고, GLFW에 의존하기 전에 내부적으로 어떤 작업이 이루어지는지 살펴보는 것은 좋은 경험이 될 것입니다. -To access native platform functions, you need to update the includes at the top: +네이티브 플랫폼 함수에 접근하려면 상단의 include 구문을 다음과 같이 수정해야 합니다. ```c++ #define VK_USE_PLATFORM_WIN32_KHR @@ -52,10 +26,7 @@ To access native platform functions, you need to update the includes at the top: #include ``` -Because a window surface is a Vulkan object, it comes with a -`VkWin32SurfaceCreateInfoKHR` struct that needs to be filled in. It has two -important parameters: `hwnd` and `hinstance`. These are the handles to the -window and the process. +윈도우 서피스는 Vulkan 객체이므로, `VkWin32SurfaceCreateInfoKHR` 구조체를 채워야 합니다. 여기에는 `hwnd`와 `hinstance`라는 두 가지 중요한 매개변수가 있습니다. 이들은 각각 창과 프로세스에 대한 핸들입니다. ```c++ VkWin32SurfaceCreateInfoKHR createInfo{}; @@ -64,11 +35,9 @@ createInfo.hwnd = glfwGetWin32Window(window); createInfo.hinstance = GetModuleHandle(nullptr); ``` -The `glfwGetWin32Window` function is used to get the raw `HWND` from the GLFW -window object. The `GetModuleHandle` call returns the `HINSTANCE` handle of the -current process. +`glfwGetWin32Window` 함수는 GLFW 윈도우 객체로부터 원시 `HWND`를 가져오는 데 사용됩니다. `GetModuleHandle` 호출은 현재 프로세스의 `HINSTANCE` 핸들을 반환합니다. -After that the surface can be created with `vkCreateWin32SurfaceKHR`, which includes a parameter for the instance, surface creation details, custom allocators and the variable for the surface handle to be stored in. Technically this is a WSI extension function, but it is so commonly used that the standard Vulkan loader includes it, so unlike other extensions you don't need to explicitly load it. +그 후 `vkCreateWin32SurfaceKHR` 함수로 서피스를 생성할 수 있습니다. 이 함수는 인스턴스, 서피스 생성 정보, 사용자 정의 할당자, 그리고 서피스 핸들을 저장할 변수를 매개변수로 받습니다. 기술적으로 이 함수는 WSI 확장 함수이지만 매우 보편적으로 사용되기 때문에 표준 Vulkan 로더에 포함되어 있습니다. 따라서 다른 확장과 달리 명시적으로 함수 포인터를 로드할 필요가 없습니다. ```c++ if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) { @@ -76,14 +45,9 @@ if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCC } ``` -The process is similar for other platforms like Linux, where -`vkCreateXcbSurfaceKHR` takes an XCB connection and window as creation details -with X11. +이 과정은 X11을 사용하는 리눅스와 같은 다른 플랫폼에서도 비슷합니다. 리눅스에서는 `vkCreateXcbSurfaceKHR` 함수가 XCB 연결과 윈도우를 생성 정보로 받습니다. -The `glfwCreateWindowSurface` function performs exactly this operation with a -different implementation for each platform. We'll now integrate it into our -program. Add a function `createSurface` to be called from `initVulkan` right -after instance creation and `setupDebugMessenger`. +`glfwCreateWindowSurface` 함수는 각 플랫폼에 맞춰 정확히 이 작업을 수행합니다. 이제 이 함수를 우리 프로그램에 통합해 보겠습니다. `initVulkan` 함수에서 인스턴스 생성과 `setupDebugMessenger` 호출 직후에 호출될 `createSurface` 함수를 추가합니다. ```c++ void initVulkan() { @@ -99,8 +63,7 @@ void createSurface() { } ``` -The GLFW call takes simple parameters instead of a struct which makes the -implementation of the function very straightforward: +GLFW 호출은 구조체 대신 간단한 매개변수를 받기 때문에 함수 구현이 매우 간단합니다. ```c++ void createSurface() { @@ -110,10 +73,7 @@ void createSurface() { } ``` -The parameters are the `VkInstance`, GLFW window pointer, custom allocators and -pointer to `VkSurfaceKHR` variable. It simply passes through the `VkResult` from -the relevant platform call. GLFW doesn't offer a special function for destroying -a surface, but that can easily be done through the original API: +매개변수는 `VkInstance`, GLFW 윈도우 포인터, 사용자 정의 할당자, 그리고 `VkSurfaceKHR` 변수를 가리키는 포인터입니다. 이 함수는 각 플랫폼에 맞는 생성 함수의 `VkResult`를 그대로 반환합니다. GLFW는 서피스 파괴를 위한 특별한 함수를 제공하지 않지만, 원래 Vulkan API를 통해 쉽게 처리할 수 있습니다. ```c++ void cleanup() { @@ -124,21 +84,13 @@ void cleanup() { } ``` -Make sure that the surface is destroyed before the instance. +서피스는 반드시 인스턴스보다 먼저 파괴되어야 한다는 점을 명심하십시오. -## Querying for presentation support +## 프레젠테이션 지원 여부 쿼리 -Although the Vulkan implementation may support window system integration, that -does not mean that every device in the system supports it. Therefore we need to -extend `isDeviceSuitable` to ensure that a device can present images to the -surface we created. Since the presentation is a queue-specific feature, the -problem is actually about finding a queue family that supports presenting to the -surface we created. +Vulkan 구현이 윈도우 시스템 통합을 지원하더라도, 시스템의 모든 장치가 이를 지원한다는 의미는 아닙니다. 따라서 `isDeviceSuitable` 함수를 확장하여 장치가 우리가 생성한 서피스로 이미지를 출력(present)할 수 있는지 확인해야 합니다. 프레젠테이션은 큐에 특화된 기능이므로, 이 문제는 결국 우리가 만든 서피스로의 프레젠테이션을 지원하는 큐 패밀리를 찾는 문제가 됩니다. -It's actually possible that the queue families supporting drawing commands and -the ones supporting presentation do not overlap. Therefore we have to take into -account that there could be a distinct presentation queue by modifying the -`QueueFamilyIndices` structure: +드로잉 커맨드를 지원하는 큐 패밀리와 프레젠테이션을 지원하는 큐 패밀리가 서로 겹치지 않을 수도 있습니다. 따라서 별도의 프레젠테이션 큐가 존재할 수 있다는 점을 고려하여 `QueueFamilyIndices` 구조체를 수정해야 합니다. ```c++ struct QueueFamilyIndices { @@ -151,19 +103,14 @@ struct QueueFamilyIndices { }; ``` -Next, we'll modify the `findQueueFamilies` function to look for a queue family -that has the capability of presenting to our window surface. The function to -check for that is `vkGetPhysicalDeviceSurfaceSupportKHR`, which takes the -physical device, queue family index and surface as parameters. Add a call to it -in the same loop as the `VK_QUEUE_GRAPHICS_BIT`: +다음으로, `findQueueFamilies` 함수를 수정하여 우리 윈도우 서피스로 프레젠테이션할 수 있는 큐 패밀리를 찾도록 합니다. 이를 확인하는 함수는 `vkGetPhysicalDeviceSurfaceSupportKHR`이며, 물리 장치, 큐 패밀리 인덱스, 서피스를 매개변수로 받습니다. `VK_QUEUE_GRAPHICS_BIT`를 확인하는 루프 안에서 이 함수를 호출합니다. ```c++ VkBool32 presentSupport = false; vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport); ``` -Then simply check the value of the boolean and store the presentation family -queue index: +그런 다음 이 불리언 값을 확인하고 프레젠테이션 큐 패밀리의 인덱스를 저장합니다. ```c++ if (presentSupport) { @@ -171,25 +118,17 @@ if (presentSupport) { } ``` -Note that it's very likely that these end up being the same queue family after -all, but throughout the program we will treat them as if they were separate -queues for a uniform approach. Nevertheless, you could add logic to explicitly -prefer a physical device that supports drawing and presentation in the same -queue for improved performance. +이 두 큐 패밀리가 결국 동일한 큐 패밀리로 귀결될 가능성이 매우 높지만, 이 프로그램 전반에 걸쳐 일관된 접근 방식을 위해 서로 다른 큐인 것처럼 다룰 것입니다. 그럼에도 불구하고, 성능 향상을 위해 드로잉과 프레젠테이션을 동일한 큐에서 지원하는 물리 장치를 명시적으로 선호하도록 로직을 추가할 수도 있습니다. -## Creating the presentation queue +## 프레젠테이션 큐 생성하기 -The one thing that remains is modifying the logical device creation procedure to -create the presentation queue and retrieve the `VkQueue` handle. Add a member -variable for the handle: +이제 남은 작업은 논리 장치 생성 절차를 수정하여 프레젠테이션 큐를 만들고 `VkQueue` 핸들을 가져오는 것입니다. 큐 핸들을 위한 멤버 변수를 추가합니다. ```c++ VkQueue presentQueue; ``` -Next, we need to have multiple `VkDeviceQueueCreateInfo` structs to create a -queue from both families. An elegant way to do that is to create a set of all -unique queue families that are necessary for the required queues: +다음으로, 두 큐 패밀리로부터 큐를 생성하기 위해 여러 개의 `VkDeviceQueueCreateInfo` 구조체가 필요할 수 있습니다. 이를 우아하게 처리하는 방법은 필요한 큐에 대해 모든 고유한 큐 패밀리의 집합(set)을 만드는 것입니다. ```c++ #include @@ -212,22 +151,19 @@ for (uint32_t queueFamily : uniqueQueueFamilies) { } ``` -And modify `VkDeviceCreateInfo` to point to the vector: +그리고 `VkDeviceCreateInfo`가 이 벡터를 가리키도록 수정합니다. ```c++ createInfo.queueCreateInfoCount = static_cast(queueCreateInfos.size()); createInfo.pQueueCreateInfos = queueCreateInfos.data(); ``` -If the queue families are the same, then we only need to pass its index once. -Finally, add a call to retrieve the queue handle: +만약 큐 패밀리가 동일하다면, 우리는 해당 인덱스를 한 번만 전달하게 됩니다. 마지막으로, 큐 핸들을 가져오는 호출을 추가합니다. ```c++ vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue); ``` -In case the queue families are the same, the two handles will most likely have -the same value now. In the next chapter we're going to look at swap chains and -how they give us the ability to present images to the surface. +만약 그래픽스 큐 패밀리와 프레젠테이션 큐 패밀리가 같다면, 두 큐 핸들(`graphicsQueue`와 `presentQueue`)은 이제 동일한 값을 가질 가능성이 높습니다. 다음 장에서는 스왑 체인(swap chain)에 대해 알아보고, 이를 통해 어떻게 서피스에 이미지를 출력할 수 있는지 살펴보겠습니다. -[C++ code](/code/05_window_surface.cpp) +[C++ 코드](/code/05_window_surface.cpp) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md b/ko/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md index f593b5a6..48e3c3c6 100644 --- a/ko/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md +++ b/ko/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md @@ -1,30 +1,14 @@ -Vulkan does not have the concept of a "default framebuffer", hence it requires an infrastructure that will own the buffers we will render to before we visualize them on the screen. This infrastructure is -known as the *swap chain* and must be created explicitly in Vulkan. The swap -chain is essentially a queue of images that are waiting to be presented to the -screen. Our application will acquire such an image to draw to it, and then -return it to the queue. How exactly the queue works and the conditions for -presenting an image from the queue depend on how the swap chain is set up, but -the general purpose of the swap chain is to synchronize the presentation of -images with the refresh rate of the screen. - -## Checking for swap chain support - -Not all graphics cards are capable of presenting images directly to a screen for -various reasons, for example because they are designed for servers and don't -have any display outputs. Secondly, since image presentation is heavily tied -into the window system and the surfaces associated with windows, it is not -actually part of the Vulkan core. You have to enable the `VK_KHR_swapchain` -device extension after querying for its support. - -For that purpose we'll first extend the `isDeviceSuitable` function to check if -this extension is supported. We've previously seen how to list the extensions -that are supported by a `VkPhysicalDevice`, so doing that should be fairly -straightforward. Note that the Vulkan header file provides a nice macro -`VK_KHR_SWAPCHAIN_EXTENSION_NAME` that is defined as `VK_KHR_swapchain`. The -advantage of using this macro is that the compiler will catch misspellings. - -First declare a list of required device extensions, similar to the list of -validation layers to enable. +Vulkan에는 '기본 프레임버퍼(default framebuffer)'라는 개념이 없습니다. 따라서 우리가 렌더링할 버퍼를 화면에 시각화하기 전에 소유할 인프라가 필요합니다. 이 인프라를 **스왑 체인(swap chain)** 이라고 하며, Vulkan에서는 명시적으로 생성해야 합니다. + +스왑 체인은 본질적으로 화면에 표시되기를 기다리는 이미지들의 큐(queue)입니다. 우리 애플리케이션은 렌더링할 이미지를 이 큐에서 가져와(acquire) 렌더링한 다음, 다시 큐에 반환합니다. 큐가 정확히 어떻게 작동하고 큐에서 이미지를 표시하는 조건은 스왑 체인 설정 방식에 따라 다르지만, 스왑 체인의 일반적인 목적은 이미지 표시를 화면의 주사율(refresh rate)과 동기화하는 것입니다. + +## 스왑 체인 지원 확인 + +모든 그래픽 카드가 이미지를 화면에 직접 표시할 수 있는 것은 아닙니다. 예를 들어 서버용으로 설계되어 디스플레이 출력이 없는 경우가 그렇습니다. 둘째로, 이미지 표시는 창 시스템(window system) 및 창과 관련된 표면(surface)과 밀접하게 연관되어 있으므로 실제 Vulkan 코어의 일부가 아닙니다. 따라서 `VK_KHR_swapchain` 장치 확장 기능의 지원 여부를 쿼리한 후 활성화해야 합니다. + +이를 위해 먼저 `isDeviceSuitable` 함수를 확장하여 이 확장이 지원되는지 확인합니다. 이전에 `VkPhysicalDevice`가 지원하는 확장 기능을 나열하는 방법을 보았으므로, 이 작업은 매우 간단할 것입니다. Vulkan 헤더 파일은 `VK_KHR_swapchain`으로 정의된 `VK_KHR_SWAPCHAIN_EXTENSION_NAME`이라는 멋진 매크로를 제공합니다. 이 매크로를 사용하면 컴파일러가 오타를 잡아낼 수 있다는 장점이 있습니다. + +먼저, 활성화할 유효성 검사 레이어 목록과 유사하게 필요한 장치 확장 기능 목록을 선언합니다. ```c++ const std::vector deviceExtensions = { @@ -32,8 +16,7 @@ const std::vector deviceExtensions = { }; ``` -Next, create a new function `checkDeviceExtensionSupport` that is called from -`isDeviceSuitable` as an additional check: +다음으로, `isDeviceSuitable`에서 추가 검사로 호출될 새로운 함수 `checkDeviceExtensionSupport`를 만듭니다. ```c++ bool isDeviceSuitable(VkPhysicalDevice device) { @@ -49,8 +32,7 @@ bool checkDeviceExtensionSupport(VkPhysicalDevice device) { } ``` -Modify the body of the function to enumerate the extensions and check if all of -the required extensions are amongst them. +이제 함수의 본문을 수정하여 확장 기능을 열거하고 필요한 모든 확장이 그 안에 포함되어 있는지 확인합니다. ```c++ bool checkDeviceExtensionSupport(VkPhysicalDevice device) { @@ -70,46 +52,30 @@ bool checkDeviceExtensionSupport(VkPhysicalDevice device) { } ``` -I've chosen to use a set of strings here to represent the unconfirmed required -extensions. That way we can easily tick them off while enumerating the sequence -of available extensions. Of course you can also use a nested loop like in -`checkValidationLayerSupport`. The performance difference is irrelevant. Now run -the code and verify that your graphics card is indeed capable of creating a -swap chain. It should be noted that the availability of a presentation queue, -as we checked in the previous chapter, implies that the swap chain extension -must be supported. However, it's still good to be explicit about things, and -the extension does have to be explicitly enabled. +저는 여기서 아직 확인되지 않은 필수 확장 기능들을 표현하기 위해 문자열 집합(set)을 사용했습니다. 이렇게 하면 사용 가능한 확장 기능 시퀀스를 열거하면서 쉽게 하나씩 지워나갈(tick off) 수 있습니다. 물론 `checkValidationLayerSupport`에서처럼 중첩 루프를 사용할 수도 있습니다. 성능 차이는 무시할 수 있는 수준입니다. 이제 코드를 실행하여 그래픽 카드가 실제로 스왑 체인을 생성할 수 있는지 확인하십시오. 이전 장에서 확인했던 표현 큐(presentation queue)의 가용성은 스왑 체인 확장이 지원되어야 함을 의미합니다. 하지만, 여전히 명시적으로 확인하는 것이 좋으며, 확장은 명시적으로 활성화되어야 합니다. -## Enabling device extensions +## 장치 확장 기능 활성화 -Using a swapchain requires enabling the `VK_KHR_swapchain` extension first. -Enabling the extension just requires a small change to the logical device -creation structure: +스왑 체인을 사용하려면 먼저 `VK_KHR_swapchain` 확장을 활성화해야 합니다. 확장 기능을 활성화하려면 논리 장치 생성 구조체를 약간 변경하면 됩니다. ```c++ createInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); createInfo.ppEnabledExtensionNames = deviceExtensions.data(); ``` -Make sure to replace the existing line `createInfo.enabledExtensionCount = 0;` when you do so. +이 코드를 추가할 때 기존의 `createInfo.enabledExtensionCount = 0;` 줄을 교체해야 합니다. -## Querying details of swap chain support +## 스왑 체인 지원 상세 정보 쿼리 -Just checking if a swap chain is available is not sufficient, because it may not -actually be compatible with our window surface. Creating a swap chain also -involves a lot more settings than instance and device creation, so we need to -query for some more details before we're able to proceed. +스왑 체인이 사용 가능한지 확인하는 것만으로는 충분하지 않습니다. 스왑 체인이 우리 창 표면(window surface)과 호환되지 않을 수 있기 때문입니다. 또한 스왑 체인 생성에는 인스턴스 및 장치 생성보다 훨씬 더 많은 설정이 포함되므로, 계속 진행하기 전에 몇 가지 세부 정보를 더 쿼리해야 합니다. -There are basically three kinds of properties we need to check: +기본적으로 세 가지 종류의 속성을 확인해야 합니다. -* Basic surface capabilities (min/max number of images in swap chain, min/max -width and height of images) -* Surface formats (pixel format, color space) -* Available presentation modes +* 기본 표면 기능 (스왑 체인의 최소/최대 이미지 수, 이미지의 최소/최대 너비 및 높이) +* 표면 형식 (픽셀 형식, 색 공간) +* 사용 가능한 표현 모드 -Similar to `findQueueFamilies`, we'll use a struct to pass these details around -once they've been queried. The three aforementioned types of properties come in -the form of the following structs and lists of structs: +`findQueueFamilies`와 유사하게, 이러한 세부 정보가 쿼리되면 구조체를 사용하여 전달할 것입니다. 위에서 언급한 세 가지 속성 유형은 다음 구조체 및 구조체 목록의 형태로 제공됩니다. ```c++ struct SwapChainSupportDetails { @@ -119,8 +85,7 @@ struct SwapChainSupportDetails { }; ``` -We'll now create a new function `querySwapChainSupport` that will populate this -struct. +이제 이 구조체를 채울 새로운 함수 `querySwapChainSupport`를 만들겠습니다. ```c++ SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) { @@ -130,24 +95,17 @@ SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) { } ``` -This section covers how to query the structs that include this information. The -meaning of these structs and exactly which data they contain is discussed in the -next section. +이 섹션에서는 이 정보를 포함하는 구조체를 쿼리하는 방법을 다룹니다. 이 구조체의 의미와 정확히 어떤 데이터가 포함되어 있는지는 다음 섹션에서 논의합니다. -Let's start with the basic surface capabilities. These properties are simple to -query and are returned into a single `VkSurfaceCapabilitiesKHR` struct. +먼저 기본 표면 기능부터 시작하겠습니다. 이러한 속성은 쿼리하기 간단하며 단일 `VkSurfaceCapabilitiesKHR` 구조체로 반환됩니다. ```c++ vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities); ``` -This function takes the specified `VkPhysicalDevice` and `VkSurfaceKHR` window -surface into account when determining the supported capabilities. All of the -support querying functions have these two as first parameters because they are -the core components of the swap chain. +이 함수는 지원되는 기능을 결정할 때 지정된 `VkPhysicalDevice`와 `VkSurfaceKHR` 창 표면을 고려합니다. 모든 지원 쿼리 함수는 이 두 가지를 첫 번째 매개변수로 사용하는데, 이들이 스왑 체인의 핵심 구성 요소이기 때문입니다. -The next step is about querying the supported surface formats. Because this is a -list of structs, it follows the familiar ritual of 2 function calls: +다음 단계는 지원되는 표면 형식을 쿼리하는 것입니다. 이것은 구조체 목록이므로, 두 번의 함수 호출이라는 익숙한 절차를 따릅니다. ```c++ uint32_t formatCount; @@ -159,9 +117,7 @@ if (formatCount != 0) { } ``` -Make sure that the vector is resized to hold all the available formats. And -finally, querying the supported presentation modes works exactly the same way -with `vkGetPhysicalDeviceSurfacePresentModesKHR`: +벡터가 사용 가능한 모든 형식을 담을 수 있도록 크기가 조정되었는지 확인하십시오. 마지막으로, 지원되는 표현 모드를 쿼리하는 것도 `vkGetPhysicalDeviceSurfacePresentModesKHR`를 사용하여 정확히 동일한 방식으로 작동합니다. ```c++ uint32_t presentModeCount; @@ -173,11 +129,7 @@ if (presentModeCount != 0) { } ``` -All of the details are in the struct now, so let's extend `isDeviceSuitable` -once more to utilize this function to verify that swap chain support is -adequate. Swap chain support is sufficient for this tutorial if there is at -least one supported image format and one supported presentation mode given the -window surface we have. +이제 모든 세부 정보가 구조체에 담겼습니다. `isDeviceSuitable`을 한 번 더 확장하여 이 함수를 활용해 스왑 체인 지원이 적절한지 확인합시다. 이 튜토리얼에서는 우리가 가진 창 표면을 고려할 때, 지원되는 이미지 형식이 하나 이상이고 지원되는 표현 모드가 하나 이상이면 스왑 체인 지원은 충분하다고 봅니다. ```c++ bool swapChainAdequate = false; @@ -187,32 +139,25 @@ if (extensionsSupported) { } ``` -It is important that we only try to query for swap chain support after verifying -that the extension is available. The last line of the function changes to: +확장 기능이 사용 가능한지 확인한 후에만 스왑 체인 지원을 쿼리하는 것이 중요합니다. 함수의 마지막 줄은 다음과 같이 변경됩니다. ```c++ return indices.isComplete() && extensionsSupported && swapChainAdequate; ``` -## Choosing the right settings for the swap chain +## 스왑 체인에 적합한 설정 선택하기 -If the `swapChainAdequate` conditions were met then the support is definitely -sufficient, but there may still be many different modes of varying optimality. -We'll now write a couple of functions to find the right settings for the best -possible swap chain. There are three types of settings to determine: +`swapChainAdequate` 조건이 충족되면 지원은 확실히 충분하지만, 최적성이 다양한 여러 모드가 있을 수 있습니다. 이제 최상의 스왑 체인을 위한 올바른 설정을 찾는 몇 가지 함수를 작성해 보겠습니다. 결정해야 할 설정에는 세 가지 유형이 있습니다. -* Surface format (color depth) -* Presentation mode (conditions for "swapping" images to the screen) -* Swap extent (resolution of images in swap chain) +* 표면 형식 (색상 깊이) +* 표현 모드 (이미지를 화면으로 "스왑"하는 조건) +* 스왑 범위 (스왑 체인 이미지의 해상도) -For each of these settings we'll have an ideal value in mind that we'll go with -if it's available and otherwise we'll create some logic to find the next best -thing. +각 설정에 대해 이상적인 값을 염두에 두고, 사용 가능하다면 그 값을 사용하고, 그렇지 않다면 차선책을 찾는 로직을 만들 것입니다. -### Surface format +### 표면 형식 -The function for this setting starts out like this. We'll later pass the -`formats` member of the `SwapChainSupportDetails` struct as argument. +이 설정을 위한 함수는 다음과 같이 시작합니다. 나중에 `SwapChainSupportDetails` 구조체의 `formats` 멤버를 인자로 전달할 것입니다. ```c++ VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { @@ -220,18 +165,11 @@ VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector } ``` -Each `VkSurfaceFormatKHR` entry contains a `format` and a `colorSpace` member. The -`format` member specifies the color channels and types. For example, -`VK_FORMAT_B8G8R8A8_SRGB` means that we store the B, G, R and alpha channels in -that order with an 8 bit unsigned integer for a total of 32 bits per pixel. The -`colorSpace` member indicates if the SRGB color space is supported or not using -the `VK_COLOR_SPACE_SRGB_NONLINEAR_KHR` flag. Note that this flag used to be -called `VK_COLORSPACE_SRGB_NONLINEAR_KHR` in old versions of the specification. +각 `VkSurfaceFormatKHR` 항목에는 `format`과 `colorSpace` 멤버가 포함됩니다. `format` 멤버는 색상 채널과 유형을 지정합니다. 예를 들어, `VK_FORMAT_B8G8R8A8_SRGB`는 B, G, R, 알파 채널을 픽셀당 총 32비트의 8비트 부호 없는 정수로 저장한다는 의미입니다. `colorSpace` 멤버는 `VK_COLOR_SPACE_SRGB_NONLINEAR_KHR` 플래그를 사용하여 SRGB 색 공간이 지원되는지 여부를 나타냅니다. 이 플래그는 이전 버전의 사양에서는 `VK_COLORSPACE_SRGB_NONLINEAR_KHR`로 불렸습니다. -For the color space we'll use SRGB if it is available, because it [results in more accurate perceived colors](http://stackoverflow.com/questions/12524623/). It is also pretty much the standard color space for images, like the textures we'll use later on. -Because of that we should also use an SRGB color format, of which one of the most common ones is `VK_FORMAT_B8G8R8A8_SRGB`. +색 공간으로는 SRGB를 사용할 것입니다. [더 정확하게 인식되는 색상을 표현해주기](http://stackoverflow.com/questions/12524623/) 때문입니다. 또한 나중에 사용할 텍스처와 같은 이미지의 표준 색 공간이기도 합니다. 따라서 `VK_FORMAT_B8G8R8A8_SRGB`와 같은 SRGB 색상 형식을 사용하는 것이 좋습니다. -Let's go through the list and see if the preferred combination is available: +목록을 살펴보고 선호하는 조합이 사용 가능한지 확인해 봅시다. ```c++ for (const auto& availableFormat : availableFormats) { @@ -241,9 +179,7 @@ for (const auto& availableFormat : availableFormats) { } ``` -If that also fails then we could start ranking the available formats based on -how "good" they are, but in most cases it's okay to just settle with the first -format that is specified. +이 방법이 실패한다면 사용 가능한 형식들의 "좋음" 정도에 따라 순위를 매길 수도 있지만, 대부분의 경우 목록의 첫 번째 형식을 사용하는 것으로도 충분합니다. ```c++ VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { @@ -257,31 +193,16 @@ VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector } ``` -### Presentation mode +### 표현 모드 -The presentation mode is arguably the most important setting for the swap chain, -because it represents the actual conditions for showing images to the screen. -There are four possible modes available in Vulkan: +표현 모드는 스왑 체인에서 가장 중요한 설정이라고 할 수 있습니다. 이미지를 화면에 보여주는 실제 조건을 나타내기 때문입니다. Vulkan에는 네 가지 가능한 모드가 있습니다. -* `VK_PRESENT_MODE_IMMEDIATE_KHR`: Images submitted by your application are -transferred to the screen right away, which may result in tearing. -* `VK_PRESENT_MODE_FIFO_KHR`: The swap chain is a queue where the display takes -an image from the front of the queue when the display is refreshed and the -program inserts rendered images at the back of the queue. If the queue is full -then the program has to wait. This is most similar to vertical sync as found in -modern games. The moment that the display is refreshed is known as "vertical -blank". -* `VK_PRESENT_MODE_FIFO_RELAXED_KHR`: This mode only differs from the previous -one if the application is late and the queue was empty at the last vertical -blank. Instead of waiting for the next vertical blank, the image is transferred -right away when it finally arrives. This may result in visible tearing. -* `VK_PRESENT_MODE_MAILBOX_KHR`: This is another variation of the second mode. -Instead of blocking the application when the queue is full, the images that are -already queued are simply replaced with the newer ones. This mode can be used to -render frames as fast as possible while still avoiding tearing, resulting in fewer latency issues than standard vertical sync. This is commonly known as "triple buffering", although the existence of three buffers alone does not necessarily mean that the framerate is unlocked. +* `VK_PRESENT_MODE_IMMEDIATE_KHR`: 애플리케이션이 제출한 이미지가 즉시 화면으로 전송되어 티어링(tearing)이 발생할 수 있습니다. +* `VK_PRESENT_MODE_FIFO_KHR`: 스왑 체인은 큐이며, 디스플레이가 새로 고쳐질 때 큐의 앞에서 이미지를 가져가고 프로그램은 렌더링된 이미지를 큐의 뒤에 삽입합니다. 큐가 가득 차면 프로그램은 기다려야 합니다. 이는 현대 게임에서 볼 수 있는 수직 동기화(vertical sync)와 가장 유사합니다. 디스플레이가 새로 고쳐지는 순간을 "수직 블랭크(vertical blank)"라고 합니다. +* `VK_PRESENT_MODE_FIFO_RELAXED_KHR`: 이 모드는 애플리케이션이 늦어져 마지막 수직 블랭크 시점에 큐가 비어 있었을 경우에만 이전 모드와 다릅니다. 다음 수직 블랭크를 기다리는 대신, 이미지가 도착하는 즉시 전송됩니다. 이로 인해 눈에 보이는 티어링이 발생할 수 있습니다. +* `VK_PRESENT_MODE_MAILBOX_KHR`: 두 번째 모드의 또 다른 변형입니다. 큐가 가득 찼을 때 애플리케이션을 차단하는 대신, 이미 큐에 있는 이미지들을 단순히 새 이미지로 교체합니다. 이 모드는 티어링을 피하면서 가능한 한 빨리 프레임을 렌더링하여 표준 수직 동기화보다 지연 시간 문제를 줄일 수 있습니다. 이는 일반적으로 "삼중 버퍼링(triple buffering)"으로 알려져 있지만, 세 개의 버퍼가 존재한다고 해서 반드시 프레임 속도가 제한되지 않는다는 의미는 아닙니다. -Only the `VK_PRESENT_MODE_FIFO_KHR` mode is guaranteed to be available, so we'll -again have to write a function that looks for the best mode that is available: +`VK_PRESENT_MODE_FIFO_KHR` 모드만 보장되므로, 사용 가능한 최상의 모드를 찾는 함수를 다시 작성해야 합니다. ```c++ VkPresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { @@ -289,7 +210,7 @@ VkPresentModeKHR chooseSwapPresentMode(const std::vector& avai } ``` -I personally think that `VK_PRESENT_MODE_MAILBOX_KHR` is a very nice trade-off if energy usage is not a concern. It allows us to avoid tearing while still maintaining a fairly low latency by rendering new images that are as up-to-date as possible right until the vertical blank. On mobile devices, where energy usage is more important, you will probably want to use `VK_PRESENT_MODE_FIFO_KHR` instead. Now, let's look through the list to see if `VK_PRESENT_MODE_MAILBOX_KHR` is available: +개인적으로 에너지 사용량이 문제가 되지 않는다면 `VK_PRESENT_MODE_MAILBOX_KHR`가 매우 좋은 절충안이라고 생각합니다. 수직 블랭크 직전까지 가능한 한 최신 이미지를 렌더링하여 티어링을 피하면서도 상당히 낮은 지연 시간을 유지할 수 있습니다. 에너지 사용량이 더 중요한 모바일 장치에서는 `VK_PRESENT_MODE_FIFO_KHR`를 사용하는 것이 좋습니다. 이제 목록을 살펴보고 `VK_PRESENT_MODE_MAILBOX_KHR`가 사용 가능한지 확인해 봅시다. ```c++ VkPresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { @@ -303,9 +224,9 @@ VkPresentModeKHR chooseSwapPresentMode(const std::vector& avai } ``` -### Swap extent +### 스왑 범위 -That leaves only one major property, for which we'll add one last function: +이제 마지막 주요 속성 하나가 남았으며, 이를 위해 마지막 함수를 하나 더 추가하겠습니다. ```c++ VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) { @@ -313,34 +234,14 @@ VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) { } ``` -The swap extent is the resolution of the swap chain images and it's almost -always exactly equal to the resolution of the window that we're drawing to _in -pixels_ (more on that in a moment). The range of the possible resolutions is -defined in the `VkSurfaceCapabilitiesKHR` structure. Vulkan tells us to match -the resolution of the window by setting the width and height in the -`currentExtent` member. However, some window managers do allow us to differ here -and this is indicated by setting the width and height in `currentExtent` to a -special value: the maximum value of `uint32_t`. In that case we'll pick the -resolution that best matches the window within the `minImageExtent` and -`maxImageExtent` bounds. But we must specify the resolution in the correct unit. - -GLFW uses two units when measuring sizes: pixels and -[screen coordinates](https://www.glfw.org/docs/latest/intro_guide.html#coordinate_systems). -For example, the resolution `{WIDTH, HEIGHT}` that we specified earlier when -creating the window is measured in screen coordinates. But Vulkan works with -pixels, so the swap chain extent must be specified in pixels as well. -Unfortunately, if you are using a high DPI display (like Apple's Retina -display), screen coordinates don't correspond to pixels. Instead, due to the -higher pixel density, the resolution of the window in pixel will be larger than -the resolution in screen coordinates. So if Vulkan doesn't fix the swap extent -for us, we can't just use the original `{WIDTH, HEIGHT}`. Instead, we must use -`glfwGetFramebufferSize` to query the resolution of the window in pixel before -matching it against the minimum and maximum image extent. - -```c++ -#include // Necessary for uint32_t -#include // Necessary for std::numeric_limits -#include // Necessary for std::clamp +스왑 범위(swap extent)는 스왑 체인 이미지의 해상도이며, 우리가 렌더링할 창의 해상도와 거의 항상 정확히 일치합니다(단위: _픽셀_). 가능한 해상도의 범위는 `VkSurfaceCapabilitiesKHR` 구조체에 정의되어 있습니다. Vulkan은 `currentExtent` 멤버의 너비와 높이를 설정하여 창의 해상도와 일치시키라고 알려줍니다. 그러나 일부 창 관리자는 여기서 다르게 설정하는 것을 허용하며, 이는 `currentExtent`의 너비와 높이를 특별한 값, 즉 `uint32_t`의 최대값으로 설정하여 표시됩니다. 이 경우 `minImageExtent`와 `maxImageExtent` 범위 내에서 창과 가장 잘 맞는 해상도를 선택합니다. 하지만 해상도를 올바른 단위로 지정해야 합니다. + +GLFW는 크기를 측정할 때 픽셀과 [화면 좌표(screen coordinates)](https://www.glfw.org/docs/latest/intro_guide.html#coordinate_systems)라는 두 가지 단위를 사용합니다. 예를 들어, 이전에 창을 생성할 때 지정한 해상도 `{WIDTH, HEIGHT}`는 화면 좌표로 측정됩니다. 하지만 Vulkan은 픽셀 단위로 작동하므로 스왑 체인 범위도 픽셀로 지정해야 합니다. 안타깝게도 고해상도 디스플레이(high DPI display, 예: Apple의 Retina 디스플레이)를 사용하는 경우 화면 좌표는 픽셀과 일치하지 않습니다. 대신, 더 높은 픽셀 밀도로 인해 창의 해상도(픽셀 단위)가 화면 좌표 단위의 해상도보다 더 큽니다. 따라서 Vulkan이 스왑 범위를 고정해주지 않으면 원래의 `{WIDTH, HEIGHT}`를 그대로 사용할 수 없습니다. 대신, `glfwGetFramebufferSize`를 사용하여 창의 해상도를 픽셀 단위로 쿼리한 다음, 최소 및 최대 이미지 범위와 비교해야 합니다. + +```c++ +#include // uint32_t에 필요 +#include // std::numeric_limits에 필요 +#include // std::clamp에 필요 ... @@ -364,16 +265,13 @@ VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) { } ``` -The `clamp` function is used here to bound the values of `width` and `height` between the allowed minimum and maximum extents that are supported by the implementation. +여기서 `clamp` 함수는 `width`와 `height` 값을 구현체에서 지원하는 허용된 최소 및 최대 범위 사이로 제한하는 데 사용됩니다. -## Creating the swap chain +## 스왑 체인 생성 -Now that we have all of these helper functions assisting us with the choices we -have to make at runtime, we finally have all the information that is needed to -create a working swap chain. +이제 런타임에 내려야 할 선택을 도와주는 모든 헬퍼 함수를 갖추었으므로, 마침내 작동하는 스왑 체인을 만드는 데 필요한 모든 정보를 갖게 되었습니다. -Create a `createSwapChain` function that starts out with the results of these -calls and make sure to call it from `initVulkan` after logical device creation. +이러한 호출 결과를 가지고 시작하는 `createSwapChain` 함수를 만들고, 논리 장치를 생성한 후 `initVulkan`에서 이 함수를 호출하도록 합니다. ```c++ void initVulkan() { @@ -394,19 +292,19 @@ void createSwapChain() { } ``` -Aside from these properties we also have to decide how many images we would like to have in the swap chain. The implementation specifies the minimum number that it requires to function: +이러한 속성 외에도 스왑 체인에 몇 개의 이미지를 가질지 결정해야 합니다. 구현체는 작동에 필요한 최소 이미지 수를 지정합니다. ```c++ uint32_t imageCount = swapChainSupport.capabilities.minImageCount; ``` -However, simply sticking to this minimum means that we may sometimes have to wait on the driver to complete internal operations before we can acquire another image to render to. Therefore it is recommended to request at least one more image than the minimum: +그러나 이 최소값에만 머무르면, 렌더링할 다른 이미지를 얻기 전에 드라이버가 내부 작업을 완료할 때까지 기다려야 할 수 있습니다. 따라서 최소값보다 최소 하나 이상의 이미지를 요청하는 것이 좋습니다. ```c++ uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1; ``` -We should also make sure to not exceed the maximum number of images while doing this, where `0` is a special value that means that there is no maximum: +또한 이 작업을 수행하는 동안 최대 이미지 수를 초과하지 않도록 해야 합니다. 여기서 `0`은 최대값이 없음을 의미하는 특별한 값입니다. ```c++ if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) { @@ -414,8 +312,7 @@ if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSup } ``` -As is tradition with Vulkan objects, creating the swap chain object requires -filling in a large structure. It starts out very familiarly: +Vulkan 객체의 전통처럼, 스왑 체인 객체를 생성하려면 큰 구조체를 채워야 합니다. 시작은 매우 익숙합니다. ```c++ VkSwapchainCreateInfoKHR createInfo{}; @@ -423,8 +320,7 @@ createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; createInfo.surface = surface; ``` -After specifying which surface the swap chain should be tied to, the details of -the swap chain images are specified: +스왑 체인이 연결될 표면을 지정한 후, 스왑 체인 이미지의 세부 정보를 지정합니다. ```c++ createInfo.minImageCount = imageCount; @@ -435,15 +331,7 @@ createInfo.imageArrayLayers = 1; createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; ``` -The `imageArrayLayers` specifies the amount of layers each image consists of. -This is always `1` unless you are developing a stereoscopic 3D application. The -`imageUsage` bit field specifies what kind of operations we'll use the images in -the swap chain for. In this tutorial we're going to render directly to them, -which means that they're used as color attachment. It is also possible that -you'll render images to a separate image first to perform operations like -post-processing. In that case you may use a value like -`VK_IMAGE_USAGE_TRANSFER_DST_BIT` instead and use a memory operation to transfer -the rendered image to a swap chain image. +`imageArrayLayers`는 각 이미지가 구성되는 레이어의 양을 지정합니다. 입체 3D 애플리케이션을 개발하지 않는 한 항상 `1`입니다. `imageUsage` 비트 필드는 스왑 체인의 이미지를 어떤 종류의 작업에 사용할지를 지정합니다. 이 튜토리얼에서는 이미지에 직접 렌더링할 것이므로, 색상 첨부 파일(color attachment)로 사용됨을 의미합니다. 후처리(post-processing)와 같은 작업을 수행하기 위해 이미지를 별도의 이미지에 먼저 렌더링할 수도 있습니다. 이 경우 `VK_IMAGE_USAGE_TRANSFER_DST_BIT`와 같은 값을 사용하고 메모리 연산을 통해 렌더링된 이미지를 스왑 체인 이미지로 전송할 수 있습니다. ```c++ QueueFamilyIndices indices = findQueueFamilies(physicalDevice); @@ -455,80 +343,50 @@ if (indices.graphicsFamily != indices.presentFamily) { createInfo.pQueueFamilyIndices = queueFamilyIndices; } else { createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; - createInfo.queueFamilyIndexCount = 0; // Optional - createInfo.pQueueFamilyIndices = nullptr; // Optional + createInfo.queueFamilyIndexCount = 0; // 선택 사항 + createInfo.pQueueFamilyIndices = nullptr; // 선택 사항 } ``` -Next, we need to specify how to handle swap chain images that will be used -across multiple queue families. That will be the case in our application if the -graphics queue family is different from the presentation queue. We'll be drawing -on the images in the swap chain from the graphics queue and then submitting them -on the presentation queue. There are two ways to handle images that are -accessed from multiple queues: +다음으로, 여러 큐 패밀리에서 사용될 스왑 체인 이미지를 처리하는 방법을 지정해야 합니다. 우리 애플리케이션에서는 그래픽 큐 패밀리가 표현 큐와 다른 경우에 해당됩니다. 우리는 그래픽 큐에서 스왑 체인의 이미지에 렌더링한 다음 표현 큐에 제출할 것입니다. 여러 큐에서 접근하는 이미지를 처리하는 방법에는 두 가지가 있습니다. -* `VK_SHARING_MODE_EXCLUSIVE`: An image is owned by one queue family at a time -and ownership must be explicitly transferred before using it in another queue -family. This option offers the best performance. -* `VK_SHARING_MODE_CONCURRENT`: Images can be used across multiple queue -families without explicit ownership transfers. +* `VK_SHARING_MODE_EXCLUSIVE`: 이미지는 한 번에 하나의 큐 패밀리에 의해 소유되며, 다른 큐 패밀리에서 사용하기 전에 명시적으로 소유권을 이전해야 합니다. 이 옵션은 최고의 성능을 제공합니다. +* `VK_SHARING_MODE_CONCURRENT`: 이미지는 명시적인 소유권 이전 없이 여러 큐 패밀리에서 사용될 수 있습니다. -If the queue families differ, then we'll be using the concurrent mode in this -tutorial to avoid having to do the ownership chapters, because these involve -some concepts that are better explained at a later time. Concurrent mode -requires you to specify in advance between which queue families ownership will -be shared using the `queueFamilyIndexCount` and `pQueueFamilyIndices` -parameters. If the graphics queue family and presentation queue family are the -same, which will be the case on most hardware, then we should stick to exclusive -mode, because concurrent mode requires you to specify at least two distinct -queue families. +큐 패밀리가 다른 경우, 이 튜토리얼에서는 동시 모드(concurrent mode)를 사용할 것입니다. 소유권 이전 관련 장을 피하기 위해서인데, 이 내용은 나중에 더 잘 설명될 개념을 포함하기 때문입니다. 동시 모드를 사용하려면 `queueFamilyIndexCount`와 `pQueueFamilyIndices` 매개변수를 사용하여 어떤 큐 패밀리 간에 소유권이 공유될지 미리 지정해야 합니다. 대부분의 하드웨어에서처럼 그래픽 큐 패밀리와 표현 큐 패밀리가 같다면, 배타적 모드(exclusive mode)를 고수해야 합니다. 동시 모드는 최소 두 개의 다른 큐 패밀리를 지정해야 하기 때문입니다. ```c++ createInfo.preTransform = swapChainSupport.capabilities.currentTransform; ``` -We can specify that a certain transform should be applied to images in the swap -chain if it is supported (`supportedTransforms` in `capabilities`), like a 90 -degree clockwise rotation or horizontal flip. To specify that you do not want -any transformation, simply specify the current transformation. +`capabilities`의 `supportedTransforms`에서 지원된다면, 90도 시계 방향 회전이나 수평 뒤집기와 같은 특정 변환을 스왑 체인의 이미지에 적용하도록 지정할 수 있습니다. 변환을 원하지 않는다고 지정하려면 현재 변환을 그대로 지정하면 됩니다. ```c++ createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; ``` -The `compositeAlpha` field specifies if the alpha channel should be used for -blending with other windows in the window system. You'll almost always want to -simply ignore the alpha channel, hence `VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR`. +`compositeAlpha` 필드는 알파 채널을 창 시스템의 다른 창과 혼합하는 데 사용할지 여부를 지정합니다. 거의 항상 알파 채널을 무시하고 싶을 것이므로 `VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR`를 사용합니다. ```c++ createInfo.presentMode = presentMode; createInfo.clipped = VK_TRUE; ``` -The `presentMode` member speaks for itself. If the `clipped` member is set to -`VK_TRUE` then that means that we don't care about the color of pixels that are -obscured, for example because another window is in front of them. Unless you -really need to be able to read these pixels back and get predictable results, -you'll get the best performance by enabling clipping. +`presentMode` 멤버는 이름 그대로입니다. `clipped` 멤버가 `VK_TRUE`로 설정되면, 다른 창이 앞에 가려져 보이지 않는 픽셀의 색상은 신경 쓰지 않겠다는 의미입니다. 이러한 픽셀을 다시 읽어 예측 가능한 결과를 얻을 필요가 없다면, 클리핑을 활성화하여 최상의 성능을 얻을 수 있습니다. ```c++ createInfo.oldSwapchain = VK_NULL_HANDLE; ``` -That leaves one last field, `oldSwapchain`. With Vulkan it's possible that your swap chain becomes invalid or unoptimized while your application is -running, for example because the window was resized. In that case the swap chain -actually needs to be recreated from scratch and a reference to the old one must -be specified in this field. This is a complex topic that we'll learn more about -in [a future chapter](!en/Drawing_a_triangle/Swap_chain_recreation). For now we'll -assume that we'll only ever create one swap chain. +마지막 필드인 `oldSwapchain`이 남았습니다. Vulkan에서는 애플리케이션이 실행되는 동안 스왑 체인이 유효하지 않거나 최적화되지 않을 수 있습니다(예: 창 크기 조정). 이 경우 스왑 체인을 처음부터 다시 만들어야 하며, 이전 스왑 체인에 대한 참조를 이 필드에 지정해야 합니다. 이것은 [추후 장](!en/Drawing_a_triangle/Swap_chain_recreation)에서 더 자세히 배울 복잡한 주제입니다. 지금은 스왑 체인을 한 번만 생성한다고 가정하겠습니다. -Now add a class member to store the `VkSwapchainKHR` object: +이제 `VkSwapchainKHR` 객체를 저장할 클래스 멤버를 추가합니다. ```c++ VkSwapchainKHR swapChain; ``` -Creating the swap chain is now as simple as calling `vkCreateSwapchainKHR`: +스왑 체인 생성은 이제 `vkCreateSwapchainKHR`를 호출하는 것만큼 간단합니다. ```c++ if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) { @@ -536,9 +394,7 @@ if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS } ``` -The parameters are the logical device, swap chain creation info, optional custom -allocators and a pointer to the variable to store the handle in. No surprises -there. It should be cleaned up using `vkDestroySwapchainKHR` before the device: +매개변수는 논리 장치, 스왑 체인 생성 정보, 선택적 사용자 지정 할당자, 그리고 핸들을 저장할 변수에 대한 포인터입니다. 놀랄 것은 없습니다. 스왑 체인은 장치보다 먼저 `vkDestroySwapchainKHR`를 사용하여 정리해야 합니다. ```c++ void cleanup() { @@ -547,32 +403,23 @@ void cleanup() { } ``` -Now run the application to ensure that the swap chain is created successfully! If at this point you get an access violation error in `vkCreateSwapchainKHR` or see a message like `Failed to find 'vkGetInstanceProcAddress' in layer SteamOverlayVulkanLayer.dll`, then see the [FAQ entry](!en/FAQ) about the Steam overlay layer. +이제 애플리케이션을 실행하여 스왑 체인이 성공적으로 생성되었는지 확인하십시오! 이 시점에서 `vkCreateSwapchainKHR`에서 접근 위반 오류가 발생하거나 `Failed to find 'vkGetInstanceProcAddress' in layer SteamOverlayVulkanLayer.dll`과 같은 메시지가 표시되면 Steam 오버레이 레이어에 대한 [FAQ 항목](!en/FAQ)을 참조하십시오. -Try removing the `createInfo.imageExtent = extent;` line with validation layers -enabled. You'll see that one of the validation layers immediately catches the -mistake and a helpful message is printed: +유효성 검사 레이어를 활성화한 상태에서 `createInfo.imageExtent = extent;` 줄을 제거해 보십시오. 유효성 검사 레이어 중 하나가 즉시 실수를 잡아내고 유용한 메시지를 출력하는 것을 볼 수 있습니다. -![](/images/swap_chain_validation_layer.png) +![유효성 검사 레이어가 스왑 체인 생성 오류를 보고하는 이미지](/images/swap_chain_validation_layer.png) -## Retrieving the swap chain images +## 스왑 체인 이미지 가져오기 -The swap chain has been created now, so all that remains is retrieving the -handles of the `VkImage`s in it. We'll reference these during rendering -operations in later chapters. Add a class member to store the handles: +이제 스왑 체인이 생성되었으므로, 남은 일은 스왑 체인에 있는 `VkImage`들의 핸들을 가져오는 것입니다. 이 핸들은 이후 장에서 렌더링 작업 중에 참조할 것입니다. 핸들을 저장할 클래스 멤버를 추가합니다. ```c++ std::vector swapChainImages; ``` -The images were created by the implementation for the swap chain and they will -be automatically cleaned up once the swap chain has been destroyed, therefore we -don't need to add any cleanup code. +이미지들은 스왑 체인을 위해 구현체에 의해 생성되었으며, 스왑 체인이 파괴되면 자동으로 정리되므로 별도의 정리 코드를 추가할 필요가 없습니다. -I'm adding the code to retrieve the handles to the end of the `createSwapChain` -function, right after the `vkCreateSwapchainKHR` call. Retrieving them is very -similar to the other times where we retrieved an array of objects from Vulkan. Remember that we only specified a minimum number of images in the swap chain, so the implementation is allowed to create a swap chain with more. That's why we'll first query the final number of images with `vkGetSwapchainImagesKHR`, then resize the container and finally call it again -to retrieve the handles. +저는 `vkCreateSwapchainKHR` 호출 직후, `createSwapChain` 함수의 끝에 핸들을 가져오는 코드를 추가하겠습니다. 핸들을 가져오는 것은 Vulkan에서 객체 배열을 가져오는 다른 경우와 매우 유사합니다. 우리는 스왑 체인에 최소 이미지 수만 지정했으므로 구현체는 더 많은 이미지를 가진 스왑 체인을 생성할 수 있습니다. 따라서 먼저 `vkGetSwapchainImagesKHR`로 최종 이미지 수를 쿼리한 다음, 컨테이너의 크기를 조정하고, 마지막으로 다시 호출하여 핸들을 가져옵니다. ```c++ vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr); @@ -580,8 +427,7 @@ swapChainImages.resize(imageCount); vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data()); ``` -One last thing, store the format and extent we've chosen for the swap chain -images in member variables. We'll need them in future chapters. +마지막으로, 스왑 체인 이미지에 대해 선택한 형식과 범위를 멤버 변수에 저장합니다. 이는 앞으로의 장에서 필요할 것입니다. ```c++ VkSwapchainKHR swapChain; @@ -595,9 +441,6 @@ swapChainImageFormat = surfaceFormat.format; swapChainExtent = extent; ``` -We now have a set of images that can be drawn onto and can be presented to the -window. The next chapter will begin to cover how we can set up the images as -render targets and then we start looking into the actual graphics pipeline and -drawing commands! +이제 우리는 렌더링하고 창에 표시할 수 있는 이미지 집합을 갖게 되었습니다. 다음 장에서는 이미지를 렌더링 대상으로 설정하는 방법을 다루고, 그 다음 실제 그래픽 파이프라인과 그리기 명령에 대해 알아보기 시작하겠습니다! -[C++ code](/code/06_swap_chain_creation.cpp) +[C++ 코드](/code/06_swap_chain_creation.cpp) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/01_Presentation/02_Image_views.md b/ko/03_Drawing_a_triangle/01_Presentation/02_Image_views.md index 5988468a..5167e037 100644 --- a/ko/03_Drawing_a_triangle/01_Presentation/02_Image_views.md +++ b/ko/03_Drawing_a_triangle/01_Presentation/02_Image_views.md @@ -1,21 +1,14 @@ -To use any `VkImage`, including those in the swap chain, in the render pipeline -we have to create a `VkImageView` object. An image view is quite literally a -view into an image. It describes how to access the image and which part of the -image to access, for example if it should be treated as a 2D texture depth -texture without any mipmapping levels. +스왑 체인에 있는 이미지를 포함한 모든 `VkImage`를 렌더 파이프라인에서 사용하려면 `VkImageView` 객체를 생성해야 합니다. 이미지 뷰는 말 그대로 이미지로의 뷰(view)입니다. 이는 이미지에 접근하는 방법과 접근할 이미지의 부분을 기술합니다. 예를 들어, 밉매핑 레벨이 없는 2D 텍스처나 깊이 텍스처로 취급해야 하는지 등을 명시합니다. -In this chapter we'll write a `createImageViews` function that creates a basic -image view for every image in the swap chain so that we can use them as color -targets later on. +이번 장에서는 스왑 체인의 모든 이미지에 대한 기본적인 이미지 뷰를 만드는 `createImageViews` 함수를 작성할 것입니다. 이렇게 하면 나중에 이미지 뷰들을 컬러 타겟으로 사용할 수 있습니다. -First add a class member to store the image views in: +먼저, 이미지 뷰를 저장할 클래스 멤버를 추가합니다. ```c++ std::vector swapChainImageViews; ``` -Create the `createImageViews` function and call it right after swap chain -creation. +`createImageViews` 함수를 만들고 스왑 체인 생성 직후에 호출하도록 합니다. ```c++ void initVulkan() { @@ -33,8 +26,7 @@ void createImageViews() { } ``` -The first thing we need to do is resize the list to fit all of the image views -we'll be creating: +가장 먼저 할 일은 생성할 모든 이미지 뷰를 담을 수 있도록 리스트의 크기를 조절하는 것입니다. ```c++ void createImageViews() { @@ -43,7 +35,7 @@ void createImageViews() { } ``` -Next, set up the loop that iterates over all of the swap chain images. +다음으로, 스왑 체인의 모든 이미지를 순회하는 루프를 설정합니다. ```c++ for (size_t i = 0; i < swapChainImages.size(); i++) { @@ -51,8 +43,7 @@ for (size_t i = 0; i < swapChainImages.size(); i++) { } ``` -The parameters for image view creation are specified in a -`VkImageViewCreateInfo` structure. The first few parameters are straightforward. +이미지 뷰 생성을 위한 파라미터들은 `VkImageViewCreateInfo` 구조체에 명시됩니다. 처음 몇 개의 파라미터는 간단합니다. ```c++ VkImageViewCreateInfo createInfo{}; @@ -60,19 +51,14 @@ createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; createInfo.image = swapChainImages[i]; ``` -The `viewType` and `format` fields specify how the image data should be -interpreted. The `viewType` parameter allows you to treat images as 1D textures, -2D textures, 3D textures and cube maps. +`viewType`과 `format` 필드는 이미지 데이터를 어떻게 해석할지를 지정합니다. `viewType` 파라미터를 사용하면 이미지를 1D 텍스처, 2D 텍스처, 3D 텍스처, 큐브 맵으로 다룰 수 있습니다. ```c++ createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; createInfo.format = swapChainImageFormat; ``` -The `components` field allows you to swizzle the color channels around. For -example, you can map all of the channels to the red channel for a monochrome -texture. You can also map constant values of `0` and `1` to a channel. In our -case we'll stick to the default mapping. +`components` 필드를 사용하면 컬러 채널을 스위즐(swizzle)할 수 있습니다. 예를 들어, 단색 텍스처를 위해 모든 채널을 빨간색 채널에 매핑할 수 있습니다. 또한 `0`이나 `1`과 같은 상수 값을 채널에 매핑할 수도 있습니다. 우리의 경우에는 기본 매핑을 사용할 것입니다. ```c++ createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; @@ -81,9 +67,7 @@ createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; ``` -The `subresourceRange` field describes what the image's purpose is and which -part of the image should be accessed. Our images will be used as color targets -without any mipmapping levels or multiple layers. +`subresourceRange` 필드는 이미지의 용도와 접근할 이미지의 부분을 기술합니다. 우리의 이미지는 밉매핑 레벨이나 여러 레이어 없이 컬러 타겟으로 사용될 것입니다. ```c++ createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; @@ -93,12 +77,9 @@ createInfo.subresourceRange.baseArrayLayer = 0; createInfo.subresourceRange.layerCount = 1; ``` -If you were working on a stereographic 3D application, then you would create a -swap chain with multiple layers. You could then create multiple image views for -each image representing the views for the left and right eyes by accessing -different layers. +만약 입체 3D 애플리케이션을 작업한다면, 여러 레이어를 가진 스왑 체인을 생성할 것입니다. 그런 다음 각 이미지에 대해 여러 이미지 뷰를 생성하여, 서로 다른 레이어에 접근함으로써 왼쪽 눈과 오른쪽 눈에 대한 뷰를 표현할 수 있습니다. -Creating the image view is now a matter of calling `vkCreateImageView`: +이제 `vkCreateImageView`를 호출하여 이미지 뷰를 생성하기만 하면 됩니다. ```c++ if (vkCreateImageView(device, &createInfo, nullptr, &swapChainImageViews[i]) != VK_SUCCESS) { @@ -106,8 +87,7 @@ if (vkCreateImageView(device, &createInfo, nullptr, &swapChainImageViews[i]) != } ``` -Unlike images, the image views were explicitly created by us, so we need to add -a similar loop to destroy them again at the end of the program: +이미지와 달리, 이미지 뷰는 우리가 명시적으로 생성했으므로, 프로그램이 끝날 때 이를 다시 파괴하기 위한 비슷한 루프를 추가해야 합니다. ```c++ void cleanup() { @@ -119,9 +99,6 @@ void cleanup() { } ``` -An image view is sufficient to start using an image as a texture, but it's not -quite ready to be used as a render target just yet. That requires one more step -of indirection, known as a framebuffer. But first we'll have to set up the -graphics pipeline. +이미지 뷰는 이미지를 텍스처로 사용하기 시작하기에는 충분하지만, 렌더 타겟으로 사용되기에는 아직 완전히 준비되지 않았습니다. 이를 위해서는 프레임버퍼(framebuffer)라고 알려진 한 단계의 간접(indirection) 과정이 더 필요합니다. 하지만 그 전에 그래픽 파이프라인을 먼저 설정해야 합니다. -[C++ code](/code/07_image_views.cpp) +[C++ 코드](/code/07_image_views.cpp) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md index 9ee7f739..7af1d48a 100644 --- a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md @@ -1,81 +1,32 @@ -Over the course of the next few chapters we'll be setting up a graphics pipeline -that is configured to draw our first triangle. The graphics pipeline is the -sequence of operations that take the vertices and textures of your meshes all -the way to the pixels in the render targets. A simplified overview is displayed -below: +앞으로 몇 개의 챕터에 걸쳐 첫 번째 삼각형을 그리기 위해 구성된 그래픽스 파이프라인을 설정할 것입니다. 그래픽스 파이프라인은 메시의 정점(vertices)과 텍스처(textures)를 가져와 렌더 타겟의 픽셀에 이르는 일련의 작업 순서입니다. 아래에 간소화된 개요가 표시되어 있습니다: ![](/images/vulkan_simplified_pipeline.svg) -The *input assembler* collects the raw vertex data from the buffers you specify -and may also use an index buffer to repeat certain elements without having to -duplicate the vertex data itself. - -The *vertex shader* is run for every vertex and generally applies -transformations to turn vertex positions from model space to screen space. It -also passes per-vertex data down the pipeline. - -The *tessellation shaders* allow you to subdivide geometry based on certain -rules to increase the mesh quality. This is often used to make surfaces like -brick walls and staircases look less flat when they are nearby. - -The *geometry shader* is run on every primitive (triangle, line, point) and can -discard it or output more primitives than came in. This is similar to the -tessellation shader, but much more flexible. However, it is not used much in -today's applications because the performance is not that good on most graphics -cards except for Intel's integrated GPUs. - -The *rasterization* stage discretizes the primitives into *fragments*. These are -the pixel elements that they fill on the framebuffer. Any fragments that fall -outside the screen are discarded and the attributes outputted by the vertex -shader are interpolated across the fragments, as shown in the figure. Usually -the fragments that are behind other primitive fragments are also discarded here -because of depth testing. - -The *fragment shader* is invoked for every fragment that survives and determines -which framebuffer(s) the fragments are written to and with which color and depth -values. It can do this using the interpolated data from the vertex shader, which -can include things like texture coordinates and normals for lighting. - -The *color blending* stage applies operations to mix different fragments that -map to the same pixel in the framebuffer. Fragments can simply overwrite each -other, add up or be mixed based upon transparency. - -Stages with a green color are known as *fixed-function* stages. These stages -allow you to tweak their operations using parameters, but the way they work is -predefined. - -Stages with an orange color on the other hand are `programmable`, which means -that you can upload your own code to the graphics card to apply exactly the -operations you want. This allows you to use fragment shaders, for example, to -implement anything from texturing and lighting to ray tracers. These programs -run on many GPU cores simultaneously to process many objects, like vertices and -fragments in parallel. - -If you've used older APIs like OpenGL and Direct3D before, then you'll be used -to being able to change any pipeline settings at will with calls like -`glBlendFunc` and `OMSetBlendState`. The graphics pipeline in Vulkan is almost -completely immutable, so you must recreate the pipeline from scratch if you want -to change shaders, bind different framebuffers or change the blend function. The -disadvantage is that you'll have to create a number of pipelines that represent -all of the different combinations of states you want to use in your rendering -operations. However, because all of the operations you'll be doing in the -pipeline are known in advance, the driver can optimize for it much better. - -Some of the programmable stages are optional based on what you intend to do. For -example, the tessellation and geometry stages can be disabled if you are just -drawing simple geometry. If you are only interested in depth values then you can -disable the fragment shader stage, which is useful for [shadow map](https://en.wikipedia.org/wiki/Shadow_mapping) -generation. - -In the next chapter we'll first create the two programmable stages required to -put a triangle onto the screen: the vertex shader and fragment shader. The -fixed-function configuration like blending mode, viewport, rasterization will be -set up in the chapter after that. The final part of setting up the graphics -pipeline in Vulkan involves the specification of input and output framebuffers. - -Create a `createGraphicsPipeline` function that is called right after -`createImageViews` in `initVulkan`. We'll work on this function throughout the -following chapters. +*입력 조립기(Input assembler)*는 지정한 버퍼에서 원시 정점 데이터를 수집하고, 인덱스 버퍼를 사용하여 정점 데이터 자체를 복제하지 않고도 특정 요소를 반복할 수도 있습니다. + +*정점 셰이더(Vertex shader)*는 모든 정점에 대해 실행되며, 일반적으로 정점 위치를 모델 공간(model space)에서 스크린 공간(screen space)으로 변환하는 작업을 적용합니다. 또한 정점별 데이터를 파이프라인의 다음 단계로 전달합니다. + +*테셀레이션 셰이더(Tessellation shaders)*를 사용하면 특정 규칙에 따라 지오메트리(geometry)를 세분화하여 메시 품질을 높일 수 있습니다. 이는 벽돌 벽이나 계단과 같은 표면이 가까이 있을 때 덜 평평하게 보이도록 만드는 데 자주 사용됩니다. + +*지오메트리 셰이더(Geometry shader)*는 모든 프리미티브(primitive, 삼각형, 선, 점)에 대해 실행되며, 이를 폐기하거나 들어온 것보다 더 많은 프리미티브를 출력할 수 있습니다. 이는 테셀레이션 셰이더와 유사하지만 훨씬 더 유연합니다. 하지만 Intel의 내장 GPU를 제외한 대부분의 그래픽 카드에서는 성능이 좋지 않기 때문에 오늘날의 애플리케이션에서는 많이 사용되지 않습니다. + +*래스터화(Rasterization)* 단계는 프리미티브를 *프래그먼트(fragment)*로 이산화(discretize)합니다. 이것들은 프레임버퍼에서 채우는 픽셀 요소입니다. 화면 밖에 있는 모든 프래그먼트는 폐기되고, 정점 셰이더에서 출력된 속성들은 그림과 같이 프래그먼트 전체에 걸쳐 보간(interpolated)됩니다. 일반적으로 다른 프리미티브 프래그먼트 뒤에 있는 프래그먼트도 깊이 테스팅(depth testing)으로 인해 여기서 폐기됩니다. + +*프래그먼트 셰이더(Fragment shader)*는 살아남은 모든 프래그먼트에 대해 호출되며, 프래그먼트가 어떤 프레임버퍼에 어떤 색상과 깊이 값으로 기록될지를 결정합니다. 이는 텍스처 좌표 및 조명을 위한 법선(normal)과 같은 것들을 포함할 수 있는, 정점 셰이더로부터 보간된 데이터를 사용하여 수행할 수 있습니다. + +*색상 혼합(Color blending)* 단계는 프레임버퍼의 동일한 픽셀에 매핑되는 다른 프래그먼트들을 혼합하는 연산을 적용합니다. 프래그먼트는 서로를 덮어쓰거나, 더해지거나, 투명도에 따라 혼합될 수 있습니다. + +녹색으로 표시된 단계는 *고정 기능(fixed-function)* 단계라고 합니다. 이 단계에서는 매개변수를 사용하여 작업을 조정할 수 있지만, 작동 방식은 미리 정의되어 있습니다. + +반면에 주황색으로 표시된 단계는 `프로그래밍 가능(programmable)`하며, 이는 그래픽 카드에 자신만의 코드를 업로드하여 원하는 작업을 정확하게 적용할 수 있음을 의미합니다. 이를 통해 예를 들어 프래그먼트 셰이더를 사용하여 텍스처링과 조명에서부터 레이 트레이서에 이르기까지 모든 것을 구현할 수 있습니다. 이 프로그램들은 많은 GPU 코어에서 동시에 실행되어 정점이나 프래그먼트와 같은 많은 객체를 병렬로 처리합니다. + +이전에 OpenGL이나 Direct3D와 같은 오래된 API를 사용해 본 적이 있다면, `glBlendFunc`나 `OMSetBlendState` 같은 호출로 파이프라인 설정을 마음대로 변경하는 데 익숙할 것입니다. Vulkan의 그래픽스 파이프라인은 거의 완전히 *불변(immutable)*하므로, 셰이더를 변경하거나, 다른 프레임버퍼를 바인딩하거나, 혼합 함수를 변경하려면 파이프라인을 처음부터 다시 생성해야 합니다. 단점은 렌더링 작업에서 사용하려는 모든 다른 상태 조합을 나타내는 여러 개의 파이프라인을 만들어야 한다는 것입니다. 하지만 파이프라인에서 수행할 모든 작업이 미리 알려져 있기 때문에 드라이버가 이를 훨씬 더 잘 최적화할 수 있습니다. + +일부 프로그래밍 가능 단계는 무엇을 하려는지에 따라 선택 사항입니다. 예를 들어, 간단한 지오메트리만 그리는 경우 테셀레이션 및 지오메트리 단계를 비활성화할 수 있습니다. 깊이 값에만 관심이 있다면 프래그먼트 셰이더 단계를 비활성화할 수 있으며, 이는 [그림자 맵핑(shadow mapping)](https://en.wikipedia.org/wiki/Shadow_mapping) 생성에 유용합니다. + +다음 챕터에서는 먼저 화면에 삼각형을 표시하는 데 필요한 두 가지 프로그래밍 가능 단계인 정점 셰이더와 프래그먼트 셰이더를 만들 것입니다. 혼합 모드, 뷰포트, 래스터화와 같은 고정 기능 구성은 그 다음 챕터에서 설정할 것입니다. Vulkan에서 그래픽스 파이프라인 설정의 마지막 부분은 입력 및 출력 프레임버퍼를 지정하는 것입니다. + +`initVulkan`의 `createImageViews` 바로 뒤에 호출되는 `createGraphicsPipeline` 함수를 만드세요. 이 함수는 앞으로의 챕터에 걸쳐 계속 작업하게 될 것입니다. ```c++ void initVulkan() { @@ -96,4 +47,4 @@ void createGraphicsPipeline() { } ``` -[C++ code](/code/08_graphics_pipeline.cpp) +[C++ 코드](/code/08_graphics_pipeline.cpp) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md index ef12e836..ed62b75b 100644 --- a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md @@ -1,87 +1,30 @@ -Unlike earlier APIs, shader code in Vulkan has to be specified in a bytecode -format as opposed to human-readable syntax like [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language) -and [HLSL](https://en.wikipedia.org/wiki/High-Level_Shading_Language). This -bytecode format is called [SPIR-V](https://www.khronos.org/spir) and is designed -to be used with both Vulkan and OpenCL (both Khronos APIs). It is a format that -can be used to write graphics and compute shaders, but we will focus on shaders -used in Vulkan's graphics pipelines in this tutorial. - -The advantage of using a bytecode format is that the compilers written by GPU -vendors to turn shader code into native code are significantly less complex. The -past has shown that with human-readable syntax like GLSL, some GPU vendors were -rather flexible with their interpretation of the standard. If you happen to -write non-trivial shaders with a GPU from one of these vendors, then you'd risk -other vendor's drivers rejecting your code due to syntax errors, or worse, your -shader running differently because of compiler bugs. With a straightforward -bytecode format like SPIR-V that will hopefully be avoided. - -However, that does not mean that we need to write this bytecode by hand. Khronos -has released their own vendor-independent compiler that compiles GLSL to SPIR-V. -This compiler is designed to verify that your shader code is fully standards -compliant and produces one SPIR-V binary that you can ship with your program. -You can also include this compiler as a library to produce SPIR-V at runtime, -but we won't be doing that in this tutorial. Although we can use this compiler directly via `glslangValidator.exe`, we will be using `glslc.exe` by Google instead. The advantage of `glslc` is that it uses the same parameter format as well-known compilers like GCC and Clang and includes some extra functionality like *includes*. Both of them are already included in the Vulkan SDK, so you don't need to download anything extra. - -GLSL is a shading language with a C-style syntax. Programs written in it have a -`main` function that is invoked for every object. Instead of using parameters -for input and a return value as output, GLSL uses global variables to handle -input and output. The language includes many features to aid in graphics -programming, like built-in vector and matrix primitives. Functions for -operations like cross products, matrix-vector products and reflections around a -vector are included. The vector type is called `vec` with a number indicating -the amount of elements. For example, a 3D position would be stored in a `vec3`. -It is possible to access single components through members like `.x`, but it's -also possible to create a new vector from multiple components at the same time. -For example, the expression `vec3(1.0, 2.0, 3.0).xy` would result in `vec2`. The -constructors of vectors can also take combinations of vector objects and scalar -values. For example, a `vec3` can be constructed with -`vec3(vec2(1.0, 2.0), 3.0)`. - -As the previous chapter mentioned, we need to write a vertex shader and a -fragment shader to get a triangle on the screen. The next two sections will -cover the GLSL code of each of those and after that I'll show you how to produce -two SPIR-V binaries and load them into the program. - -## Vertex shader - -The vertex shader processes each incoming vertex. It takes its attributes, like -model space position, color, normal and texture coordinates as input. The output is -the final position in clip coordinates and the attributes that need to be passed -on to the fragment shader, like color and texture coordinates. These values will -then be interpolated over the fragments by the rasterizer to produce a smooth -gradient. - -A *clip coordinate* is a four dimensional vector from the vertex shader that is -subsequently turned into a *normalized device coordinate* by dividing the whole -vector by its last component. These normalized device coordinates are -[homogeneous coordinates](https://en.wikipedia.org/wiki/Homogeneous_coordinates) -that map the framebuffer to a [-1, 1] by [-1, 1] coordinate system that looks -like the following: +이전 API들과 달리, Vulkan의 셰이더 코드는 [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language)이나 [HLSL](https://en.wikipedia.org/wiki/High-Level_Shading_Language)처럼 사람이 읽을 수 있는 문법이 아닌 바이트코드 형식으로 명시되어야 합니다. 이 바이트코드 형식을 [SPIR-V](https://www.khronos.org/spir)라고 부르며, Vulkan과 OpenCL(둘 다 Khronos API) 모두에서 사용하도록 설계되었습니다. SPIR-V는 그래픽 및 컴퓨트 셰이더를 작성하는 데 사용될 수 있지만, 이 튜토리얼에서는 Vulkan의 그래픽 파이프라인에서 사용되는 셰이더에 초점을 맞출 것입니다. + +바이트코드 형식을 사용하는 것의 장점은, GPU 제조사가 셰이더 코드를 네이티브 코드로 변환하기 위해 작성하는 컴파일러가 훨씬 덜 복잡해진다는 점입니다. 과거 GLSL과 같이 사람이 읽을 수 있는 문법의 경우, 일부 GPU 제조사는 표준을 다소 유연하게 해석하는 경향이 있었습니다. 만약 여러분이 이런 제조사 중 하나의 GPU로 복잡한 셰이더를 작성했다면, 다른 제조사의 드라이버가 문법 오류로 코드를 거부하거나, 더 심하게는 컴파일러 버그로 셰이더가 다르게 동작할 위험이 있었습니다. SPIR-V와 같은 직관적인 바이트코드 형식을 사용하면 이러한 문제를 피할 수 있을 것입니다. + +하지만 그렇다고 해서 우리가 이 바이트코드를 직접 손으로 작성해야 한다는 의미는 아닙니다. Khronos는 GLSL을 SPIR-V로 컴파일하는 자체적인 벤더 독립적 컴파일러를 출시했습니다. 이 컴파일러는 여러분의 셰이더 코드가 표준을 완벽하게 준수하는지 확인하고, 프로그램과 함께 배포할 수 있는 단일 SPIR-V 바이너리를 생성하도록 설계되었습니다. 이 컴파일러를 라이브러리로 포함하여 런타임에 SPIR-V를 생성할 수도 있지만, 이 튜토리얼에서는 그렇게 하지 않을 것입니다. `glslangValidator.exe`를 통해 직접 컴파일러를 사용할 수도 있지만, 우리는 대신 구글의 `glslc.exe`를 사용할 것입니다. `glslc`의 장점은 GCC나 Clang과 같은 잘 알려진 컴파일러와 동일한 파라미터 형식을 사용하고, *인클루드*와 같은 추가 기능을 포함한다는 것입니다. 이 두 컴파일러는 모두 Vulkan SDK에 이미 포함되어 있으므로 추가로 다운로드할 필요가 없습니다. + +GLSL은 C 스타일 문법을 가진 셰이딩 언어입니다. 이 언어로 작성된 프로그램은 모든 객체에 대해 호출되는 `main` 함수를 가집니다. 입력에 파라미터를 사용하고 출력에 반환 값을 사용하는 대신, GLSL은 전역 변수를 사용하여 입출력을 처리합니다. 이 언어에는 내장 벡터 및 행렬 프리미티브와 같은 그래픽 프로그래밍을 돕는 많은 기능이 포함되어 있습니다. 외적, 행렬-벡터 곱, 벡터 주위 반사와 같은 연산을 위한 함수들이 포함되어 있습니다. 벡터 타입은 `vec`이라고 불리며 뒤에 요소의 수를 나타내는 숫자가 붙습니다. 예를 들어, 3D 위치는 `vec3`에 저장됩니다. `.x`와 같은 멤버를 통해 단일 구성 요소에 접근할 수 있을 뿐만 아니라, 여러 구성 요소를 동시에 사용하여 새로운 벡터를 만들 수도 있습니다. 예를 들어, `vec3(1.0, 2.0, 3.0).xy` 표현식은 `vec2`를 결과로 냅니다. 벡터의 생성자는 벡터 객체와 스칼라 값의 조합을 받을 수도 있습니다. 예를 들어, `vec3`는 `vec3(vec2(1.0, 2.0), 3.0)`으로 생성할 수 있습니다. + +이전 장에서 언급했듯이, 화면에 삼각형을 표시하려면 버텍스 셰이더와 프래그먼트 셰이더를 작성해야 합니다. 다음 두 섹션에서는 각 셰이더의 GLSL 코드를 다루고, 그 후에 두 개의 SPIR-V 바이너리를 생성하고 프로그램에 로드하는 방법을 보여드리겠습니다. + +## 버텍스 셰이더 + +버텍스 셰이더는 들어오는 각 정점(vertex)을 처리합니다. 모델 공간 위치, 색상, 법선, 텍스처 좌표와 같은 속성을 입력으로 받습니다. 출력은 클립 좌표에서의 최종 위치와 프래그먼트 셰이더로 전달되어야 하는 속성들(예: 색상, 텍스처 좌표)입니다. 이 값들은 래스터라이저에 의해 프래그먼트 전체에 걸쳐 보간되어 부드러운 그라데이션을 만들어냅니다. + +*클립 좌표(clip coordinate)*는 버텍스 셰이더에서 출력되는 4차원 벡터이며, 이후 전체 벡터를 마지막 구성 요소로 나누어 *정규화된 디바이스 좌표(normalized device coordinate)*로 변환됩니다. 이 정규화된 디바이스 좌표는 프레임버퍼를 다음과 같이 [-1, 1] x [-1, 1] 좌표계에 매핑하는 [동차 좌표](https://ko.wikipedia.org/wiki/동차좌표)입니다. ![](/images/normalized_device_coordinates.svg) -You should already be familiar with these if you have dabbled in computer -graphics before. If you have used OpenGL before, then you'll notice that the -sign of the Y coordinates is now flipped. The Z coordinate now uses the same -range as it does in Direct3D, from 0 to 1. +이전에 컴퓨터 그래픽스를 다뤄본 적이 있다면 이 좌표계에 이미 익숙할 것입니다. 만약 이전에 OpenGL을 사용해봤다면, Y 좌표의 부호가 뒤집힌 것을 알 수 있습니다. Z 좌표는 이제 Direct3D에서처럼 0에서 1까지의 범위를 사용합니다. -For our first triangle we won't be applying any transformations, we'll just -specify the positions of the three vertices directly as normalized device -coordinates to create the following shape: +첫 번째 삼각형에서는 어떠한 변환도 적용하지 않을 것입니다. 대신 세 정점의 위치를 정규화된 디바이스 좌표로 직접 명시하여 다음 모양을 만들 것입니다. ![](/images/triangle_coordinates.svg) -We can directly output normalized device coordinates by outputting them as clip -coordinates from the vertex shader with the last component set to `1`. That way -the division to transform clip coordinates to normalized device coordinates will -not change anything. +버텍스 셰이더에서 마지막 구성 요소를 `1`로 설정하여 클립 좌표로 출력함으로써 정규화된 디바이스 좌표를 직접 출력할 수 있습니다. 이렇게 하면 클립 좌표를 정규화된 디바이스 좌표로 변환하는 나눗셈 과정에서 아무것도 변하지 않게 됩니다. -Normally these coordinates would be stored in a vertex buffer, but creating a -vertex buffer in Vulkan and filling it with data is not trivial. Therefore I've -decided to postpone that until after we've had the satisfaction of seeing a -triangle pop up on the screen. We're going to do something a little unorthodox -in the meanwhile: include the coordinates directly inside the vertex shader. The -code looks like this: +일반적으로 이 좌표들은 버텍스 버퍼에 저장되지만, Vulkan에서 버텍스 버퍼를 만들고 데이터를 채우는 것은 간단하지 않습니다. 따라서 화면에 삼각형이 나타나는 만족감을 얻은 후에 그 부분을 다루기로 결정했습니다. 그동안 우리는 약간 이례적인 방법을 사용할 것입니다: 버텍스 셰이더 내부에 좌표를 직접 포함시키는 것입니다. 코드는 다음과 같습니다. ```glsl #version 450 @@ -97,21 +40,11 @@ void main() { } ``` -The `main` function is invoked for every vertex. The built-in `gl_VertexIndex` -variable contains the index of the current vertex. This is usually an index into -the vertex buffer, but in our case it will be an index into a hardcoded array -of vertex data. The position of each vertex is accessed from the constant array -in the shader and combined with dummy `z` and `w` components to produce a -position in clip coordinates. The built-in variable `gl_Position` functions as -the output. +`main` 함수는 모든 정점에 대해 호출됩니다. 내장 변수인 `gl_VertexIndex`는 현재 정점의 인덱스를 포함합니다. 이는 보통 버텍스 버퍼의 인덱스이지만, 우리 경우에는 하드코딩된 정점 데이터 배열의 인덱스가 될 것입니다. 각 정점의 위치는 셰이더 내의 상수 배열에서 접근되며, 더미 `z`와 `w` 구성 요소와 결합하여 클립 좌표에서의 위치를 생성합니다. 내장 변수인 `gl_Position`이 출력으로 사용됩니다. -## Fragment shader +## 프래그먼트 셰이더 -The triangle that is formed by the positions from the vertex shader fills an -area on the screen with fragments. The fragment shader is invoked on these -fragments to produce a color and depth for the framebuffer (or framebuffers). A -simple fragment shader that outputs the color red for the entire triangle looks -like this: +버텍스 셰이더의 위치로 형성된 삼각형은 화면의 한 영역을 프래그먼트(fragment)로 채웁니다. 프래그먼트 셰이더는 이 프래그먼트들에 대해 호출되어 프레임버퍼(들)를 위한 색상과 깊이 값을 생성합니다. 전체 삼각형에 대해 빨간색을 출력하는 간단한 프래그먼트 셰이더는 다음과 같습니다. ```glsl #version 450 @@ -123,26 +56,15 @@ void main() { } ``` -The `main` function is called for every fragment just like the vertex shader -`main` function is called for every vertex. Colors in GLSL are 4-component -vectors with the R, G, B and alpha channels within the [0, 1] range. Unlike -`gl_Position` in the vertex shader, there is no built-in variable to output a -color for the current fragment. You have to specify your own output variable for -each framebuffer where the `layout(location = 0)` modifier specifies the index -of the framebuffer. The color red is written to this `outColor` variable that is -linked to the first (and only) framebuffer at index `0`. +`main` 함수는 버텍스 셰이더의 `main` 함수가 모든 정점에 대해 호출되는 것처럼 모든 프래그먼트에 대해 호출됩니다. GLSL에서 색상은 [0, 1] 범위의 R, G, B, 알파 채널을 가진 4-요소 벡터입니다. 버텍스 셰이더의 `gl_Position`과 달리, 현재 프래그먼트의 색상을 출력하기 위한 내장 변수는 없습니다. 각 프레임버퍼에 대해 자신만의 출력 변수를 지정해야 하며, `layout(location = 0)` 지정자는 프레임버퍼의 인덱스를 명시합니다. 빨간색은 인덱스 `0`에 있는 첫 번째 (그리고 유일한) 프레임버퍼에 연결된 `outColor` 변수에 기록됩니다. -## Per-vertex colors +## 정점별 색상 -Making the entire triangle red is not very interesting, wouldn't something like -the following look a lot nicer? +삼각형 전체를 빨갛게 만드는 것은 별로 흥미롭지 않습니다. 다음과 같은 모양이 훨씬 더 멋지지 않을까요? ![](/images/triangle_coordinates_colors.png) -We have to make a couple of changes to both shaders to accomplish this. First -off, we need to specify a distinct color for each of the three vertices. The -vertex shader should now include an array with colors just like it does for -positions: +이를 달성하기 위해 두 셰이더 모두에 몇 가지 변경을 해야 합니다. 먼저, 세 정점 각각에 대해 고유한 색상을 지정해야 합니다. 버텍스 셰이더는 이제 위치와 마찬가지로 색상 배열을 포함해야 합니다. ```glsl vec3 colors[3] = vec3[]( @@ -152,9 +74,7 @@ vec3 colors[3] = vec3[]( ); ``` -Now we just need to pass these per-vertex colors to the fragment shader so it -can output their interpolated values to the framebuffer. Add an output for color -to the vertex shader and write to it in the `main` function: +이제 이 정점별 색상을 프래그먼트 셰이더로 전달하여 보간된 값을 프레임버퍼에 출력하도록 해야 합니다. 버텍스 셰이더에 색상 출력을 추가하고 `main` 함수에서 값을 기록합니다. ```glsl layout(location = 0) out vec3 fragColor; @@ -165,7 +85,7 @@ void main() { } ``` -Next, we need to add a matching input in the fragment shader: +다음으로, 프래그먼트 셰이더에 일치하는 입력을 추가해야 합니다. ```glsl layout(location = 0) in vec3 fragColor; @@ -175,21 +95,13 @@ void main() { } ``` -The input variable does not necessarily have to use the same name, they will be -linked together using the indexes specified by the `location` directives. The -`main` function has been modified to output the color along with an alpha value. -As shown in the image above, the values for `fragColor` will be automatically -interpolated for the fragments between the three vertices, resulting in a smooth -gradient. +입력 변수는 반드시 같은 이름을 사용할 필요는 없으며, `location` 지시자에 의해 지정된 인덱스를 사용하여 연결됩니다. `main` 함수는 알파 값과 함께 색상을 출력하도록 수정되었습니다. 위 이미지에서 볼 수 있듯이, `fragColor`의 값들은 세 정점 사이의 프래그먼트들에 대해 자동으로 보간되어, 부드러운 그라데이션을 만들어냅니다. -## Compiling the shaders +## 셰이더 컴파일하기 -Create a directory called `shaders` in the root directory of your project and -store the vertex shader in a file called `shader.vert` and the fragment shader -in a file called `shader.frag` in that directory. GLSL shaders don't have an -official extension, but these two are commonly used to distinguish them. +프로젝트의 루트 디렉토리에 `shaders`라는 디렉토리를 만들고, 버텍스 셰이더를 `shader.vert` 파일에, 프래그먼트 셰이더를 `shader.frag` 파일에 저장하십시오. GLSL 셰이더에는 공식적인 확장자가 없지만, 이 두 확장자는 셰이더를 구별하기 위해 일반적으로 사용됩니다. -The contents of `shader.vert` should be: +`shader.vert`의 내용은 다음과 같아야 합니다. ```glsl #version 450 @@ -214,7 +126,7 @@ void main() { } ``` -And the contents of `shader.frag` should be: +그리고 `shader.frag`의 내용은 다음과 같아야 합니다. ```glsl #version 450 @@ -228,12 +140,11 @@ void main() { } ``` -We're now going to compile these into SPIR-V bytecode using the -`glslc` program. +이제 `glslc` 프로그램을 사용하여 이들을 SPIR-V 바이트코드로 컴파일할 것입니다. **Windows** -Create a `compile.bat` file with the following contents: +다음 내용으로 `compile.bat` 파일을 만드십시오. ```bash C:/VulkanSDK/x.x.x.x/Bin/glslc.exe shader.vert -o vert.spv @@ -241,39 +152,30 @@ C:/VulkanSDK/x.x.x.x/Bin/glslc.exe shader.frag -o frag.spv pause ``` -Replace the path to `glslc.exe` with the path to where you installed -the Vulkan SDK. Double click the file to run it. +`glslc.exe`의 경로를 여러분이 Vulkan SDK를 설치한 경로로 교체하십시오. 파일을 더블 클릭하여 실행합니다. **Linux** -Create a `compile.sh` file with the following contents: +다음 내용으로 `compile.sh` 파일을 만드십시오. ```bash /home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.vert -o vert.spv /home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.frag -o frag.spv ``` -Replace the path to `glslc` with the path to where you installed the -Vulkan SDK. Make the script executable with `chmod +x compile.sh` and run it. +`glslc`의 경로를 여러분이 Vulkan SDK를 설치한 경로로 교체하십시오. `chmod +x compile.sh`로 스크립트를 실행 가능하게 만들고 실행합니다. -**End of platform-specific instructions** +**플랫폼별 지침 끝** -These two commands tell the compiler to read the GLSL source file and output a SPIR-V bytecode file using the `-o` (output) flag. +이 두 명령어는 컴파일러에게 GLSL 소스 파일을 읽고 `-o`(output) 플래그를 사용하여 SPIR-V 바이트코드 파일을 출력하도록 지시합니다. -If your shader contains a syntax error then the compiler will tell you the line -number and problem, as you would expect. Try leaving out a semicolon for example -and run the compile script again. Also try running the compiler without any -arguments to see what kinds of flags it supports. It can, for example, also -output the bytecode into a human-readable format so you can see exactly what -your shader is doing and any optimizations that have been applied at this stage. +셰이더에 문법 오류가 있으면 컴파일러가 예상대로 줄 번호와 문제를 알려줄 것입니다. 예를 들어 세미콜론을 하나 빼고 컴파일 스크립트를 다시 실행해보십시오. 또한 아무 인수 없이 컴파일러를 실행하여 어떤 종류의 플래그를 지원하는지 확인해볼 수도 있습니다. 예를 들어, 바이트코드를 사람이 읽을 수 있는 형식으로 출력하여 셰이더가 정확히 무엇을 하는지, 이 단계에서 어떤 최적화가 적용되었는지 볼 수도 있습니다. -Compiling shaders on the commandline is one of the most straightforward options and it's the one that we'll use in this tutorial, but it's also possible to compile shaders directly from your own code. The Vulkan SDK includes [libshaderc](https://github.com/google/shaderc), which is a library to compile GLSL code to SPIR-V from within your program. +명령줄에서 셰이더를 컴파일하는 것은 가장 간단한 옵션 중 하나이며 이 튜토리얼에서 사용할 방법이지만, 자신의 코드에서 직접 셰이더를 컴파일하는 것도 가능합니다. Vulkan SDK에는 프로그램 내에서 GLSL 코드를 SPIR-V로 컴파일하는 라이브러리인 [libshaderc](https://github.com/google/shaderc)가 포함되어 있습니다. -## Loading a shader +## 셰이더 로드하기 -Now that we have a way of producing SPIR-V shaders, it's time to load them into -our program to plug them into the graphics pipeline at some point. We'll first -write a simple helper function to load the binary data from the files. +이제 SPIR-V 셰이더를 생성하는 방법을 알았으니, 이를 프로그램에 로드하여 그래픽 파이프라인의 특정 지점에 연결할 시간입니다. 먼저 파일에서 바이너리 데이터를 로드하는 간단한 헬퍼 함수를 작성하겠습니다. ```c++ #include @@ -289,30 +191,26 @@ static std::vector readFile(const std::string& filename) { } ``` -The `readFile` function will read all of the bytes from the specified file and -return them in a byte array managed by `std::vector`. We start by opening the -file with two flags: +`readFile` 함수는 지정된 파일의 모든 바이트를 읽어 `std::vector`가 관리하는 바이트 배열로 반환합니다. 다음 두 플래그를 사용하여 파일을 엽니다. -* `ate`: Start reading at the end of the file -* `binary`: Read the file as binary file (avoid text transformations) +* `ate`: 파일의 끝에서 읽기 시작 +* `binary`: 파일을 바이너리 파일로 읽기 (텍스트 변환 방지) -The advantage of starting to read at the end of the file is that we can use the -read position to determine the size of the file and allocate a buffer: +파일의 끝에서 읽기를 시작하는 것의 장점은 읽기 위치를 사용하여 파일 크기를 결정하고 버퍼를 할당할 수 있다는 것입니다. ```c++ size_t fileSize = (size_t) file.tellg(); std::vector buffer(fileSize); ``` -After that, we can seek back to the beginning of the file and read all of the -bytes at once: +그런 다음, 파일의 시작 부분으로 다시 이동하여 모든 바이트를 한 번에 읽을 수 있습니다. ```c++ file.seekg(0); file.read(buffer.data(), fileSize); ``` -And finally close the file and return the bytes: +마지막으로 파일을 닫고 바이트를 반환합니다. ```c++ file.close(); @@ -320,8 +218,7 @@ file.close(); return buffer; ``` -We'll now call this function from `createGraphicsPipeline` to load the bytecode -of the two shaders: +이제 `createGraphicsPipeline` 함수에서 이 함수를 호출하여 두 셰이더의 바이트코드를 로드합니다. ```c++ void createGraphicsPipeline() { @@ -330,14 +227,11 @@ void createGraphicsPipeline() { } ``` -Make sure that the shaders are loaded correctly by printing the size of the -buffers and checking if they match the actual file size in bytes. Note that the code doesn't need to be null terminated since it's binary code and we will later be explicit about its size. +버퍼의 크기를 출력하여 실제 파일 크기(바이트 단위)와 일치하는지 확인하여 셰이더가 올바르게 로드되었는지 확인하십시오. 코드는 바이너리 코드이므로 null로 끝나지 않아도 되며, 나중에 코드의 크기를 명시적으로 지정할 것입니다. -## Creating shader modules +## 셰이더 모듈 생성하기 -Before we can pass the code to the pipeline, we have to wrap it in a -`VkShaderModule` object. Let's create a helper function `createShaderModule` to -do that. +코드를 파이프라인에 전달하기 전에 `VkShaderModule` 객체로 감싸야 합니다. 이를 위해 `createShaderModule`이라는 헬퍼 함수를 만들어 보겠습니다. ```c++ VkShaderModule createShaderModule(const std::vector& code) { @@ -345,18 +239,9 @@ VkShaderModule createShaderModule(const std::vector& code) { } ``` -The function will take a buffer with the bytecode as parameter and create a -`VkShaderModule` from it. +이 함수는 바이트코드가 담긴 버퍼를 파라미터로 받아 `VkShaderModule`을 생성합니다. -Creating a shader module is simple, we only need to specify a pointer to the -buffer with the bytecode and the length of it. This information is specified in -a `VkShaderModuleCreateInfo` structure. The one catch is that the size of the -bytecode is specified in bytes, but the bytecode pointer is a `uint32_t` pointer -rather than a `char` pointer. Therefore we will need to cast the pointer with -`reinterpret_cast` as shown below. When you perform a cast like this, you also -need to ensure that the data satisfies the alignment requirements of `uint32_t`. -Lucky for us, the data is stored in an `std::vector` where the default allocator -already ensures that the data satisfies the worst case alignment requirements. +셰이더 모듈을 만드는 것은 간단합니다. 바이트코드가 있는 버퍼에 대한 포인터와 그 길이를 지정하기만 하면 됩니다. 이 정보는 `VkShaderModuleCreateInfo` 구조체에 명시됩니다. 한 가지 주의할 점은 바이트코드의 크기는 바이트 단위로 지정되지만, 바이트코드 포인터는 `char` 포인터가 아닌 `uint32_t` 포인터라는 것입니다. 따라서 아래와 같이 `reinterpret_cast`를 사용하여 포인터를 캐스팅해야 합니다. 이와 같은 캐스팅을 수행할 때는 데이터가 `uint32_t`의 정렬(alignment) 요구 사항을 만족하는지 확인해야 합니다. 다행히도, 데이터는 `std::vector`에 저장되며, 기본 할당자는 이미 데이터가 최악의 경우의 정렬 요구 사항을 만족하도록 보장합니다. ```c++ VkShaderModuleCreateInfo createInfo{}; @@ -365,7 +250,7 @@ createInfo.codeSize = code.size(); createInfo.pCode = reinterpret_cast(code.data()); ``` -The `VkShaderModule` can then be created with a call to `vkCreateShaderModule`: +`VkShaderModule`은 `vkCreateShaderModule` 호출로 생성할 수 있습니다. ```c++ VkShaderModule shaderModule; @@ -374,17 +259,13 @@ if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCC } ``` -The parameters are the same as those in previous object creation functions: the -logical device, pointer to create info structure, optional pointer to custom -allocators and handle output variable. The buffer with the code can be freed -immediately after creating the shader module. Don't forget to return the created -shader module: +파라미터는 이전 객체 생성 함수들과 동일합니다: 논리 장치, 생성 정보 구조체 포인터, 사용자 정의 할당자(선택 사항) 포인터, 핸들 출력 변수입니다. 코드가 담긴 버퍼는 셰이더 모듈을 생성한 직후에 해제할 수 있습니다. 생성된 셰이더 모듈을 반환하는 것을 잊지 마십시오. ```c++ return shaderModule; ``` -Shader modules are just a thin wrapper around the shader bytecode that we've previously loaded from a file and the functions defined in it. The compilation and linking of the SPIR-V bytecode to machine code for execution by the GPU doesn't happen until the graphics pipeline is created. That means that we're allowed to destroy the shader modules again as soon as pipeline creation is finished, which is why we'll make them local variables in the `createGraphicsPipeline` function instead of class members: +셰이더 모듈은 우리가 이전에 파일에서 로드한 셰이더 바이트코드와 그 안에 정의된 함수들을 얇게 감싼 래퍼일 뿐입니다. SPIR-V 바이트코드를 GPU가 실행할 수 있는 기계어 코드로 컴파일하고 링크하는 작업은 그래픽 파이프라인이 생성될 때까지 일어나지 않습니다. 즉, 파이프라인 생성이 완료되는 즉시 셰이더 모듈을 다시 파괴해도 괜찮다는 의미입니다. 따라서 클래스 멤버가 아닌 `createGraphicsPipeline` 함수의 지역 변수로 만들 것입니다. ```c++ void createGraphicsPipeline() { @@ -395,7 +276,7 @@ void createGraphicsPipeline() { VkShaderModule fragShaderModule = createShaderModule(fragShaderCode); ``` -The cleanup should then happen at the end of the function by adding two calls to `vkDestroyShaderModule`. All of the remaining code in this chapter will be inserted before these lines. +정리는 함수의 끝에서 `vkDestroyShaderModule`에 대한 두 번의 호출을 추가하여 이루어져야 합니다. 이 장의 나머지 모든 코드는 이 두 줄 앞에 삽입될 것입니다. ```c++ ... @@ -404,12 +285,11 @@ The cleanup should then happen at the end of the function by adding two calls to } ``` -## Shader stage creation +## 셰이더 스테이지 생성 -To actually use the shaders we'll need to assign them to a specific pipeline stage through `VkPipelineShaderStageCreateInfo` structures as part of the actual pipeline creation process. +셰이더를 실제로 사용하려면 실제 파이프라인 생성 과정의 일부로서 `VkPipelineShaderStageCreateInfo` 구조체를 통해 특정 파이프라인 단계에 할당해야 합니다. -We'll start by filling in the structure for the vertex shader, again in the -`createGraphicsPipeline` function. +`createGraphicsPipeline` 함수에서 버텍스 셰이더를 위한 구조체를 채우는 것으로 시작하겠습니다. ```c++ VkPipelineShaderStageCreateInfo vertShaderStageInfo{}; @@ -417,32 +297,18 @@ vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; ``` -The first step, besides the obligatory `sType` member, is telling Vulkan in -which pipeline stage the shader is going to be used. There is an enum value for -each of the programmable stages described in the previous chapter. +필수적인 `sType` 멤버 외에 첫 번째 단계는 Vulkan에게 셰이더가 사용될 파이프라인 단계를 알려주는 것입니다. 이전 장에서 설명한 프로그래밍 가능한 각 단계에 대한 열거형 값이 있습니다. ```c++ vertShaderStageInfo.module = vertShaderModule; vertShaderStageInfo.pName = "main"; ``` -The next two members specify the shader module containing the code, and the -function to invoke, known as the *entrypoint*. That means that it's possible to combine multiple fragment -shaders into a single shader module and use different entry points to -differentiate between their behaviors. In this case we'll stick to the standard -`main`, however. +다음 두 멤버는 코드를 포함하는 셰이더 모듈과 호출할 함수, 즉 *엔트리포인트(entrypoint)*를 지정합니다. 이는 여러 프래그먼트 셰이더를 단일 셰이더 모듈로 결합하고 다른 엔트리포인트를 사용하여 그들의 동작을 구별할 수 있음을 의미합니다. 하지만 이 경우에는 표준적인 `main`을 사용할 것입니다. -There is one more (optional) member, `pSpecializationInfo`, which we won't be -using here, but is worth discussing. It allows you to specify values for shader -constants. You can use a single shader module where its behavior can be -configured at pipeline creation by specifying different values for the constants -used in it. This is more efficient than configuring the shader using variables -at render time, because the compiler can do optimizations like eliminating `if` -statements that depend on these values. If you don't have any constants like -that, then you can set the member to `nullptr`, which our struct initialization -does automatically. +여기서는 사용하지 않지만 논의할 가치가 있는 `pSpecializationInfo`라는 또 다른 (선택적) 멤버가 있습니다. 이 멤버를 사용하면 셰이더 상수에 대한 값을 지정할 수 있습니다. 단일 셰이더 모듈을 사용하면서 파이프라인 생성 시 셰이더에 사용된 상수에 대해 다른 값을 지정하여 동작을 구성할 수 있습니다. 이는 렌더링 시 변수를 사용하여 셰이더를 구성하는 것보다 더 효율적인데, 왜냐하면 컴파일러가 이러한 값에 의존하는 `if` 문을 제거하는 것과 같은 최적화를 수행할 수 있기 때문입니다. 만약 그런 상수가 없다면, 멤버를 `nullptr`로 설정할 수 있으며, 우리 구조체 초기화가 자동으로 그렇게 합니다. -Modifying the structure to suit the fragment shader is easy: +프래그먼트 셰이더에 맞게 구조체를 수정하는 것은 쉽습니다. ```c++ VkPipelineShaderStageCreateInfo fragShaderStageInfo{}; @@ -452,16 +318,14 @@ fragShaderStageInfo.module = fragShaderModule; fragShaderStageInfo.pName = "main"; ``` -Finish by defining an array that contains these two structs, which we'll later -use to reference them in the actual pipeline creation step. +마지막으로 이 두 구조체를 포함하는 배열을 정의합니다. 이 배열은 나중에 실제 파이프라인 생성 단계에서 이들을 참조하는 데 사용될 것입니다. ```c++ VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; ``` -That's all there is to describing the programmable stages of the pipeline. In -the next chapter we'll look at the fixed-function stages. +이것으로 파이프라인의 프로그래밍 가능 단계를 기술하는 작업이 모두 끝났습니다. 다음 장에서는 고정 함수 단계를 살펴보겠습니다. -[C++ code](/code/09_shader_modules.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +[C++ 코드](/code/09_shader_modules.cpp) / +[버텍스 셰이더](/code/09_shader_base.vert) / +[프래그먼트 셰이더](/code/09_shader_base.frag) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md index 5b4bfdec..963b7836 100644 --- a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md @@ -1,16 +1,8 @@ +이전의 그래픽 API들은 그래픽 파이프라인의 대부분 단계에 대한 기본 상태를 제공했습니다. Vulkan에서는 대부분의 파이프라인 상태를 명시적으로 지정해야 하며, 이 상태들은 불변(immutable)의 파이프라인 상태 객체(PSO)로 구워지기(baked) 때문입니다. 이번 장에서는 이러한 고정 함수(fixed-function) 연산을 구성하기 위한 모든 구조체를 채워 넣을 것입니다. -The older graphics APIs provided default state for most of the stages of the -graphics pipeline. In Vulkan you have to be explicit about most pipeline states as -it'll be baked into an immutable pipeline state object. In this chapter we'll fill -in all of the structures to configure these fixed-function operations. +## 동적 상태 (Dynamic state) -## Dynamic state - -While *most* of the pipeline state needs to be baked into the pipeline state, -a limited amount of the state *can* actually be changed without recreating the -pipeline at draw time. Examples are the size of the viewport, line width -and blend constants. If you want to use dynamic state and keep these properties out, -then you'll have to fill in a `VkPipelineDynamicStateCreateInfo` structure like this: +*대부분의* 파이프라인 상태는 파이프라인 상태 객체에 구워져야 하지만, 제한된 일부 상태는 파이프라인을 다시 만들지 않고도 드로우 타임(draw time)에 변경할 수 *있습니다*. 뷰포트의 크기, 선 두께, 블렌딩 상수 등이 그 예입니다. 만약 동적 상태를 사용하고 이러한 속성들을 파이프라인 생성 시에 고정하지 않으려면, 다음과 같이 `VkPipelineDynamicStateCreateInfo` 구조체를 채워야 합니다. ```c++ std::vector dynamicStates = { @@ -24,25 +16,16 @@ dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); dynamicState.pDynamicStates = dynamicStates.data(); ``` -This will cause the configuration of these values to be ignored and you will be -able (and required) to specify the data at drawing time. This results in a more flexible -setup and is very common for things like viewport and scissor state, which would -result in a more complex setup when being baked into the pipeline state. +이렇게 하면 이 값들의 구성이 파이프라인 생성 시에는 무시되며, 드로잉 시점에 이 데이터를 지정할 수 있게 되고 또 지정해야만 합니다. 이는 더 유연한 설정을 가능하게 하며, 뷰포트나 시저 상태처럼 파이프라인 상태에 고정시킬 경우 설정이 더 복잡해질 수 있는 항목들에 대해 매우 일반적인 방식입니다. -## Vertex input +## 정점 입력 (Vertex input) -The `VkPipelineVertexInputStateCreateInfo` structure describes the format of the -vertex data that will be passed to the vertex shader. It describes this in -roughly two ways: +`VkPipelineVertexInputStateCreateInfo` 구조체는 정점 셰이더로 전달될 정점 데이터의 형식을 설명합니다. 이 설명은 크게 두 가지 방식으로 이루어집니다. -* Bindings: spacing between data and whether the data is per-vertex or -per-instance (see [instancing](https://en.wikipedia.org/wiki/Geometry_instancing)) -* Attribute descriptions: type of the attributes passed to the vertex shader, -which binding to load them from and at which offset +* **바인딩(Bindings)**: 데이터 간의 간격 및 데이터가 정점별(per-vertex)인지 인스턴스별(per-instance)인지 여부 (자세한 내용은 [인스턴싱](https://ko.wikipedia.org/wiki/인스턴싱) 참조) +* **속성 서술(Attribute descriptions)**: 정점 셰이더로 전달되는 속성의 유형, 어떤 바인딩에서 로드할지, 그리고 어떤 오프셋에 있는지 -Because we're hard coding the vertex data directly in the vertex shader, we'll -fill in this structure to specify that there is no vertex data to load for now. -We'll get back to it in the vertex buffer chapter. +지금은 정점 데이터를 정점 셰이더에 직접 하드코딩하고 있으므로, 이 구조체를 채워서 로드할 정점 데이터가 없음을 명시할 것입니다. 이 부분은 정점 버퍼(vertex buffer) 장에서 다시 다룰 것입니다. ```c++ VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; @@ -53,36 +36,21 @@ vertexInputInfo.vertexAttributeDescriptionCount = 0; vertexInputInfo.pVertexAttributeDescriptions = nullptr; // Optional ``` -The `pVertexBindingDescriptions` and `pVertexAttributeDescriptions` members -point to an array of structs that describe the aforementioned details for -loading vertex data. Add this structure to the `createGraphicsPipeline` function -right after the `shaderStages` array. - -## Input assembly - -The `VkPipelineInputAssemblyStateCreateInfo` struct describes two things: what -kind of geometry will be drawn from the vertices and if primitive restart should -be enabled. The former is specified in the `topology` member and can have values -like: - -* `VK_PRIMITIVE_TOPOLOGY_POINT_LIST`: points from vertices -* `VK_PRIMITIVE_TOPOLOGY_LINE_LIST`: line from every 2 vertices without reuse -* `VK_PRIMITIVE_TOPOLOGY_LINE_STRIP`: the end vertex of every line is used as -start vertex for the next line -* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST`: triangle from every 3 vertices without -reuse -* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP `: the second and third vertex of every -triangle are used as first two vertices of the next triangle - -Normally, the vertices are loaded from the vertex buffer by index in sequential -order, but with an *element buffer* you can specify the indices to use yourself. -This allows you to perform optimizations like reusing vertices. If you set the -`primitiveRestartEnable` member to `VK_TRUE`, then it's possible to break up -lines and triangles in the `_STRIP` topology modes by using a special index of -`0xFFFF` or `0xFFFFFFFF`. - -We intend to draw triangles throughout this tutorial, so we'll stick to the -following data for the structure: +`pVertexBindingDescriptions`와 `pVertexAttributeDescriptions` 멤버는 정점 데이터 로딩에 대한 위 세부 사항들을 설명하는 구조체 배열을 가리킵니다. `createGraphicsPipeline` 함수에서 `shaderStages` 배열 바로 다음에 이 구조체를 추가하세요. + +## 입력 조립 (Input assembly) + +`VkPipelineInputAssemblyStateCreateInfo` 구조체는 두 가지를 설명합니다: 정점들로부터 어떤 종류의 지오메트리를 그릴 것인지, 그리고 프리미티브 재시작(primitive restart)을 활성화할 것인지입니다. 전자는 `topology` 멤버에서 지정하며, 다음과 같은 값을 가질 수 있습니다. + +* `VK_PRIMITIVE_TOPOLOGY_POINT_LIST`: 정점들로부터 점을 그림 +* `VK_PRIMITIVE_TOPOLOGY_LINE_LIST`: 2개의 정점마다 재사용 없이 선을 그림 +* `VK_PRIMITIVE_TOPOLOGY_LINE_STRIP`: 각 선의 끝 정점이 다음 선의 시작 정점으로 사용됨 +* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST`: 3개의 정점마다 재사용 없이 삼각형을 그림 +* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP `: 각 삼각형의 두 번째와 세 번째 정점이 다음 삼각형의 첫 두 정점으로 사용됨 + +일반적으로 정점들은 정점 버퍼에서 인덱스 순서대로 로드되지만, *엘리먼트 버퍼(element buffer)*를 사용하면 사용할 인덱스를 직접 지정할 수 있습니다. 이를 통해 정점 재사용과 같은 최적화를 수행할 수 있습니다. 만약 `primitiveRestartEnable` 멤버를 `VK_TRUE`로 설정하면, `0xFFFF` 또는 `0xFFFFFFFF` 같은 특수 인덱스를 사용하여 `_STRIP` 토폴로지 모드에서 선과 삼각형을 분리할 수 있습니다. + +이 튜토리얼에서는 계속 삼각형을 그릴 것이므로, 구조체에 다음 데이터를 사용하겠습니다. ```c++ VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; @@ -91,11 +59,9 @@ inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; inputAssembly.primitiveRestartEnable = VK_FALSE; ``` -## Viewports and scissors +## 뷰포트와 시저 (Viewports and scissors) -A viewport basically describes the region of the framebuffer that the output -will be rendered to. This will almost always be `(0, 0)` to `(width, height)` -and in this tutorial that will also be the case. +뷰포트(viewport)는 기본적으로 출력이 렌더링될 프레임버퍼의 영역을 설명합니다. 이는 거의 항상 `(0, 0)`에서 `(width, height)`까지이며, 이 튜토리얼에서도 마찬가지입니다. ```c++ VkViewport viewport{}; @@ -107,26 +73,15 @@ viewport.minDepth = 0.0f; viewport.maxDepth = 1.0f; ``` -Remember that the size of the swap chain and its images may differ from the -`WIDTH` and `HEIGHT` of the window. The swap chain images will be used as -framebuffers later on, so we should stick to their size. +스왑체인과 그 이미지들의 크기는 창의 `WIDTH`와 `HEIGHT`와 다를 수 있음을 기억하세요. 스왑체인 이미지들은 나중에 프레임버퍼로 사용될 것이므로, 그 크기를 따라야 합니다. -The `minDepth` and `maxDepth` values specify the range of depth values to use -for the framebuffer. These values must be within the `[0.0f, 1.0f]` range, but -`minDepth` may be higher than `maxDepth`. If you aren't doing anything special, -then you should stick to the standard values of `0.0f` and `1.0f`. +`minDepth`와 `maxDepth` 값은 프레임버퍼에 사용할 깊이 값의 범위를 지정합니다. 이 값들은 `[0.0f, 1.0f]` 범위 내에 있어야 하지만, `minDepth`가 `maxDepth`보다 클 수도 있습니다. 특별한 작업을 하지 않는다면 표준 값인 `0.0f`와 `1.0f`를 사용해야 합니다. -While viewports define the transformation from the image to the framebuffer, -scissor rectangles define in which regions pixels will actually be stored. Any -pixels outside the scissor rectangles will be discarded by the rasterizer. They -function like a filter rather than a transformation. The difference is -illustrated below. Note that the left scissor rectangle is just one of the many -possibilities that would result in that image, as long as it's larger than the -viewport. +뷰포트가 이미지에서 프레임버퍼로의 변환을 정의하는 반면, 시저 사각형(scissor rectangle)은 실제로 픽셀이 저장될 영역을 정의합니다. 시저 사각형 밖의 모든 픽셀은 래스터라이저에 의해 버려집니다. 이는 변환이라기보다는 필터처럼 작동합니다. 차이점은 아래 그림에 설명되어 있습니다. 왼쪽 시저 사각형은 뷰포트보다 크기만 하다면 해당 이미지를 만들어내는 많은 가능성 중 하나일 뿐입니다. ![](/images/viewports_scissors.png) -So if we wanted to draw to the entire framebuffer, we would specify a scissor rectangle that covers it entirely: +따라서 전체 프레임버퍼에 그리고 싶다면, 프레임버퍼 전체를 덮는 시저 사각형을 지정하면 됩니다. ```c++ VkRect2D scissor{}; @@ -134,9 +89,9 @@ scissor.offset = {0, 0}; scissor.extent = swapChainExtent; ``` -Viewport(s) and scissor rectangle(s) can either be specified as a static part of the pipeline or as a [dynamic state](#dynamic-state) set in the command buffer. While the former is more in line with the other states it's often convenient to make viewport and scissor state dynamic as it gives you a lot more flexibility. This is very common and all implementations can handle this dynamic state without a performance penalty. +뷰포트와 시저 사각형은 파이프라인의 정적(static) 부분으로 지정하거나, 커맨드 버퍼에서 설정되는 [동적 상태](#동적-상태)로 지정할 수 있습니다. 전자가 다른 상태들과 더 일관성이 있지만, 뷰포트와 시저 상태를 동적으로 만드는 것이 훨씬 더 많은 유연성을 제공하기 때문에 종종 편리합니다. 이는 매우 일반적인 방식이며 모든 구현체에서 성능 저하 없이 이 동적 상태를 처리할 수 있습니다. -When opting for dynamic viewport(s) and scissor rectangle(s) you need to enable the respective dynamic states for the pipeline: +동적 뷰포트와 시저 사각형을 선택하는 경우, 파이프라인에 대해 해당 동적 상태를 활성화해야 합니다. ```c++ std::vector dynamicStates = { @@ -150,7 +105,7 @@ dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); dynamicState.pDynamicStates = dynamicStates.data(); ``` -And then you only need to specify their count at pipeline creation time: +그런 다음 파이프라인 생성 시에는 그들의 개수만 지정하면 됩니다. ```c++ VkPipelineViewportStateCreateInfo viewportState{}; @@ -159,12 +114,9 @@ viewportState.viewportCount = 1; viewportState.scissorCount = 1; ``` -The actual viewport(s) and scissor rectangle(s) will then later be set up at drawing time. - -With dynamic state it's even possible to specify different viewports and or scissor rectangles within a single command buffer. +실제 뷰포트와 시저 사각형은 나중에 드로잉 시점에 설정될 것입니다. 동적 상태를 사용하면 단일 커맨드 버퍼 내에서 다른 뷰포트 및/또는 시저 사각형을 지정하는 것조차 가능합니다. -Without dynamic state, the viewport and scissor rectangle need to be set in the pipeline using the `VkPipelineViewportStateCreateInfo` struct. This makes the viewport and scissor rectangle for this pipeline immutable. -Any changes required to these values would require a new pipeline to be created with the new values. +동적 상태를 사용하지 않는다면, 뷰포트와 시저 사각형은 `VkPipelineViewportStateCreateInfo` 구조체를 사용하여 파이프라인에 설정되어야 합니다. 이는 이 파이프라인의 뷰포트와 시저 사각형을 불변으로 만듭니다. 이 값들을 변경하려면 새 값으로 새 파이프라인을 생성해야 합니다. ```c++ VkPipelineViewportStateCreateInfo viewportState{}; @@ -175,17 +127,11 @@ viewportState.scissorCount = 1; viewportState.pScissors = &scissor; ``` -Independent of how you set them, it's possible to use multiple viewports and scissor rectangles on some graphics cards, so the structure members reference an array of them. Using multiple requires enabling a GPU feature (see logical device creation). +어떻게 설정하든, 일부 그래픽 카드에서는 여러 개의 뷰포트와 시저 사각형을 사용할 수 있으며, 이를 위해 구조체 멤버들이 배열을 참조합니다. 여러 개를 사용하려면 GPU 기능을 활성화해야 합니다(논리 장치 생성 참조). -## Rasterizer +## 래스터라이저 (Rasterizer) -The rasterizer takes the geometry that is shaped by the vertices from the vertex -shader and turns it into fragments to be colored by the fragment shader. It also -performs [depth testing](https://en.wikipedia.org/wiki/Z-buffering), -[face culling](https://en.wikipedia.org/wiki/Back-face_culling) and the scissor -test, and it can be configured to output fragments that fill entire polygons or -just the edges (wireframe rendering). All this is configured using the -`VkPipelineRasterizationStateCreateInfo` structure. +래스터라이저는 정점 셰이더에서 만들어진 지오메트리를 가져와 프래그먼트 셰이더에서 색상을 칠할 프래그먼트(fragment)로 변환합니다. 또한 [깊이 테스팅](https://ko.wikipedia.org/wiki/Z-버퍼링), [면 컬링](https://ko.wikipedia.org/wiki/후면_추려내기), 시저 테스트를 수행하며, 폴리곤 전체를 채우는 프래그먼트나 가장자리만(와이어프레임 렌더링) 출력하도록 구성할 수 있습니다. 이 모든 것은 `VkPipelineRasterizationStateCreateInfo` 구조체를 사용하여 구성됩니다. ```c++ VkPipelineRasterizationStateCreateInfo rasterizer{}; @@ -193,50 +139,38 @@ rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; rasterizer.depthClampEnable = VK_FALSE; ``` -If `depthClampEnable` is set to `VK_TRUE`, then fragments that are beyond the -near and far planes are clamped to them as opposed to discarding them. This is -useful in some special cases like shadow maps. Using this requires enabling a -GPU feature. +`depthClampEnable`이 `VK_TRUE`로 설정되면, 근평면(near plane)과 원평면(far plane)을 벗어나는 프래그먼트는 버려지는 대신 평면에 클램핑됩니다. 이는 섀도우 맵과 같은 일부 특수한 경우에 유용합니다. 이를 사용하려면 GPU 기능을 활성화해야 합니다. ```c++ rasterizer.rasterizerDiscardEnable = VK_FALSE; ``` -If `rasterizerDiscardEnable` is set to `VK_TRUE`, then geometry never passes -through the rasterizer stage. This basically disables any output to the -framebuffer. +`rasterizerDiscardEnable`이 `VK_TRUE`로 설정되면, 지오메트리는 래스터라이저 단계를 통과하지 않습니다. 이는 기본적으로 프레임버퍼로의 모든 출력을 비활성화합니다. ```c++ rasterizer.polygonMode = VK_POLYGON_MODE_FILL; ``` -The `polygonMode` determines how fragments are generated for geometry. The -following modes are available: +`polygonMode`는 지오메트리에 대해 프래그먼트가 생성되는 방식을 결정합니다. 사용 가능한 모드는 다음과 같습니다. -* `VK_POLYGON_MODE_FILL`: fill the area of the polygon with fragments -* `VK_POLYGON_MODE_LINE`: polygon edges are drawn as lines -* `VK_POLYGON_MODE_POINT`: polygon vertices are drawn as points +* `VK_POLYGON_MODE_FILL`: 폴리곤 영역을 프래그먼트로 채움 +* `VK_POLYGON_MODE_LINE`: 폴리곤 가장자리를 선으로 그림 +* `VK_POLYGON_MODE_POINT`: 폴리곤 정점을 점으로 그림 -Using any mode other than fill requires enabling a GPU feature. +`fill` 이외의 모드를 사용하려면 GPU 기능을 활성화해야 합니다. ```c++ rasterizer.lineWidth = 1.0f; ``` -The `lineWidth` member is straightforward, it describes the thickness of lines -in terms of number of fragments. The maximum line width that is supported -depends on the hardware and any line thicker than `1.0f` requires you to enable -the `wideLines` GPU feature. +`lineWidth` 멤버는 직관적으로, 프래그먼트 개수 단위로 선의 두께를 나타냅니다. 지원되는 최대 선 두께는 하드웨어에 따라 다르며, `1.0f`보다 두꺼운 선을 사용하려면 `wideLines` GPU 기능을 활성화해야 합니다. ```c++ rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE; ``` -The `cullMode` variable determines the type of face culling to use. You can -disable culling, cull the front faces, cull the back faces or both. The -`frontFace` variable specifies the vertex order for faces to be considered -front-facing and can be clockwise or counterclockwise. +`cullMode` 변수는 사용할 면 컬링(face culling)의 유형을 결정합니다. 컬링을 비활성화하거나, 앞면(front face)을 컬링하거나, 뒷면(back face)을 컬링하거나, 둘 다 컬링할 수 있습니다. `frontFace` 변수는 앞면으로 간주될 면의 정점 순서를 지정하며, 시계 방향(clockwise) 또는 반시계 방향(counter-clockwise)이 될 수 있습니다. ```c++ rasterizer.depthBiasEnable = VK_FALSE; @@ -245,20 +179,11 @@ rasterizer.depthBiasClamp = 0.0f; // Optional rasterizer.depthBiasSlopeFactor = 0.0f; // Optional ``` -The rasterizer can alter the depth values by adding a constant value or biasing -them based on a fragment's slope. This is sometimes used for shadow mapping, but -we won't be using it. Just set `depthBiasEnable` to `VK_FALSE`. +래스터라이저는 상수 값을 더하거나 프래그먼트의 기울기에 따라 깊이 값을 변경할 수 있습니다. 이는 때때로 섀도우 매핑에 사용되지만, 우리는 사용하지 않을 것입니다. `depthBiasEnable`을 `VK_FALSE`로 설정하세요. -## Multisampling +## 멀티샘플링 (Multisampling) -The `VkPipelineMultisampleStateCreateInfo` struct configures multisampling, -which is one of the ways to perform [anti-aliasing](https://en.wikipedia.org/wiki/Multisample_anti-aliasing). -It works by combining the fragment shader results of multiple polygons that -rasterize to the same pixel. This mainly occurs along edges, which is also where -the most noticeable aliasing artifacts occur. Because it doesn't need to run the -fragment shader multiple times if only one polygon maps to a pixel, it is -significantly less expensive than simply rendering to a higher resolution and -then downscaling. Enabling it requires enabling a GPU feature. +`VkPipelineMultisampleStateCreateInfo` 구조체는 [안티 앨리어싱](https://ko.wikipedia.org/wiki/다중샘플링_안티에일리어싱)을 수행하는 방법 중 하나인 멀티샘플링을 구성합니다. 동일한 픽셀로 래스터화되는 여러 폴리곤의 프래그먼트 셰이더 결과를 결합하여 작동합니다. 이는 주로 가장자리에서 발생하며, 가장 눈에 띄는 앨리어싱 현상이 발생하는 곳이기도 합니다. 하나의 폴리곤만 픽셀에 매핑되는 경우에는 프래그먼트 셰이더를 여러 번 실행할 필요가 없기 때문에, 단순히 더 높은 해상도로 렌더링한 다음 다운스케일링하는 것보다 훨씬 비용이 적게 듭니다. 이를 활성화하려면 GPU 기능을 활성화해야 합니다. ```c++ VkPipelineMultisampleStateCreateInfo multisampling{}; @@ -271,29 +196,20 @@ multisampling.alphaToCoverageEnable = VK_FALSE; // Optional multisampling.alphaToOneEnable = VK_FALSE; // Optional ``` -We'll revisit multisampling in later chapter, for now let's keep it disabled. +멀티샘플링은 나중 장에서 다시 다룰 것이므로, 지금은 비활성화 상태로 두겠습니다. -## Depth and stencil testing +## 깊이 및 스텐실 테스팅 (Depth and stencil testing) -If you are using a depth and/or stencil buffer, then you also need to configure -the depth and stencil tests using `VkPipelineDepthStencilStateCreateInfo`. We -don't have one right now, so we can simply pass a `nullptr` instead of a pointer -to such a struct. We'll get back to it in the depth buffering chapter. +깊이 및/또는 스텐실 버퍼를 사용하는 경우, `VkPipelineDepthStencilStateCreateInfo`를 사용하여 깊이 및 스텐실 테스트도 구성해야 합니다. 지금은 없으므로 해당 구조체에 대한 포인터 대신 단순히 `nullptr`를 전달할 수 있습니다. 이 부분은 깊이 버퍼링 장에서 다시 다루겠습니다. -## Color blending +## 색상 혼합 (Color blending) -After a fragment shader has returned a color, it needs to be combined with the -color that is already in the framebuffer. This transformation is known as color -blending and there are two ways to do it: +프래그먼트 셰이더가 색상을 반환한 후, 프레임버퍼에 이미 있는 색상과 결합되어야 합니다. 이 변환은 색상 혼합(color blending)으로 알려져 있으며, 두 가지 방법이 있습니다. -* Mix the old and new value to produce a final color -* Combine the old and new value using a bitwise operation +* 이전 값과 새 값을 혼합하여 최종 색상을 생성 +* 비트 연산을 사용하여 이전 값과 새 값을 결합 -There are two types of structs to configure color blending. The first struct, -`VkPipelineColorBlendAttachmentState` contains the configuration per attached -framebuffer and the second struct, `VkPipelineColorBlendStateCreateInfo` -contains the *global* color blending settings. In our case we only have one -framebuffer: +색상 혼합을 구성하는 데는 두 가지 유형의 구조체가 있습니다. 첫 번째 구조체인 `VkPipelineColorBlendAttachmentState`는 연결된 각 프레임버퍼별 구성을 포함하고, 두 번째 구조체인 `VkPipelineColorBlendStateCreateInfo`는 *전역* 색상 혼합 설정을 포함합니다. 우리의 경우 프레임버퍼는 하나뿐입니다. ```c++ VkPipelineColorBlendAttachmentState colorBlendAttachment{}; @@ -307,9 +223,7 @@ colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // Optional ``` -This per-framebuffer struct allows you to configure the first way of color -blending. The operations that will be performed are best demonstrated using the -following pseudocode: +이 프레임버퍼별 구조체를 사용하면 첫 번째 방식의 색상 혼합을 구성할 수 있습니다. 수행될 연산은 다음 의사 코드로 가장 잘 설명할 수 있습니다. ```c++ if (blendEnable) { @@ -322,21 +236,16 @@ if (blendEnable) { finalColor = finalColor & colorWriteMask; ``` -If `blendEnable` is set to `VK_FALSE`, then the new color from the fragment -shader is passed through unmodified. Otherwise, the two mixing operations are -performed to compute a new color. The resulting color is AND'd with the -`colorWriteMask` to determine which channels are actually passed through. +`blendEnable`이 `VK_FALSE`로 설정되면 프래그먼트 셰이더의 새 색상이 수정 없이 그대로 전달됩니다. 그렇지 않으면 두 혼합 연산이 수행되어 새 색상을 계산합니다. 결과 색상은 `colorWriteMask`와 AND 연산되어 실제로 어떤 채널이 통과할지 결정됩니다. -The most common way to use color blending is to implement alpha blending, where -we want the new color to be blended with the old color based on its opacity. The -`finalColor` should then be computed as follows: +색상 혼합을 사용하는 가장 일반적인 방법은 알파 블렌딩을 구현하는 것입니다. 여기서 우리는 새 색상이 불투명도에 따라 이전 색상과 혼합되기를 원합니다. `finalColor`는 다음과 같이 계산되어야 합니다. ```c++ finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor; finalColor.a = newAlpha.a; ``` -This can be accomplished with the following parameters: +이는 다음 매개변수로 달성할 수 있습니다. ```c++ colorBlendAttachment.blendEnable = VK_TRUE; @@ -348,12 +257,9 @@ colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; ``` -You can find all of the possible operations in the `VkBlendFactor` and -`VkBlendOp` enumerations in the specification. +모든 가능한 연산은 명세서의 `VkBlendFactor` 및 `VkBlendOp` 열거형에서 찾을 수 있습니다. -The second structure references the array of structures for all of the -framebuffers and allows you to set blend constants that you can use as blend -factors in the aforementioned calculations. +두 번째 구조체는 모든 프레임버퍼에 대한 구조체 배열을 참조하며, 앞서 언급한 계산에서 혼합 인자로 사용할 수 있는 블렌드 상수를 설정할 수 있습니다. ```c++ VkPipelineColorBlendStateCreateInfo colorBlending{}; @@ -368,35 +274,21 @@ colorBlending.blendConstants[2] = 0.0f; // Optional colorBlending.blendConstants[3] = 0.0f; // Optional ``` -If you want to use the second method of blending (bitwise combination), then you -should set `logicOpEnable` to `VK_TRUE`. The bitwise operation can then be -specified in the `logicOp` field. Note that this will automatically disable the -first method, as if you had set `blendEnable` to `VK_FALSE` for every -attached framebuffer! The `colorWriteMask` will also be used in this mode to -determine which channels in the framebuffer will actually be affected. It is -also possible to disable both modes, as we've done here, in which case the -fragment colors will be written to the framebuffer unmodified. +두 번째 혼합 방법(비트 조합)을 사용하려면 `logicOpEnable`을 `VK_TRUE`로 설정해야 합니다. 비트 연산은 `logicOp` 필드에서 지정할 수 있습니다. 이렇게 하면 연결된 모든 프레임버퍼에 대해 `blendEnable`을 `VK_FALSE`로 설정한 것처럼 첫 번째 방법이 자동으로 비활성화됩니다! `colorWriteMask`는 이 모드에서도 프레임버퍼의 어떤 채널이 실제로 영향을 받을지 결정하는 데 사용됩니다. 여기서처럼 두 모드를 모두 비활성화하는 것도 가능하며, 이 경우 프래그먼트 색상이 수정 없이 프레임버퍼에 쓰여집니다. -## Pipeline layout +## 파이프라인 레이아웃 (Pipeline layout) -You can use `uniform` values in shaders, which are globals similar to dynamic -state variables that can be changed at drawing time to alter the behavior of -your shaders without having to recreate them. They are commonly used to pass the -transformation matrix to the vertex shader, or to create texture samplers in the -fragment shader. +셰이더에서 `uniform` 값을 사용할 수 있습니다. 이는 동적 상태 변수와 유사한 전역 변수로, 드로잉 시점에 변경하여 셰이더를 다시 만들지 않고도 동작을 변경할 수 있습니다. 일반적으로 변환 행렬을 정점 셰이더에 전달하거나 프래그먼트 셰이더에서 텍스처 샘플러를 생성하는 데 사용됩니다. -These uniform values need to be specified during pipeline creation by creating a -`VkPipelineLayout` object. Even though we won't be using them until a future -chapter, we are still required to create an empty pipeline layout. +이러한 uniform 값은 파이프라인 생성 중에 `VkPipelineLayout` 객체를 생성하여 지정해야 합니다. 나중 장까지는 사용하지 않겠지만, 비어있는 파이프라인 레이아웃이라도 반드시 생성해야 합니다. -Create a class member to hold this object, because we'll refer to it from other -functions at a later point in time: +나중에 다른 함수에서 이 객체를 참조할 것이므로 클래스 멤버를 만들어 이 객체를 저장합니다. ```c++ VkPipelineLayout pipelineLayout; ``` -And then create the object in the `createGraphicsPipeline` function: +그리고 `createGraphicsPipeline` 함수에서 객체를 생성합니다. ```c++ VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; @@ -411,10 +303,7 @@ if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout } ``` -The structure also specifies *push constants*, which are another way of passing -dynamic values to shaders that we may get into in a future chapter. The pipeline -layout will be referenced throughout the program's lifetime, so it should be -destroyed at the end: +이 구조체는 *푸시 상수(push constants)*도 지정하는데, 이는 셰이더에 동적 값을 전달하는 또 다른 방법으로, 나중에 다룰 수 있습니다. 파이프라인 레이아웃은 프로그램 수명 내내 참조되므로 마지막에 파괴되어야 합니다. ```c++ void cleanup() { @@ -423,17 +312,12 @@ void cleanup() { } ``` -## Conclusion +## 결론 -That's it for all of the fixed-function state! It's a lot of work to set all of -this up from scratch, but the advantage is that we're now nearly fully aware of -everything that is going on in the graphics pipeline! This reduces the chance of -running into unexpected behavior because the default state of certain components -is not what you expect. +이것으로 모든 고정 함수 상태에 대한 설정이 끝났습니다! 모든 것을 처음부터 설정하는 것은 많은 작업이지만, 그 장점은 이제 그래픽 파이프라인에서 일어나는 거의 모든 일을 완전히 인지하게 되었다는 것입니다! 이는 특정 컴포넌트의 기본 상태가 예상과 달라 예기치 않은 동작에 부딪힐 가능성을 줄여줍니다. -There is however one more object to create before we can finally create the -graphics pipeline and that is a [render pass](!en/Drawing_a_triangle/Graphics_pipeline_basics/Render_passes). +하지만 그래픽 파이프라인을 최종적으로 생성하기 전에 만들어야 할 객체가 하나 더 있으며, 그것은 바로 [렌더 패스](!en/Drawing_a_triangle/Graphics_pipeline_basics/Render_passes)입니다. -[C++ code](/code/10_fixed_functions.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +[C++ 코드](/code/10_fixed_functions.cpp) / +[정점 셰이더](/code/09_shader_base.vert) / +[프래그먼트 셰이더](/code/09_shader_base.frag) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md index a635d32f..076f6927 100644 --- a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md @@ -1,12 +1,6 @@ -## Setup +## 설정 -Before we can finish creating the pipeline, we need to tell Vulkan about the -framebuffer attachments that will be used while rendering. We need to specify -how many color and depth buffers there will be, how many samples to use for each -of them and how their contents should be handled throughout the rendering -operations. All of this information is wrapped in a *render pass* object, for -which we'll create a new `createRenderPass` function. Call this function from -`initVulkan` before `createGraphicsPipeline`. +파이프라인 생성을 완료하기 전에, Vulkan에게 렌더링 중에 사용될 프레임버퍼 어태치먼트(attachment)에 대해 알려주어야 합니다. 우리는 몇 개의 색상 및 깊이 버퍼가 있을지, 각각에 몇 개의 샘플을 사용할지, 그리고 렌더링 작업 전반에 걸쳐 해당 콘텐츠를 어떻게 처리해야 하는지 지정해야 합니다. 이 모든 정보는 *렌더 패스(render pass)* 객체에 담기게 되며, 이를 위해 새로운 `createRenderPass` 함수를 만들 것입니다. 이 함수를 `initVulkan`에서 `createGraphicsPipeline` 앞에 호출하세요. ```c++ void initVulkan() { @@ -28,10 +22,9 @@ void createRenderPass() { } ``` -## Attachment description +## 어태치먼트 명세 (Attachment description) -In our case we'll have just a single color buffer attachment represented by one -of the images from the swap chain. +우리의 경우, 스왑체인의 이미지 중 하나로 표현되는 단일 색상 버퍼 어태치먼트만 갖게 될 것입니다. ```c++ void createRenderPass() { @@ -41,89 +34,55 @@ void createRenderPass() { } ``` -The `format` of the color attachment should match the format of the swap chain -images, and we're not doing anything with multisampling yet, so we'll stick to 1 -sample. +색상 어태치먼트의 `format`은 스왑체인 이미지의 포맷과 일치해야 하며, 아직 멀티샘플링은 다루지 않으므로 1개의 샘플을 사용하겠습니다. ```c++ colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; ``` -The `loadOp` and `storeOp` determine what to do with the data in the attachment -before rendering and after rendering. We have the following choices for -`loadOp`: +`loadOp`와 `storeOp`는 렌더링 전후에 어태치먼트의 데이터를 어떻게 처리할지를 결정합니다. `loadOp`에 사용할 수 있는 선택지는 다음과 같습니다: -* `VK_ATTACHMENT_LOAD_OP_LOAD`: Preserve the existing contents of the attachment -* `VK_ATTACHMENT_LOAD_OP_CLEAR`: Clear the values to a constant at the start -* `VK_ATTACHMENT_LOAD_OP_DONT_CARE`: Existing contents are undefined; we don't -care about them +* `VK_ATTACHMENT_LOAD_OP_LOAD`: 어태치먼트의 기존 내용을 보존합니다. +* `VK_ATTACHMENT_LOAD_OP_CLEAR`: 시작 시 값을 특정 상수로 지웁니다. +* `VK_ATTACHMENT_LOAD_OP_DONT_CARE`: 기존 내용이 정의되지 않음(undefined); 신경 쓰지 않습니다. -In our case we're going to use the clear operation to clear the framebuffer to -black before drawing a new frame. There are only two possibilities for the -`storeOp`: +우리의 경우, 새 프레임을 그리기 전에 프레임버퍼를 검은색으로 지우기 위해 clear 작업을 사용할 것입니다. `storeOp`에는 두 가지 가능성만 있습니다: -* `VK_ATTACHMENT_STORE_OP_STORE`: Rendered contents will be stored in memory and -can be read later -* `VK_ATTACHMENT_STORE_OP_DONT_CARE`: Contents of the framebuffer will be -undefined after the rendering operation +* `VK_ATTACHMENT_STORE_OP_STORE`: 렌더링된 내용은 메모리에 저장되어 나중에 읽을 수 있습니다. +* `VK_ATTACHMENT_STORE_OP_DONT_CARE`: 렌더링 작업 후 프레임버퍼의 내용은 정의되지 않습니다. -We're interested in seeing the rendered triangle on the screen, so we're going -with the store operation here. +우리는 렌더링된 삼각형을 화면에서 보고 싶으므로, store 작업을 사용할 것입니다. ```c++ colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; ``` -The `loadOp` and `storeOp` apply to color and depth data, and `stencilLoadOp` / -`stencilStoreOp` apply to stencil data. Our application won't do anything with -the stencil buffer, so the results of loading and storing are irrelevant. +`loadOp`와 `storeOp`는 색상 및 깊이 데이터에 적용되며, `stencilLoadOp` / `stencilStoreOp`는 스텐실 데이터에 적용됩니다. 우리 애플리케이션은 스텐실 버퍼를 사용하지 않으므로, 로딩과 저장 결과는 중요하지 않습니다. ```c++ colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; ``` -Textures and framebuffers in Vulkan are represented by `VkImage` objects with a -certain pixel format, however the layout of the pixels in memory can change -based on what you're trying to do with an image. - -Some of the most common layouts are: - -* `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`: Images used as color attachment -* `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`: Images to be presented in the swap chain -* `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`: Images to be used as destination for a -memory copy operation - -We'll discuss this topic in more depth in the texturing chapter, but what's -important to know right now is that images need to be transitioned to specific -layouts that are suitable for the operation that they're going to be involved in -next. - -The `initialLayout` specifies which layout the image will have before the render -pass begins. The `finalLayout` specifies the layout to automatically transition -to when the render pass finishes. Using `VK_IMAGE_LAYOUT_UNDEFINED` for -`initialLayout` means that we don't care what previous layout the image was in. -The caveat of this special value is that the contents of the image are not -guaranteed to be preserved, but that doesn't matter since we're going to clear -it anyway. We want the image to be ready for presentation using the swap chain -after rendering, which is why we use `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR` as -`finalLayout`. - -## Subpasses and attachment references - -A single render pass can consist of multiple subpasses. Subpasses are subsequent -rendering operations that depend on the contents of framebuffers in previous -passes, for example a sequence of post-processing effects that are applied one -after another. If you group these rendering operations into one render pass, -then Vulkan is able to reorder the operations and conserve memory bandwidth for -possibly better performance. For our very first triangle, however, we'll stick -to a single subpass. - -Every subpass references one or more of the attachments that we've described -using the structure in the previous sections. These references are themselves -`VkAttachmentReference` structs that look like this: +Vulkan에서 텍스처와 프레임버퍼는 특정 픽셀 포맷을 가진 `VkImage` 객체로 표현됩니다. 하지만 이미지로 무엇을 하려는지에 따라 메모리 내 픽셀의 레이아웃이 변경될 수 있습니다. + +가장 일반적인 레이아웃 중 일부는 다음과 같습니다: + +* `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`: 색상 어태치먼트로 사용되는 이미지 +* `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`: 스왑체인에 표시(present)될 이미지 +* `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`: 메모리 복사 작업의 대상으로 사용될 이미지 + +이 주제는 텍스처링 장에서 더 깊이 다룰 것이지만, 지금 알아야 할 중요한 점은 이미지는 다음에 수행할 작업에 적합한 특정 레이아웃으로 전환되어야 한다는 점입니다. + +`initialLayout`은 렌더 패스가 시작되기 전에 이미지가 어떤 레이아웃을 가질지 지정합니다. `finalLayout`은 렌더 패스가 끝날 때 자동으로 전환될 레이아웃을 지정합니다. `initialLayout`에 `VK_IMAGE_LAYOUT_UNDEFINED`를 사용하면 이미지의 이전 레이아웃이 무엇이었는지 신경 쓰지 않겠다는 의미입니다. 이 특별한 값의 주의할 점은 이미지의 내용이 보존된다는 보장이 없다는 것이지만, 어차피 우리가 내용을 지울 것이기 때문에 문제 되지 않습니다. 우리는 렌더링 후에 이미지가 스왑체인을 통해 화면에 표시될 준비가 되기를 원하므로, `finalLayout`으로 `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`을 사용합니다. + +## 서브패스와 어태치먼트 참조 + +하나의 렌더 패스는 여러 개의 서브패스(subpass)로 구성될 수 있습니다. 서브패스는 이전 패스의 프레임버퍼 내용에 의존하는 후속 렌더링 작업입니다. 예를 들어, 연달아 적용되는 일련의 후처리 효과 같은 것들입니다. 이러한 렌더링 작업들을 하나의 렌더 패스로 그룹화하면, Vulkan은 연산을 재정렬하고 메모리 대역폭을 절약하여 성능을 향상시킬 수 있습니다. 하지만 우리의 첫 번째 삼각형에서는 단일 서브패스만 사용할 것입니다. + +모든 서브패스는 이전 섹션에서 설명한 구조체를 사용하여 명세한 어태치먼트 중 하나 이상을 참조합니다. 이러한 참조 자체는 다음과 같은 `VkAttachmentReference` 구조체입니다. ```c++ VkAttachmentReference colorAttachmentRef{}; @@ -131,57 +90,41 @@ colorAttachmentRef.attachment = 0; colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; ``` -The `attachment` parameter specifies which attachment to reference by its index -in the attachment descriptions array. Our array consists of a single -`VkAttachmentDescription`, so its index is `0`. The `layout` specifies which -layout we would like the attachment to have during a subpass that uses this -reference. Vulkan will automatically transition the attachment to this layout -when the subpass is started. We intend to use the attachment to function as a -color buffer and the `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL` layout will give -us the best performance, as its name implies. +`attachment` 파라미터는 어태치먼트 명세 배열의 인덱스를 통해 참조할 어태치먼트를 지정합니다. 우리 배열은 단일 `VkAttachmentDescription`으로 구성되어 있으므로 인덱스는 `0`입니다. `layout`은 이 참조를 사용하는 서브패스 동안 어태치먼트가 가지길 원하는 레이아웃을 지정합니다. Vulkan은 서브패스가 시작될 때 자동으로 어태치먼트를 이 레이아웃으로 전환합니다. 우리는 이 어태치먼트를 색상 버퍼로 사용할 것이며, 이름에서 알 수 있듯이 `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL` 레이아웃이 최상의 성능을 제공할 것입니다. -The subpass is described using a `VkSubpassDescription` structure: +서브패스는 `VkSubpassDescription` 구조체를 사용하여 설명됩니다: ```c++ VkSubpassDescription subpass{}; subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; ``` -Vulkan may also support compute subpasses in the future, so we have to be -explicit about this being a graphics subpass. Next, we specify the reference to -the color attachment: +Vulkan은 미래에 컴퓨트 서브패스도 지원할 수 있으므로, 이것이 그래픽스 서브패스임을 명시적으로 지정해야 합니다. 다음으로, 색상 어태치먼트에 대한 참조를 지정합니다. ```c++ subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &colorAttachmentRef; ``` -The index of the attachment in this array is directly referenced from the -fragment shader with the `layout(location = 0) out vec4 outColor` directive! +이 배열에서 어태치먼트의 인덱스는 프래그먼트 셰이더에서 `layout(location = 0) out vec4 outColor` 지시문을 통해 직접 참조됩니다! -The following other types of attachments can be referenced by a subpass: +서브패스에서 참조할 수 있는 다른 유형의 어태치먼트는 다음과 같습니다: -* `pInputAttachments`: Attachments that are read from a shader -* `pResolveAttachments`: Attachments used for multisampling color attachments -* `pDepthStencilAttachment`: Attachment for depth and stencil data -* `pPreserveAttachments`: Attachments that are not used by this subpass, but for -which the data must be preserved +* `pInputAttachments`: 셰이더에서 읽어오는 어태치먼트 +* `pResolveAttachments`: 멀티샘플링된 색상 어태치먼트를 위해 사용되는 어태치먼트 +* `pDepthStencilAttachment`: 깊이 및 스텐실 데이터를 위한 어태치먼트 +* `pPreserveAttachments`: 이 서브패스에서는 사용되지 않지만 데이터가 보존되어야 하는 어태치먼트 -## Render pass +## 렌더 패스 -Now that the attachment and a basic subpass referencing it have been described, -we can create the render pass itself. Create a new class member variable to hold -the `VkRenderPass` object right above the `pipelineLayout` variable: +이제 어태치먼트와 이를 참조하는 기본 서브패스가 설명되었으므로, 렌더 패스 자체를 생성할 수 있습니다. `pipelineLayout` 변수 바로 위에 `VkRenderPass` 객체를 담을 새로운 클래스 멤버 변수를 만드세요. ```c++ VkRenderPass renderPass; VkPipelineLayout pipelineLayout; ``` -The render pass object can then be created by filling in the -`VkRenderPassCreateInfo` structure with an array of attachments and subpasses. -The `VkAttachmentReference` objects reference attachments using the indices of -this array. +그런 다음 `VkRenderPassCreateInfo` 구조체를 어태치먼트와 서브패스 배열로 채워서 렌더 패스 객체를 생성할 수 있습니다. `VkAttachmentReference` 객체들은 이 배열의 인덱스를 사용하여 어태치먼트를 참조합니다. ```c++ VkRenderPassCreateInfo renderPassInfo{}; @@ -196,8 +139,7 @@ if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCC } ``` -Just like the pipeline layout, the render pass will be referenced throughout the -program, so it should only be cleaned up at the end: +파이프라인 레이아웃과 마찬가지로 렌더 패스는 프로그램 전반에서 참조되므로, 프로그램이 끝날 때 정리해야 합니다. ```c++ void cleanup() { @@ -207,9 +149,8 @@ void cleanup() { } ``` -That was a lot of work, but in the next chapter it all comes together to finally -create the graphics pipeline object! +상당히 많은 작업이었지만, 다음 장에서는 이 모든 것이 합쳐져 마침내 그래픽스 파이프라인 객체를 생성하게 될 것입니다! -[C++ code](/code/11_render_passes.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +[C++ 코드](/code/11_render_passes.cpp) / +[버텍스 셰이더](/code/09_shader_base.vert) / +[프래그먼트 셰이더](/code/09_shader_base.frag) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md index 4a16585e..8baa3f86 100644 --- a/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md @@ -1,20 +1,11 @@ -We can now combine all of the structures and objects from the previous chapters -to create the graphics pipeline! Here's the types of objects we have now, as a -quick recap: - -* Shader stages: the shader modules that define the functionality of the -programmable stages of the graphics pipeline -* Fixed-function state: all of the structures that define the fixed-function -stages of the pipeline, like input assembly, rasterizer, viewport and color -blending -* Pipeline layout: the uniform and push values referenced by the shader that can -be updated at draw time -* Render pass: the attachments referenced by the pipeline stages and their usage - -All of these combined fully define the functionality of the graphics pipeline, -so we can now begin filling in the `VkGraphicsPipelineCreateInfo` structure at -the end of the `createGraphicsPipeline` function. But before the calls to -`vkDestroyShaderModule` because these are still to be used during the creation. +이제 이전 챕터들에서 다룬 모든 구조체와 객체를 조합하여 그래픽스 파이프라인을 만들 수 있습니다! 우리가 지금까지 다룬 객체 유형을 간단히 요약하면 다음과 같습니다: + +* **셰이더 스테이지(Shader stages)**: 그래픽스 파이프라인의 프로그래밍 가능한 스테이지의 기능을 정의하는 셰이더 모듈 +* **고정 함수 상태(Fixed-function state)**: 입력 어셈블리, 래스터라이저, 뷰포트, 색상 혼합과 같이 파이프라인의 고정 함수 스테이지를 정의하는 모든 구조체 +* **파이프라인 레이아웃(Pipeline layout)**: 셰이더에서 참조하며 드로우 타임에 업데이트할 수 있는 유니폼 및 푸시 값 +* **렌더 패스(Render pass)**: 파이프라인 스테이지에서 참조하는 어태치먼트와 그 사용법 + +이 모든 것을 합치면 그래픽스 파이프라인의 기능이 완벽하게 정의됩니다. 따라서 이제 `createGraphicsPipeline` 함수의 끝부분에서 `VkGraphicsPipelineCreateInfo` 구조체를 채워 넣기 시작할 수 있습니다. 단, 셰이더 모듈은 파이프라인 생성 중에 여전히 사용되므로 `vkDestroyShaderModule` 호출보다는 앞에 위치해야 합니다. ```c++ VkGraphicsPipelineCreateInfo pipelineInfo{}; @@ -23,7 +14,7 @@ pipelineInfo.stageCount = 2; pipelineInfo.pStages = shaderStages; ``` -We start by referencing the array of `VkPipelineShaderStageCreateInfo` structs. +먼저 `VkPipelineShaderStageCreateInfo` 구조체 배열을 참조하는 것으로 시작합니다. ```c++ pipelineInfo.pVertexInputState = &vertexInputInfo; @@ -31,57 +22,40 @@ pipelineInfo.pInputAssemblyState = &inputAssembly; pipelineInfo.pViewportState = &viewportState; pipelineInfo.pRasterizationState = &rasterizer; pipelineInfo.pMultisampleState = &multisampling; -pipelineInfo.pDepthStencilState = nullptr; // Optional +pipelineInfo.pDepthStencilState = nullptr; // 선택 사항 pipelineInfo.pColorBlendState = &colorBlending; pipelineInfo.pDynamicState = &dynamicState; ``` -Then we reference all of the structures describing the fixed-function stage. +그다음, 고정 함수 스테이지를 설명하는 모든 구조체를 참조합니다. ```c++ pipelineInfo.layout = pipelineLayout; ``` -After that comes the pipeline layout, which is a Vulkan handle rather than a -struct pointer. +그다음은 파이프라인 레이아웃인데, 이것은 구조체 포인터가 아닌 Vulkan 핸들입니다. ```c++ pipelineInfo.renderPass = renderPass; pipelineInfo.subpass = 0; ``` -And finally we have the reference to the render pass and the index of the sub -pass where this graphics pipeline will be used. It is also possible to use other -render passes with this pipeline instead of this specific instance, but they -have to be *compatible* with `renderPass`. The requirements for compatibility -are described [here](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap8.html#renderpass-compatibility), -but we won't be using that feature in this tutorial. +마지막으로 렌더 패스와, 이 그래픽스 파이프라인이 사용될 서브패스의 인덱스에 대한 참조가 있습니다. 이 파이프라인을 이 특정 인스턴스 대신 다른 렌더 패스와 함께 사용하는 것도 가능하지만, 그 렌더 패스들은 `renderPass`와 *호환 가능(compatible)*해야 합니다. 호환성 요구 사항은 [여기](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap8.html#renderpass-compatibility)에 설명되어 있지만, 이 튜토리얼에서는 해당 기능을 사용하지 않을 것입니다. ```c++ -pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional -pipelineInfo.basePipelineIndex = -1; // Optional +pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // 선택 사항 +pipelineInfo.basePipelineIndex = -1; // 선택 사항 ``` -There are actually two more parameters: `basePipelineHandle` and -`basePipelineIndex`. Vulkan allows you to create a new graphics pipeline by -deriving from an existing pipeline. The idea of pipeline derivatives is that it -is less expensive to set up pipelines when they have much functionality in -common with an existing pipeline and switching between pipelines from the same -parent can also be done quicker. You can either specify the handle of an -existing pipeline with `basePipelineHandle` or reference another pipeline that -is about to be created by index with `basePipelineIndex`. Right now there is -only a single pipeline, so we'll simply specify a null handle and an invalid -index. These values are only used if the `VK_PIPELINE_CREATE_DERIVATIVE_BIT` -flag is also specified in the `flags` field of `VkGraphicsPipelineCreateInfo`. - -Now prepare for the final step by creating a class member to hold the -`VkPipeline` object: +실제로는 두 개의 매개변수가 더 있습니다: `basePipelineHandle`과 `basePipelineIndex`. Vulkan에서는 기존 파이프라인에서 파생하여 새로운 그래픽스 파이프라인을 생성할 수 있습니다. 파이프라인 파생(derivatives)의 개념은, 기존 파이프라인과 많은 기능이 공통될 때 파이프라인을 설정하는 비용이 저렴해지고, 동일한 부모에서 파생된 파이프라인 간의 전환도 더 빠르게 수행할 수 있다는 것입니다. `basePipelineHandle`로 기존 파이프라인의 핸들을 지정하거나, `basePipelineIndex`로 지금 생성하려는 다른 파이프라인을 인덱스로 참조할 수 있습니다. 지금은 파이프라인이 하나뿐이므로, null 핸들과 유효하지 않은 인덱스를 지정하겠습니다. 이 값들은 `VkGraphicsPipelineCreateInfo`의 `flags` 필드에 `VK_PIPELINE_CREATE_DERIVATIVE_BIT` 플래그가 지정된 경우에만 사용됩니다. + +이제 마지막 단계를 위해 `VkPipeline` 객체를 담을 클래스 멤버를 만듭니다: ```c++ VkPipeline graphicsPipeline; ``` -And finally create the graphics pipeline: +그리고 마침내 그래픽스 파이프라인을 생성합니다: ```c++ if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) { @@ -89,20 +63,11 @@ if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, } ``` -The `vkCreateGraphicsPipelines` function actually has more parameters than the -usual object creation functions in Vulkan. It is designed to take multiple -`VkGraphicsPipelineCreateInfo` objects and create multiple `VkPipeline` objects -in a single call. +`vkCreateGraphicsPipelines` 함수는 사실 Vulkan의 일반적인 객체 생성 함수들보다 매개변수가 더 많습니다. 이 함수는 여러 개의 `VkGraphicsPipelineCreateInfo` 객체를 받아 한 번의 호출로 여러 `VkPipeline` 객체를 생성하도록 설계되었습니다. -The second parameter, for which we've passed the `VK_NULL_HANDLE` argument, -references an optional `VkPipelineCache` object. A pipeline cache can be used to -store and reuse data relevant to pipeline creation across multiple calls to -`vkCreateGraphicsPipelines` and even across program executions if the cache is -stored to a file. This makes it possible to significantly speed up pipeline -creation at a later time. We'll get into this in the pipeline cache chapter. +우리가 `VK_NULL_HANDLE` 인자를 전달한 두 번째 매개변수는 선택적인 `VkPipelineCache` 객체를 참조합니다. 파이프라인 캐시는 여러 `vkCreateGraphicsPipelines` 호출에 걸쳐 파이프라인 생성과 관련된 데이터를 저장하고 재사용하는 데 사용될 수 있으며, 캐시를 파일에 저장하면 프로그램 실행 간에도 재사용할 수 있습니다. 이렇게 하면 나중에 파이프라인 생성 속도를 크게 높일 수 있습니다. 이 내용은 파이프라인 캐시 챕터에서 다룰 것입니다. -The graphics pipeline is required for all common drawing operations, so it -should also only be destroyed at the end of the program: +그래픽스 파이프라인은 모든 일반적인 드로잉 작업에 필요하므로 프로그램이 끝날 때 파괴되어야 합니다. ```c++ void cleanup() { @@ -112,11 +77,8 @@ void cleanup() { } ``` -Now run your program to confirm that all this hard work has resulted in a -successful pipeline creation! We are already getting quite close to seeing -something pop up on the screen. In the next couple of chapters we'll set up the -actual framebuffers from the swap chain images and prepare the drawing commands. +이제 프로그램을 실행하여 이 모든 노력이 성공적인 파이프라인 생성으로 이어졌는지 확인하세요! 이제 화면에 무언가 나타나는 것에 꽤 가까워졌습니다. 다음 몇 개의 챕터에서는 스왑 체인 이미지로부터 실제 프레임버퍼를 설정하고 드로잉 커맨드를 준비할 것입니다. -[C++ code](/code/12_graphics_pipeline_complete.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +[C++ 코드](/code/12_graphics_pipeline_complete.cpp) / +[정점 셰이더](/code/09_shader_base.vert) / +[프래그먼트 셰이더](/code/09_shader_base.frag) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md b/ko/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md index bf7f84a7..b73b8dc4 100644 --- a/ko/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md +++ b/ko/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md @@ -1,24 +1,16 @@ -We've talked a lot about framebuffers in the past few chapters and we've set up -the render pass to expect a single framebuffer with the same format as the swap -chain images, but we haven't actually created any yet. +### 프레임버퍼 -The attachments specified during render pass creation are bound by wrapping them -into a `VkFramebuffer` object. A framebuffer object references all of the -`VkImageView` objects that represent the attachments. In our case that will be -only a single one: the color attachment. However, the image that we have to use -for the attachment depends on which image the swap chain returns when we retrieve one -for presentation. That means that we have to create a framebuffer for all of the -images in the swap chain and use the one that corresponds to the retrieved image -at drawing time. +지난 몇 장에 걸쳐 프레임버퍼에 대해 많이 이야기했고, 스왑 체인 이미지와 동일한 포맷을 가진 단일 프레임버퍼를 사용하도록 렌더 패스를 설정했지만, 아직 실제로 생성하지는 않았습니다. -To that end, create another `std::vector` class member to hold the framebuffers: +렌더 패스를 생성할 때 지정한 첨부(attachment)들은 `VkFramebuffer` 객체로 감싸서 바인딩됩니다. 프레임버퍼 객체는 첨부를 나타내는 모든 `VkImageView` 객체를 참조합니다. 우리의 경우에는 단 하나, 바로 색상 첨부(color attachment)입니다. 하지만 첨부에 사용해야 할 이미지는 우리가 프레젠테이션을 위해 스왑 체인에서 이미지를 가져올 때 어떤 이미지를 반환하는지에 따라 달라집니다. 이는 스왑 체인의 모든 이미지에 대해 프레임버퍼를 생성하고, 드로잉 시점에는 가져온 이미지에 해당하는 것을 사용해야 한다는 의미입니다. + +이를 위해, 프레임버퍼를 담을 또 다른 `std::vector` 클래스 멤버를 생성합니다: ```c++ std::vector swapChainFramebuffers; ``` -We'll create the objects for this array in a new function `createFramebuffers` -that is called from `initVulkan` right after creating the graphics pipeline: +이 배열을 위한 객체들은 `initVulkan`에서 그래픽 파이프라인을 생성한 직후에 호출되는 새로운 함수 `createFramebuffers`에서 생성할 것입니다: ```c++ void initVulkan() { @@ -41,7 +33,7 @@ void createFramebuffers() { } ``` -Start by resizing the container to hold all of the framebuffers: +먼저 컨테이너의 크기를 조절하여 모든 프레임버퍼를 담을 수 있도록 합니다: ```c++ void createFramebuffers() { @@ -49,7 +41,7 @@ void createFramebuffers() { } ``` -We'll then iterate through the image views and create framebuffers from them: +그런 다음 이미지 뷰를 순회하며 프레임버퍼를 생성합니다: ```c++ for (size_t i = 0; i < swapChainImageViews.size(); i++) { @@ -72,21 +64,13 @@ for (size_t i = 0; i < swapChainImageViews.size(); i++) { } ``` -As you can see, creation of framebuffers is quite straightforward. We first need -to specify with which `renderPass` the framebuffer needs to be compatible. You -can only use a framebuffer with the render passes that it is compatible with, -which roughly means that they use the same number and type of attachments. +보시다시피, 프레임버퍼 생성은 매우 간단합니다. 먼저 프레임버퍼가 어떤 `renderPass`와 호환되어야 하는지 지정해야 합니다. 프레임버퍼는 호환되는 렌더 패스와만 사용할 수 있는데, 이는 대략적으로 말해 동일한 수와 유형의 첨부를 사용한다는 것을 의미합니다. -The `attachmentCount` and `pAttachments` parameters specify the `VkImageView` -objects that should be bound to the respective attachment descriptions in -the render pass `pAttachment` array. +`attachmentCount`와 `pAttachments` 매개변수는 렌더 패스의 `pAttachment` 배열에 있는 각 첨부 설명에 바인딩될 `VkImageView` 객체를 지정합니다. -The `width` and `height` parameters are self-explanatory and `layers` refers to -the number of layers in image arrays. Our swap chain images are single images, -so the number of layers is `1`. +`width`와 `height` 매개변수는 이름에서 알 수 있듯이 명확하며, `layers`는 이미지 배열의 레이어 수를 나타냅니다. 우리의 스왑 체인 이미지는 단일 이미지이므로 레이어 수는 `1`입니다. -We should delete the framebuffers before the image views and render pass that -they are based on, but only after we've finished rendering: +프레임버퍼는 그것들이 기반으로 하는 이미지 뷰와 렌더 패스보다 먼저 삭제되어야 하지만, 렌더링을 모두 마친 후에만 삭제해야 합니다: ```c++ void cleanup() { @@ -98,10 +82,8 @@ void cleanup() { } ``` -We've now reached the milestone where we have all of the objects that are -required for rendering. In the next chapter we're going to write the first -actual drawing commands. +이제 우리는 렌더링에 필요한 모든 객체를 갖추는 중요한 단계에 도달했습니다. 다음 장에서는 첫 실제 드로잉 명령을 작성할 것입니다. -[C++ code](/code/13_framebuffers.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +[C++ 코드](/code/13_framebuffers.cpp) / +[정점 셰이더](/code/09_shader_base.vert) / +[프래그먼트 셰이더](/code/09_shader_base.frag) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md b/ko/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md index 61a40b4f..369c70c7 100644 --- a/ko/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md +++ b/ko/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md @@ -1,23 +1,14 @@ -Commands in Vulkan, like drawing operations and memory transfers, are not -executed directly using function calls. You have to record all of the operations -you want to perform in command buffer objects. The advantage of this is that when -we are ready to tell the Vulkan what we want to do, all of the commands are -submitted together and Vulkan can more efficiently process the commands since all -of them are available together. In addition, this allows command recording to -happen in multiple threads if so desired. +Vulkan에서 그리기 연산이나 메모리 전송과 같은 커맨드(command)는 함수 호출을 통해 직접 실행되지 않습니다. 대신, 수행하려는 모든 작업을 커맨드 버퍼(command buffer) 객체에 기록(record)해야 합니다. 이 방식의 장점은 Vulkan에게 무엇을 할지 알려줄 준비가 되었을 때 모든 커맨드가 함께 제출된다는 것입니다. 그러면 Vulkan은 모든 커맨드를 한 번에 사용할 수 있으므로 더 효율적으로 처리할 수 있습니다. 또한, 원한다면 여러 스레드에서 커맨드 기록을 수행할 수도 있습니다. -## Command pools +## 커맨드 풀 (Command pools) -We have to create a command pool before we can create command buffers. Command -pools manage the memory that is used to store the buffers and command buffers -are allocated from them. Add a new class member to store a `VkCommandPool`: +커맨드 버퍼를 생성하기 전에 먼저 커맨드 풀(command pool)을 생성해야 합니다. 커맨드 풀은 버퍼를 저장하는 데 사용되는 메모리를 관리하며, 커맨드 버퍼는 이 풀에서 할당됩니다. `VkCommandPool`을 저장할 새 클래스 멤버를 추가합니다. ```c++ VkCommandPool commandPool; ``` -Then create a new function `createCommandPool` and call it from `initVulkan` -after the framebuffers were created. +그런 다음 `createCommandPool`이라는 새 함수를 만들고, `initVulkan`에서 프레임버퍼가 생성된 후에 호출합니다. ```c++ void initVulkan() { @@ -41,7 +32,7 @@ void createCommandPool() { } ``` -Command pool creation only takes two parameters: +커맨드 풀 생성에는 단 두 개의 매개변수만 필요합니다. ```c++ QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice); @@ -52,23 +43,14 @@ poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value(); ``` -There are two possible flags for command pools: +커맨드 풀에는 두 가지 가능한 플래그가 있습니다: -* `VK_COMMAND_POOL_CREATE_TRANSIENT_BIT`: Hint that command buffers are -rerecorded with new commands very often (may change memory allocation behavior) -* `VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT`: Allow command buffers to be -rerecorded individually, without this flag they all have to be reset together +* `VK_COMMAND_POOL_CREATE_TRANSIENT_BIT`: 커맨드 버퍼가 새로운 커맨드로 매우 자주 다시 기록될 것임을 암시합니다 (메모리 할당 동작이 변경될 수 있음). +* `VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT`: 커맨드 버퍼를 개별적으로 다시 기록할 수 있도록 허용합니다. 이 플래그가 없으면 모든 커맨드 버퍼를 함께 리셋해야 합니다. -We will be recording a command buffer every frame, so we want to be able to -reset and rerecord over it. Thus, we need to set the -`VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT` flag bit for our command pool. - -Command buffers are executed by submitting them on one of the device queues, -like the graphics and presentation queues we retrieved. Each command pool can -only allocate command buffers that are submitted on a single type of queue. -We're going to record commands for drawing, which is why we've chosen the -graphics queue family. +우리는 매 프레임마다 커맨드 버퍼를 기록할 것이므로, 이를 리셋하고 다시 기록할 수 있어야 합니다. 따라서 커맨드 풀에 `VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT` 플래그 비트를 설정해야 합니다. +커맨드 버퍼는 우리가 가져온 그래픽스 및 프레젠테이션 큐와 같은 장치 큐 중 하나에 제출하여 실행됩니다. 각 커맨드 풀은 단일 유형의 큐에 제출되는 커맨드 버퍼만 할당할 수 있습니다. 우리는 그리기를 위한 커맨드를 기록할 것이므로 그래픽스 큐 패밀리를 선택했습니다. ```c++ if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) { @@ -76,10 +58,7 @@ if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) } ``` -Finish creating the command pool using the `vkCreateCommandPool` function. It -doesn't have any special parameters. Commands will be used throughout the -program to draw things on the screen, so the pool should only be destroyed at -the end: +`vkCreateCommandPool` 함수를 사용하여 커맨드 풀 생성을 완료합니다. 이 함수에는 특별한 매개변수가 없습니다. 커맨드는 프로그램 전반에 걸쳐 화면에 무언가를 그리는 데 사용되므로, 풀은 프로그램이 끝날 때만 파괴되어야 합니다. ```c++ void cleanup() { @@ -89,20 +68,17 @@ void cleanup() { } ``` -## Command buffer allocation +## 커맨드 버퍼 할당 -We can now start allocating command buffers. +이제 커맨드 버퍼 할당을 시작할 수 있습니다. -Create a `VkCommandBuffer` object as a class member. Command buffers -will be automatically freed when their command pool is destroyed, so we don't -need explicit cleanup. +`VkCommandBuffer` 객체를 클래스 멤버로 생성합니다. 커맨드 버퍼는 커맨드 풀이 파괴될 때 자동으로 해제되므로, 명시적인 정리 코드가 필요하지 않습니다. ```c++ VkCommandBuffer commandBuffer; ``` -We'll now start working on a `createCommandBuffer` function to allocate a single -command buffer from the command pool. +이제 커맨드 풀에서 단일 커맨드 버퍼를 할당하는 `createCommandBuffer` 함수 작업을 시작하겠습니다. ```c++ void initVulkan() { @@ -127,9 +103,7 @@ void createCommandBuffer() { } ``` -Command buffers are allocated with the `vkAllocateCommandBuffers` function, -which takes a `VkCommandBufferAllocateInfo` struct as parameter that specifies -the command pool and number of buffers to allocate: +커맨드 버퍼는 `vkAllocateCommandBuffers` 함수로 할당되며, 이 함수는 커맨드 풀과 할당할 버퍼 수를 지정하는 `VkCommandBufferAllocateInfo` 구조체를 매개변수로 받습니다. ```c++ VkCommandBufferAllocateInfo allocInfo{}; @@ -143,27 +117,18 @@ if (vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer) != VK_SUCCESS) } ``` -The `level` parameter specifies if the allocated command buffers are primary or -secondary command buffers. +`level` 매개변수는 할당된 커맨드 버퍼가 주(primary) 커맨드 버퍼인지 보조(secondary) 커맨드 버퍼인지를 지정합니다. -* `VK_COMMAND_BUFFER_LEVEL_PRIMARY`: Can be submitted to a queue for execution, -but cannot be called from other command buffers. -* `VK_COMMAND_BUFFER_LEVEL_SECONDARY`: Cannot be submitted directly, but can be -called from primary command buffers. +* `VK_COMMAND_BUFFER_LEVEL_PRIMARY`: 큐에 제출하여 실행할 수 있지만, 다른 커맨드 버퍼에서 호출될 수는 없습니다. +* `VK_COMMAND_BUFFER_LEVEL_SECONDARY`: 직접 제출할 수는 없지만, 주 커맨드 버퍼에서 호출될 수 있습니다. -We won't make use of the secondary command buffer functionality here, but you -can imagine that it's helpful to reuse common operations from primary command -buffers. +여기서는 보조 커맨드 버퍼 기능을 사용하지 않겠지만, 주 커맨드 버퍼에서 공통 작업을 재사용하는 데 유용하다는 것을 상상할 수 있습니다. -Since we are only allocating one command buffer, the `commandBufferCount` parameter -is just one. +우리는 하나의 커맨드 버퍼만 할당하므로 `commandBufferCount` 매개변수는 1입니다. -## Command buffer recording +## 커맨드 버퍼 기록 -We'll now start working on the `recordCommandBuffer` function that writes the -commands we want to execute into a command buffer. The `VkCommandBuffer` used -will be passed in as a parameter, as well as the index of the current swapchain -image we want to write to. +이제 실행하려는 커맨드를 커맨드 버퍼에 작성하는 `recordCommandBuffer` 함수 작업을 시작하겠습니다. 사용될 `VkCommandBuffer`와 현재 작성하려는 스왑체인 이미지의 인덱스가 매개변수로 전달됩니다. ```c++ void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) { @@ -171,45 +136,34 @@ void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) { } ``` -We always begin recording a command buffer by calling `vkBeginCommandBuffer` -with a small `VkCommandBufferBeginInfo` structure as argument that specifies -some details about the usage of this specific command buffer. +커맨드 버퍼 기록은 항상 `vkBeginCommandBuffer`를 호출하는 것으로 시작합니다. 이 함수는 해당 커맨드 버퍼의 사용에 대한 세부 정보를 지정하는 작은 `VkCommandBufferBeginInfo` 구조체를 인자로 받습니다. ```c++ VkCommandBufferBeginInfo beginInfo{}; beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; -beginInfo.flags = 0; // Optional -beginInfo.pInheritanceInfo = nullptr; // Optional +beginInfo.flags = 0; // 선택 사항 +beginInfo.pInheritanceInfo = nullptr; // 선택 사항 if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) { throw std::runtime_error("failed to begin recording command buffer!"); } ``` -The `flags` parameter specifies how we're going to use the command buffer. The -following values are available: +`flags` 매개변수는 우리가 커맨드 버퍼를 어떻게 사용할지를 지정합니다. 다음과 같은 값들을 사용할 수 있습니다: -* `VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT`: The command buffer will be -rerecorded right after executing it once. -* `VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT`: This is a secondary -command buffer that will be entirely within a single render pass. -* `VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT`: The command buffer can be -resubmitted while it is also already pending execution. +* `VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT`: 커맨드 버퍼는 한 번 실행된 직후 다시 기록될 것입니다. +* `VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT`: 이것은 단일 렌더 패스 내에서만 사용될 보조 커맨드 버퍼입니다. +* `VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT`: 커맨드 버퍼가 이미 실행 대기 중인 상태에서도 다시 제출될 수 있습니다. -None of these flags are applicable for us right now. +지금 우리에게는 이 플래그들 중 어느 것도 해당되지 않습니다. -The `pInheritanceInfo` parameter is only relevant for secondary command buffers. -It specifies which state to inherit from the calling primary command buffers. +`pInheritanceInfo` 매개변수는 보조 커맨드 버퍼에만 관련이 있습니다. 이 매개변수는 호출하는 주 커맨드 버퍼로부터 어떤 상태를 상속받을지 지정합니다. -If the command buffer was already recorded once, then a call to -`vkBeginCommandBuffer` will implicitly reset it. It's not possible to append -commands to a buffer at a later time. +커맨드 버퍼가 이미 한 번 기록되었다면, `vkBeginCommandBuffer`를 호출하면 암시적으로 리셋됩니다. 나중에 버퍼에 커맨드를 추가하는 것은 불가능합니다. -## Starting a render pass +## 렌더 패스 시작하기 -Drawing starts by beginning the render pass with `vkCmdBeginRenderPass`. The -render pass is configured using some parameters in a `VkRenderPassBeginInfo` -struct. +그리기는 `vkCmdBeginRenderPass`로 렌더 패스를 시작하는 것으로 시작됩니다. 렌더 패스는 `VkRenderPassBeginInfo` 구조체의 몇 가지 매개변수를 사용하여 구성됩니다. ```c++ VkRenderPassBeginInfo renderPassInfo{}; @@ -218,21 +172,14 @@ renderPassInfo.renderPass = renderPass; renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex]; ``` -The first parameters are the render pass itself and the attachments to bind. We -created a framebuffer for each swap chain image where it is specified as a color -attachment. Thus we need to bind the framebuffer for the swapchain image we want -to draw to. Using the imageIndex parameter which was passed in, we can pick the -right framebuffer for the current swapchain image. +첫 번째 매개변수는 렌더 패스 자체이고, 두 번째는 바인딩할 어태치먼트입니다. 우리는 각 스왑 체인 이미지에 대해 프레임버퍼를 생성했으며, 각 이미지는 컬러 어태치먼트로 지정되었습니다. 따라서 우리가 그리려는 스왑체인 이미지에 맞는 프레임버퍼를 바인딩해야 합니다. 전달된 `imageIndex` 매개변수를 사용하여 현재 스왑체인 이미지에 적합한 프레임버퍼를 선택할 수 있습니다. ```c++ renderPassInfo.renderArea.offset = {0, 0}; renderPassInfo.renderArea.extent = swapChainExtent; ``` -The next two parameters define the size of the render area. The render area -defines where shader loads and stores will take place. The pixels outside this -region will have undefined values. It should match the size of the attachments -for best performance. +다음 두 매개변수는 렌더 영역의 크기를 정의합니다. 렌더 영역은 셰이더 로드 및 저장이 일어날 위치를 정의합니다. 이 영역 밖의 픽셀은 정의되지 않은 값을 갖게 됩니다. 최상의 성능을 위해서는 어태치먼트의 크기와 일치해야 합니다. ```c++ VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}}; @@ -240,47 +187,33 @@ renderPassInfo.clearValueCount = 1; renderPassInfo.pClearValues = &clearColor; ``` -The last two parameters define the clear values to use for -`VK_ATTACHMENT_LOAD_OP_CLEAR`, which we used as load operation for the color -attachment. I've defined the clear color to simply be black with 100% opacity. +마지막 두 매개변수는 `VK_ATTACHMENT_LOAD_OP_CLEAR`에 사용할 소거 값(clear value)을 정의합니다. 우리는 이 값을 컬러 어태치먼트의 로드 작업으로 사용했습니다. 저는 소거 색상을 100% 불투명도의 검은색으로 정의했습니다. ```c++ vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); ``` -The render pass can now begin. All of the functions that record commands can be -recognized by their `vkCmd` prefix. They all return `void`, so there will be no -error handling until we've finished recording. +이제 렌더 패스를 시작할 수 있습니다. 커맨드를 기록하는 모든 함수는 `vkCmd` 접두사로 식별할 수 있습니다. 이 함수들은 모두 `void`를 반환하므로, 기록이 끝날 때까지 오류 처리는 없습니다. -The first parameter for every command is always the command buffer to record the -command to. The second parameter specifies the details of the render pass we've -just provided. The final parameter controls how the drawing commands within the -render pass will be provided. It can have one of two values: +모든 커맨드의 첫 번째 매개변수는 항상 커맨드를 기록할 커맨드 버퍼입니다. 두 번째 매개변수는 우리가 방금 제공한 렌더 패스의 세부 정보를 지정합니다. 마지막 매개변수는 렌더 패스 내의 드로잉 커맨드가 어떻게 제공될지를 제어합니다. 이 값은 다음 두 가지 중 하나일 수 있습니다: -* `VK_SUBPASS_CONTENTS_INLINE`: The render pass commands will be embedded in -the primary command buffer itself and no secondary command buffers will be -executed. -* `VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS`: The render pass commands will -be executed from secondary command buffers. +* `VK_SUBPASS_CONTENTS_INLINE`: 렌더 패스 커맨드가 주 커맨드 버퍼 자체에 포함되며, 보조 커맨드 버퍼는 실행되지 않습니다. +* `VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS`: 렌더 패스 커맨드가 보조 커맨드 버퍼에서 실행됩니다. -We will not be using secondary command buffers, so we'll go with the first -option. +우리는 보조 커맨드 버퍼를 사용하지 않을 것이므로, 첫 번째 옵션을 선택합니다. -## Basic drawing commands +## 기본 드로잉 커맨드 -We can now bind the graphics pipeline: +이제 그래픽스 파이프라인을 바인딩할 수 있습니다. ```c++ vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); ``` -The second parameter specifies if the pipeline object is a graphics or compute -pipeline. We've now told Vulkan which operations to execute in the graphics -pipeline and which attachment to use in the fragment shader. +두 번째 매개변수는 파이프라인 객체가 그래픽스 파이프라인인지 컴퓨트 파이프라인인지를 지정합니다. 이제 Vulkan에게 그래픽스 파이프라인에서 어떤 작업을 실행할지, 그리고 프래그먼트 셰이더에서 어떤 어태치먼트를 사용할지를 알려주었습니다. -As noted in the [fixed functions chapter](../02_Graphics_pipeline_basics/02_Fixed_functions.md#dynamic-state), -we did specify viewport and scissor state for this pipeline to be dynamic. -So we need to set them in the command buffer before issuing our draw command: +[고정 함수 챕터](../02_Graphics_pipeline_basics/02_Fixed_functions.md#dynamic-state)에서 언급했듯이, 우리는 이 파이프라인의 뷰포트와 시저 상태를 동적(dynamic)으로 지정했습니다. +따라서 드로우 커맨드를 실행하기 전에 커맨드 버퍼에서 이를 설정해야 합니다: ```c++ VkViewport viewport{}; @@ -298,34 +231,28 @@ scissor.extent = swapChainExtent; vkCmdSetScissor(commandBuffer, 0, 1, &scissor); ``` -Now we are ready to issue the draw command for the triangle: +이제 삼각형을 그리기 위한 드로우 커맨드를 실행할 준비가 되었습니다. ```c++ vkCmdDraw(commandBuffer, 3, 1, 0, 0); ``` -The actual `vkCmdDraw` function is a bit anticlimactic, but it's so simple -because of all the information we specified in advance. It has the following -parameters, aside from the command buffer: +실제 `vkCmdDraw` 함수는 다소 김이 빠지지만, 사전에 모든 정보를 지정했기 때문에 이렇게 간단합니다. 이 함수는 커맨드 버퍼 외에 다음과 같은 매개변수를 가집니다: -* `vertexCount`: Even though we don't have a vertex buffer, we technically still -have 3 vertices to draw. -* `instanceCount`: Used for instanced rendering, use `1` if you're not doing -that. -* `firstVertex`: Used as an offset into the vertex buffer, defines the lowest -value of `gl_VertexIndex`. -* `firstInstance`: Used as an offset for instanced rendering, defines the lowest -value of `gl_InstanceIndex`. +* `vertexCount`: 정점 버퍼가 없지만, 기술적으로는 여전히 3개의 정점을 그려야 합니다. +* `instanceCount`: 인스턴스 렌더링에 사용됩니다. 사용하지 않을 경우 `1`을 사용합니다. +* `firstVertex`: 정점 버퍼의 오프셋으로 사용되며, `gl_VertexIndex`의 최솟값을 정의합니다. +* `firstInstance`: 인스턴스 렌더링의 오프셋으로 사용되며, `gl_InstanceIndex`의 최솟값을 정의합니다. -## Finishing up +## 마무리 -The render pass can now be ended: +이제 렌더 패스를 종료할 수 있습니다. ```c++ vkCmdEndRenderPass(commandBuffer); ``` -And we've finished recording the command buffer: +그리고 커맨드 버퍼 기록을 마쳤습니다. ```c++ if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) { @@ -333,12 +260,8 @@ if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) { } ``` +다음 챕터에서는 메인 루프 코드를 작성할 것입니다. 이 루프는 스왑 체인에서 이미지를 가져오고, 커맨드 버퍼를 기록 및 실행한 다음, 완성된 이미지를 스왑 체인으로 반환하는 작업을 수행합니다. - -In the next chapter we'll write the code for the main loop, which will acquire -an image from the swap chain, record and execute a command buffer, then return the -finished image to the swap chain. - -[C++ code](/code/14_command_buffers.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +[C++ 코드](/code/14_command_buffers.cpp) / +[정점 셰이더](/code/09_shader_base.vert) / +[프래그먼트 셰이더](/code/09_shader_base.frag) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md b/ko/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md index 233c059d..78ce4212 100644 --- a/ko/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md +++ b/ko/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md @@ -1,8 +1,4 @@ - -This is the chapter where everything is going to come together. We're going to -write the `drawFrame` function that will be called from the main loop to put the -triangle on the screen. Let's start by creating the function and call it from -`mainLoop`: +이번 장에서는 모든 것을 하나로 합칠 시간입니다. 메인 루프에서 호출되어 삼각형을 화면에 그리는 `drawFrame` 함수를 작성할 것입니다. 먼저 함수를 만들고 `mainLoop`에서 호출해 봅시다. ```c++ void mainLoop() { @@ -19,159 +15,94 @@ void drawFrame() { } ``` -## Outline of a frame +## 프레임의 개요 -At a high level, rendering a frame in Vulkan consists of a common set of steps: +높은 수준에서 Vulkan으로 프레임을 렌더링하는 것은 다음과 같은 공통된 단계로 구성됩니다. -* Wait for the previous frame to finish -* Acquire an image from the swap chain -* Record a command buffer which draws the scene onto that image -* Submit the recorded command buffer -* Present the swap chain image +* 이전 프레임이 끝나기를 기다립니다. +* 스왑 체인에서 이미지를 가져옵니다. +* 가져온 이미지에 장면을 그리는 커맨드 버퍼를 기록합니다. +* 기록된 커맨드 버퍼를 제출합니다. +* 스왑 체인 이미지를 제시(present)합니다. -While we will expand the drawing function in later chapters, for now this is the -core of our render loop. +이후 장에서 드로잉 함수를 더 확장하겠지만, 지금으로서는 이것이 우리 렌더링 루프의 핵심입니다. - + -## Synchronization +## 동기화 - + -A core design philosophy in Vulkan is that synchronization of execution on -the GPU is explicit. The order of operations is up to us to define using various -synchronization primitives which tell the driver the order we want things to run -in. This means that many Vulkan API calls which start executing work on the GPU -are asynchronous, the functions will return before the operation has finished. +Vulkan의 핵심 설계 철학 중 하나는 GPU에서의 실행 동기화가 명시적이라는 것입니다. 작업 순서는 우리가 다양한 동기화 프리미티브(primitive)를 사용하여 드라이버에 원하는 실행 순서를 알려주는 것에 달려 있습니다. 이는 GPU에서 작업을 시작하는 많은 Vulkan API 호출이 비동기적임을 의미합니다. 즉, 함수는 작업이 완료되기 전에 반환됩니다. -In this chapter there are a number of events that we need to order explicitly -because they happen on the GPU, such as: +이번 장에서는 GPU에서 발생하는 여러 이벤트를 명시적으로 순서를 정해야 합니다. 예를 들면 다음과 같습니다. -* Acquire an image from the swap chain -* Execute commands that draw onto the acquired image -* Present that image to the screen for presentation, returning it to the swapchain +* 스왑 체인에서 이미지 가져오기 +* 가져온 이미지에 그리는 커맨드 실행하기 +* 화면에 표시(presentation)하기 위해 이미지를 제시하고, 스왑 체인에 반환하기 -Each of these events is set in motion using a single function call, but are all -executed asynchronously. The function calls will return before the operations -are actually finished and the order of execution is also undefined. That is -unfortunate, because each of the operations depends on the previous one -finishing. Thus we need to explore which primitives we can use to achieve -the desired ordering. +이 각 이벤트는 단일 함수 호출로 시작되지만 모두 비동기적으로 실행됩니다. 함수 호출은 실제 작업이 끝나기 전에 반환되며 실행 순서 또한 정의되지 않습니다. 이는 안타까운 일입니다. 왜냐하면 각 작업은 이전 작업이 완료되는 것에 의존하기 때문입니다. 따라서 원하는 순서를 달성하기 위해 어떤 프리미티브를 사용할 수 있는지 알아봐야 합니다. -### Semaphores +### 세마포어(Semaphores) -A semaphore is used to add order between queue operations. Queue operations -refer to the work we submit to a queue, either in a command buffer or from -within a function as we will see later. Examples of queues are the graphics -queue and the presentation queue. Semaphores are used both to order work inside -the same queue and between different queues. +세마포어는 큐 작업 간에 순서를 추가하는 데 사용됩니다. 큐 작업이란 커맨드 버퍼나 나중에 보게 될 함수 내에서 큐에 제출하는 작업을 말합니다. 큐의 예로는 그래픽스 큐와 프레젠테이션 큐가 있습니다. 세마포어는 동일한 큐 내의 작업을 정렬하거나 서로 다른 큐 간의 작업을 정렬하는 데 모두 사용됩니다. -There happens to be two kinds of semaphores in Vulkan, binary and timeline. -Because only binary semaphores will be used in this tutorial, we will not -discuss timeline semaphores. Further mention of the term semaphore exclusively -refers to binary semaphores. +Vulkan에는 바이너리(binary)와 타임라인(timeline) 두 종류의 세마포어가 있습니다. 이 튜토리얼에서는 바이너리 세마포어만 사용하므로 타임라인 세마포어는 다루지 않겠습니다. 앞으로 '세마포어'라는 용어는 바이너리 세마포어만을 지칭합니다. -A semaphore is either unsignaled or signaled. It begins life as unsignaled. The -way we use a semaphore to order queue operations is by providing the same -semaphore as a 'signal' semaphore in one queue operation and as a 'wait' -semaphore in another queue operation. For example, lets say we have semaphore S -and queue operations A and B that we want to execute in order. What we tell -Vulkan is that operation A will 'signal' semaphore S when it finishes executing, -and operation B will 'wait' on semaphore S before it begins executing. When -operation A finishes, semaphore S will be signaled, while operation B wont -start until S is signaled. After operation B begins executing, semaphore S -is automatically reset back to being unsignaled, allowing it to be used again. +세마포어는 신호되지 않음(unsignaled) 또는 신호됨(signaled) 상태입니다. 처음에는 신호되지 않은 상태로 시작합니다. 세마포어를 사용하여 큐 작업의 순서를 정하는 방법은 한 큐 작업에서는 '신호(signal)' 세마포어로, 다른 큐 작업에서는 '대기(wait)' 세마포어로 동일한 세마포어를 제공하는 것입니다. 예를 들어, 순서대로 실행하고 싶은 세마포어 S와 큐 작업 A, B가 있다고 가정해 봅시다. Vulkan에 작업 A가 실행을 마치면 세마포어 S에 '신호'를 보내고, 작업 B는 실행을 시작하기 전에 세마포어 S를 '대기'하라고 알려줍니다. 작업 A가 끝나면 세마포어 S는 신호 상태가 되고, 작업 B는 S가 신호될 때까지 시작되지 않습니다. 작업 B가 실행을 시작하면 세마포어 S는 자동으로 신호되지 않은 상태로 재설정되어 다시 사용할 수 있게 됩니다. -Pseudo-code of what was just described: +방금 설명한 내용의 의사 코드(pseudo-code)는 다음과 같습니다. ``` -VkCommandBuffer A, B = ... // record command buffers -VkSemaphore S = ... // create a semaphore +VkCommandBuffer A, B = ... // 커맨드 버퍼 기록 +VkSemaphore S = ... // 세마포어 생성 -// enqueue A, signal S when done - starts executing immediately +// A를 큐에 넣고, 끝나면 S에 신호 보냄 - 즉시 실행 시작 vkQueueSubmit(work: A, signal: S, wait: None) -// enqueue B, wait on S to start +// B를 큐에 넣고, 시작하기 위해 S를 대기 vkQueueSubmit(work: B, signal: None, wait: S) ``` -Note that in this code snippet, both calls to `vkQueueSubmit()` return -immediately - the waiting only happens on the GPU. The CPU continues running -without blocking. To make the CPU wait, we need a different synchronization -primitive, which we will now describe. - -### Fences - -A fence has a similar purpose, in that it is used to synchronize execution, but -it is for ordering the execution on the CPU, otherwise known as the host. -Simply put, if the host needs to know when the GPU has finished something, we -use a fence. - -Similar to semaphores, fences are either in a signaled or unsignaled state. -Whenever we submit work to execute, we can attach a fence to that work. When -the work is finished, the fence will be signaled. Then we can make the host -wait for the fence to be signaled, guaranteeing that the work has finished -before the host continues. - -A concrete example is taking a screenshot. Say we have already done the -necessary work on the GPU. Now need to transfer the image from the GPU over -to the host and then save the memory to a file. We have command buffer A which -executes the transfer and fence F. We submit command buffer A with fence F, -then immediately tell the host to wait for F to signal. This causes the host to -block until command buffer A finishes execution. Thus we are safe to let the -host save the file to disk, as the memory transfer has completed. - -Pseudo-code for what was described: +이 코드 조각에서 `vkQueueSubmit()`에 대한 두 호출은 즉시 반환됩니다. 대기는 GPU에서만 발생합니다. CPU는 블로킹(blocking)되지 않고 계속 실행됩니다. CPU를 기다리게 하려면 다른 동기화 프리미티브가 필요하며, 이제 그것에 대해 설명하겠습니다. + +### 펜스(Fences) + +펜스도 실행을 동기화하는 데 사용된다는 점에서 비슷한 목적을 가지지만, 이는 CPU, 즉 호스트(host)에서의 실행 순서를 정하기 위한 것입니다. 간단히 말해, 호스트가 GPU가 무언가를 마쳤는지 알아야 할 때 펜스를 사용합니다. + +세마포어와 마찬가지로 펜스도 신호됨 또는 신호되지 않음 상태입니다. 작업을 제출하여 실행할 때마다 해당 작업에 펜스를 첨부할 수 있습니다. 작업이 완료되면 펜스는 신호 상태가 됩니다. 그런 다음 호스트가 펜스가 신호될 때까지 기다리게 할 수 있으며, 이를 통해 호스트가 계속 진행하기 전에 작업이 완료되었음을 보장합니다. + +구체적인 예로 스크린샷을 찍는 경우를 들어보겠습니다. GPU에서 필요한 작업을 이미 마쳤다고 가정합시다. 이제 GPU에서 호스트로 이미지를 전송한 다음 메모리를 파일에 저장해야 합니다. 전송을 실행하는 커맨드 버퍼 A와 펜스 F가 있습니다. 펜스 F와 함께 커맨드 버퍼 A를 제출한 다음, 즉시 호스트에게 F가 신호될 때까지 기다리라고 지시합니다. 이로 인해 호스트는 커맨드 버퍼 A의 실행이 끝날 때까지 블로킹됩니다. 따라서 메모리 전송이 완료되었으므로 호스트가 파일을 디스크에 안전하게 저장할 수 있습니다. + +설명한 내용에 대한 의사 코드는 다음과 같습니다. ``` -VkCommandBuffer A = ... // record command buffer with the transfer -VkFence F = ... // create the fence +VkCommandBuffer A = ... // 전송을 포함한 커맨드 버퍼 기록 +VkFence F = ... // 펜스 생성 -// enqueue A, start work immediately, signal F when done +// A를 큐에 넣고, 즉시 작업 시작, 끝나면 F에 신호 보냄 vkQueueSubmit(work: A, fence: F) -vkWaitForFence(F) // blocks execution until A has finished executing +vkWaitForFence(F) // A의 실행이 끝날 때까지 실행을 블로킹함 -save_screenshot_to_disk() // can't run until the transfer has finished +save_screenshot_to_disk() // 전송이 끝나기 전까지 실행될 수 없음 ``` -Unlike the semaphore example, this example *does* block host execution. This -means the host won't do anything except wait until execution has finished. For -this case, we had to make sure the transfer was complete before we could save -the screenshot to disk. +세마포어 예제와 달리, 이 예제는 호스트 실행을 *블로킹*합니다. 이는 호스트가 실행이 끝날 때까지 기다리는 것 외에는 아무것도 하지 않는다는 것을 의미합니다. 이 경우, 스크린샷을 디스크에 저장하기 전에 전송이 완료되었는지 확인해야 했습니다. -In general, it is preferable to not block the host unless necessary. We want to -feed the GPU and the host with useful work to do. Waiting on fences to signal -is not useful work. Thus we prefer semaphores, or other synchronization -primitives not yet covered, to synchronize our work. +일반적으로, 필요하지 않다면 호스트를 블로킹하지 않는 것이 좋습니다. 우리는 GPU와 호스트에 유용한 작업을 계속 제공하고 싶습니다. 펜스가 신호되기를 기다리는 것은 유용한 작업이 아닙니다. 따라서 작업을 동기화하기 위해 세마포어나 아직 다루지 않은 다른 동기화 프리미티브를 선호합니다. -Fences must be reset manually to put them back into the unsignaled state. This -is because fences are used to control the execution of the host, and so the -host gets to decide when to reset the fence. Contrast this to semaphores which -are used to order work on the GPU without the host being involved. +펜스는 수동으로 재설정하여 신호되지 않은 상태로 되돌려야 합니다. 이는 펜스가 호스트의 실행을 제어하는 데 사용되므로, 언제 펜스를 재설정할지 호스트가 결정하기 때문입니다. 이는 호스트의 개입 없이 GPU 상의 작업 순서를 정하는 데 사용되는 세마포어와 대조적입니다. -In summary, semaphores are used to specify the execution order of operations on -the GPU while fences are used to keep the CPU and GPU in sync with each-other. +요약하자면, 세마포어는 GPU에서의 작업 실행 순서를 지정하는 데 사용되고, 펜스는 CPU와 GPU를 서로 동기화 상태로 유지하는 데 사용됩니다. -### What to choose? +### 무엇을 선택해야 할까? -We have two synchronization primitives to use and conveniently two places to -apply synchronization: Swapchain operations and waiting for the previous frame -to finish. We want to use semaphores for swapchain operations because they -happen on the GPU, thus we don't want to make the host wait around if we can -help it. For waiting on the previous frame to finish, we want to use fences -for the opposite reason, because we need the host to wait. This is so we don't -draw more than one frame at a time. Because we re-record the command buffer -every frame, we cannot record the next frame's work to the command buffer -until the current frame has finished executing, as we don't want to overwrite -the current contents of the command buffer while the GPU is using it. +우리에게는 두 가지 동기화 프리미티브가 있고, 마침 동기화를 적용할 두 곳이 있습니다: 스왑 체인 작업과 이전 프레임이 끝나기를 기다리는 것. 스왑 체인 작업은 GPU에서 발생하므로 가능하다면 호스트를 기다리게 하고 싶지 않으므로 세마포어를 사용하고 싶습니다. 이전 프레임이 끝나기를 기다리는 것에는 반대의 이유로 펜스를 사용하고 싶습니다. 왜냐하면 호스트가 기다려야 하기 때문입니다. 이는 한 번에 한 프레임 이상을 그리지 않도록 하기 위함입니다. 우리는 매 프레임마다 커맨드 버퍼를 다시 기록하므로, 현재 프레임이 실행을 마칠 때까지 다음 프레임의 작업을 커맨드 버퍼에 기록할 수 없습니다. GPU가 커맨드 버퍼를 사용하는 동안 현재 내용을 덮어쓰고 싶지 않기 때문입니다. -## Creating the synchronization objects +## 동기화 객체 생성하기 -We'll need one semaphore to signal that an image has been acquired from the -swapchain and is ready for rendering, another one to signal that rendering has -finished and presentation can happen, and a fence to make sure only one frame -is rendering at a time. +이미지가 스왑 체인에서 사용 가능해져 렌더링 준비가 되었음을 알리는 세마포어 하나, 렌더링이 끝나 프레젠테이션이 가능함을 알리는 세마포어 하나, 그리고 한 번에 한 프레임만 렌더링되도록 보장하는 펜스 하나가 필요합니다. -Create three class members to store these semaphore objects and fence object: +이 세마포어 객체들과 펜스 객체를 저장할 세 개의 클래스 멤버를 만듭니다. ```c++ VkSemaphore imageAvailableSemaphore; @@ -179,8 +110,7 @@ VkSemaphore renderFinishedSemaphore; VkFence inFlightFence; ``` -To create the semaphores, we'll add the last `create` function for this part of -the tutorial: `createSyncObjects`: +세마포어를 생성하기 위해 튜토리얼의 이 부분에 대한 마지막 `create` 함수인 `createSyncObjects`를 추가합니다. ```c++ void initVulkan() { @@ -206,9 +136,7 @@ void createSyncObjects() { } ``` -Creating semaphores requires filling in the `VkSemaphoreCreateInfo`, but in the -current version of the API it doesn't actually have any required fields besides -`sType`: +세마포어를 생성하려면 `VkSemaphoreCreateInfo`를 채워야 하지만, 현재 API 버전에서는 `sType` 외에는 필수 필드가 없습니다. ```c++ void createSyncObjects() { @@ -217,18 +145,16 @@ void createSyncObjects() { } ``` -Future versions of the Vulkan API or extensions may add functionality for the -`flags` and `pNext` parameters like it does for the other structures. +미래의 Vulkan API 버전이나 확장 기능은 다른 구조체와 마찬가지로 `flags`와 `pNext` 파라미터에 기능을 추가할 수 있습니다. -Creating a fence requires filling in the `VkFenceCreateInfo`: +펜스를 생성하려면 `VkFenceCreateInfo`를 채워야 합니다. ```c++ VkFenceCreateInfo fenceInfo{}; fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; ``` -Creating the semaphores and fence follows the familiar pattern with -`vkCreateSemaphore` & `vkCreateFence`: +세마포어와 펜스 생성은 `vkCreateSemaphore`와 `vkCreateFence`를 사용하는 익숙한 패턴을 따릅니다. ```c++ if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphore) != VK_SUCCESS || @@ -238,8 +164,7 @@ if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphore) } ``` -The semaphores and fence should be cleaned up at the end of the program, when -all commands have finished and no more synchronization is necessary: +세마포어와 펜스는 프로그램이 끝날 때, 모든 커맨드가 완료되고 더 이상 동기화가 필요 없을 때 정리되어야 합니다. ```c++ void cleanup() { @@ -248,13 +173,11 @@ void cleanup() { vkDestroyFence(device, inFlightFence, nullptr); ``` -Onto the main drawing function! +이제 메인 드로잉 함수로 넘어가 봅시다! -## Waiting for the previous frame +## 이전 프레임 기다리기 -At the start of the frame, we want to wait until the previous frame has -finished, so that the command buffer and semaphores are available to use. To do -that, we call `vkWaitForFences`: +프레임의 시작에서, 우리는 이전 프레임이 끝나기를 기다려서 커맨드 버퍼와 세마포어를 사용할 수 있게 하고 싶습니다. 이를 위해 `vkWaitForFences`를 호출합니다. ```c++ void drawFrame() { @@ -262,31 +185,19 @@ void drawFrame() { } ``` -The `vkWaitForFences` function takes an array of fences and waits on the host -for either any or all of the fences to be signaled before returning. The -`VK_TRUE` we pass here indicates that we want to wait for all fences, but in -the case of a single one it doesn't matter. This function also has a timeout -parameter that we set to the maximum value of a 64 bit unsigned integer, -`UINT64_MAX`, which effectively disables the timeout. +`vkWaitForFences` 함수는 펜스 배열을 받아, 배열의 일부 또는 모든 펜스가 신호될 때까지 호스트에서 기다린 후 반환합니다. 여기서 전달하는 `VK_TRUE`는 모든 펜스를 기다리겠다는 의미이지만, 펜스가 하나일 경우에는 상관없습니다. 이 함수에는 타임아웃 파라미터도 있는데, 우리는 64비트 부호 없는 정수의 최댓값인 `UINT64_MAX`를 설정하여 타임아웃을 사실상 비활성화합니다. + +기다린 후에는 `vkResetFences` 호출로 펜스를 수동으로 신호되지 않은 상태로 재설정해야 합니다. -After waiting, we need to manually reset the fence to the unsignaled state with -the `vkResetFences` call: ```c++ vkResetFences(device, 1, &inFlightFence); ``` -Before we can proceed, there is a slight hiccup in our design. On the first -frame we call `drawFrame()`, which immediately waits on `inFlightFence` to -be signaled. `inFlightFence` is only signaled after a frame has finished -rendering, yet since this is the first frame, there are no previous frames in -which to signal the fence! Thus `vkWaitForFences()` blocks indefinitely, -waiting on something which will never happen. +계속 진행하기 전에 우리 설계에 약간의 문제가 있습니다. 첫 프레임에서 `drawFrame()`을 호출하면 즉시 `inFlightFence`가 신호되기를 기다립니다. `inFlightFence`는 프레임 렌더링이 완료된 후에만 신호되는데, 지금은 첫 프레임이므로 펜스에 신호를 보낼 이전 프레임이 없습니다! 따라서 `vkWaitForFences()`는 결코 일어나지 않을 일을 기다리며 무한정 블로킹됩니다. -Of the many solutions to this dilemma, there is a clever workaround built into -the API. Create the fence in the signaled state, so that the first call to -`vkWaitForFences()` returns immediately since the fence is already signaled. +이 딜레마에 대한 많은 해결책 중, API에 내장된 영리한 해결 방법이 있습니다. 펜스를 신호된 상태로 생성하여 첫 번째 `vkWaitForFences()` 호출이 이미 신호된 펜스 덕분에 즉시 반환되도록 하는 것입니다. -To do this, we add the `VK_FENCE_CREATE_SIGNALED_BIT` flag to the `VkFenceCreateInfo`: +이를 위해 `VkFenceCreateInfo`에 `VK_FENCE_CREATE_SIGNALED_BIT` 플래그를 추가합니다. ```c++ void createSyncObjects() { @@ -300,11 +211,9 @@ void createSyncObjects() { } ``` -## Acquiring an image from the swap chain +## 스왑 체인에서 이미지 가져오기 -The next thing we need to do in the `drawFrame` function is acquire an image -from the swap chain. Recall that the swap chain is an extension feature, so we -must use a function with the `vk*KHR` naming convention: +`drawFrame` 함수에서 다음으로 할 일은 스왑 체인에서 이미지를 가져오는 것입니다. 스왑 체인은 확장 기능이므로 `vk*KHR` 명명 규칙을 가진 함수를 사용해야 한다는 것을 기억하세요. ```c++ void drawFrame() { @@ -315,47 +224,33 @@ void drawFrame() { } ``` -The first two parameters of `vkAcquireNextImageKHR` are the logical device and -the swap chain from which we wish to acquire an image. The third parameter -specifies a timeout in nanoseconds for an image to become available. Using the -maximum value of a 64 bit unsigned integer means we effectively disable the -timeout. +`vkAcquireNextImageKHR`의 첫 두 파라미터는 로지컬 디바이스와 이미지를 가져올 스왑 체인입니다. 세 번째 파라미터는 이미지가 사용 가능해질 때까지의 타임아웃을 나노초 단위로 지정합니다. 64비트 부호 없는 정수의 최댓값을 사용하면 타임아웃을 효과적으로 비활성화합니다. -The next two parameters specify synchronization objects that are to be signaled -when the presentation engine is finished using the image. That's the point in -time where we can start drawing to it. It is possible to specify a semaphore, -fence or both. We're going to use our `imageAvailableSemaphore` for that purpose -here. +다음 두 파라미터는 프레젠테이션 엔진이 이미지 사용을 마쳤을 때 신호를 보낼 동기화 객체를 지정합니다. 이 시점이 바로 우리가 이미지에 그리기를 시작할 수 있는 때입니다. 세마포어, 펜스 또는 둘 다 지정할 수 있습니다. 여기서는 `imageAvailableSemaphore`를 그 목적으로 사용할 것입니다. -The last parameter specifies a variable to output the index of the swap chain -image that has become available. The index refers to the `VkImage` in our -`swapChainImages` array. We're going to use that index to pick the `VkFrameBuffer`. +마지막 파라미터는 사용 가능해진 스왑 체인 이미지의 인덱스를 출력할 변수를 지정합니다. 이 인덱스는 `swapChainImages` 배열의 `VkImage`를 가리킵니다. 우리는 이 인덱스를 사용하여 `VkFramebuffer`를 선택할 것입니다. -## Recording the command buffer +## 커맨드 버퍼 기록하기 -With the imageIndex specifying the swap chain image to use in hand, we can now -record the command buffer. First, we call `vkResetCommandBuffer` on the command -buffer to make sure it is able to be recorded. +사용할 스왑 체인 이미지를 지정하는 `imageIndex`를 확보했으므로 이제 커맨드 버퍼를 기록할 수 있습니다. 먼저 커맨드 버퍼에 `vkResetCommandBuffer`를 호출하여 기록할 수 있는 상태인지 확인합니다. ```c++ vkResetCommandBuffer(commandBuffer, 0); ``` -The second parameter of `vkResetCommandBuffer` is a `VkCommandBufferResetFlagBits` -flag. Since we don't want to do anything special, we leave it as 0. +`vkResetCommandBuffer`의 두 번째 파라미터는 `VkCommandBufferResetFlagBits` 플래그입니다. 특별한 작업을 원하지 않으므로 0으로 둡니다. -Now call the function `recordCommandBuffer` to record the commands we want. +이제 `recordCommandBuffer` 함수를 호출하여 원하는 커맨드를 기록합니다. ```c++ recordCommandBuffer(commandBuffer, imageIndex); ``` -With a fully recorded command buffer, we can now submit it. +완전히 기록된 커맨드 버퍼가 있으므로 이제 제출할 수 있습니다. -## Submitting the command buffer +## 커맨드 버퍼 제출하기 -Queue submission and synchronization is configured through parameters in the -`VkSubmitInfo` structure. +큐 제출 및 동기화는 `VkSubmitInfo` 구조체의 파라미터를 통해 구성됩니다. ```c++ VkSubmitInfo submitInfo{}; @@ -368,21 +263,14 @@ submitInfo.pWaitSemaphores = waitSemaphores; submitInfo.pWaitDstStageMask = waitStages; ``` -The first three parameters specify which semaphores to wait on before execution -begins and in which stage(s) of the pipeline to wait. We want to wait with -writing colors to the image until it's available, so we're specifying the stage -of the graphics pipeline that writes to the color attachment. That means that -theoretically the implementation can already start executing our vertex shader -and such while the image is not yet available. Each entry in the `waitStages` -array corresponds to the semaphore with the same index in `pWaitSemaphores`. +처음 세 파라미터는 실행이 시작되기 전에 대기할 세마포어와 파이프라인의 어느 단계에서 대기할지를 지정합니다. 우리는 이미지가 사용 가능해질 때까지 이미지에 색상을 쓰는 것을 기다리고 싶으므로, 컬러 어태치먼트에 쓰는 그래픽스 파이프라인 단계를 지정합니다. 이론적으로 이는 구현이 이미지가 아직 사용 가능하지 않은 동안에도 버텍스 셰이더 등을 실행 시작할 수 있음을 의미합니다. `waitStages` 배열의 각 항목은 `pWaitSemaphores`의 동일한 인덱스를 가진 세마포어에 해당합니다. ```c++ submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &commandBuffer; ``` -The next two parameters specify which command buffers to actually submit for -execution. We simply submit the single command buffer we have. +다음 두 파라미터는 실제로 실행을 위해 제출할 커맨드 버퍼를 지정합니다. 우리는 가지고 있는 단일 커맨드 버퍼를 간단히 제출합니다. ```c++ VkSemaphore signalSemaphores[] = {renderFinishedSemaphore}; @@ -390,9 +278,7 @@ submitInfo.signalSemaphoreCount = 1; submitInfo.pSignalSemaphores = signalSemaphores; ``` -The `signalSemaphoreCount` and `pSignalSemaphores` parameters specify which -semaphores to signal once the command buffer(s) have finished execution. In our -case we're using the `renderFinishedSemaphore` for that purpose. +`signalSemaphoreCount`와 `pSignalSemaphores` 파라미터는 커맨드 버퍼(들)의 실행이 완료되면 신호를 보낼 세마포어를 지정합니다. 우리의 경우 `renderFinishedSemaphore`를 그 목적으로 사용합니다. ```c++ if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFence) != VK_SUCCESS) { @@ -400,36 +286,15 @@ if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFence) != VK_SUCCESS) { } ``` -We can now submit the command buffer to the graphics queue using -`vkQueueSubmit`. The function takes an array of `VkSubmitInfo` structures as -argument for efficiency when the workload is much larger. The last parameter -references an optional fence that will be signaled when the command buffers -finish execution. This allows us to know when it is safe for the command -buffer to be reused, thus we want to give it `inFlightFence`. Now on the next -frame, the CPU will wait for this command buffer to finish executing before it -records new commands into it. - -## Subpass dependencies - -Remember that the subpasses in a render pass automatically take care of image -layout transitions. These transitions are controlled by *subpass dependencies*, -which specify memory and execution dependencies between subpasses. We have only -a single subpass right now, but the operations right before and right after this -subpass also count as implicit "subpasses". - -There are two built-in dependencies that take care of the transition at the -start of the render pass and at the end of the render pass, but the former does -not occur at the right time. It assumes that the transition occurs at the start -of the pipeline, but we haven't acquired the image yet at that point! There are -two ways to deal with this problem. We could change the `waitStages` for the -`imageAvailableSemaphore` to `VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT` to ensure that -the render passes don't begin until the image is available, or we can make the -render pass wait for the `VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT` stage. -I've decided to go with the second option here, because it's a good excuse to -have a look at subpass dependencies and how they work. - -Subpass dependencies are specified in `VkSubpassDependency` structs. Go to the -`createRenderPass` function and add one: +이제 `vkQueueSubmit`을 사용하여 그래픽스 큐에 커맨드 버퍼를 제출할 수 있습니다. 이 함수는 작업량이 훨씬 클 때 효율성을 위해 `VkSubmitInfo` 구조체 배열을 인자로 받습니다. 마지막 파라미터는 커맨드 버퍼 실행이 완료될 때 신호를 받을 선택적 펜스를 참조합니다. 이를 통해 커맨드 버퍼를 재사용해도 안전한 시점을 알 수 있으므로, `inFlightFence`를 전달하고 싶습니다. 이제 다음 프레임에서 CPU는 이 커맨드 버퍼의 실행이 끝날 때까지 기다린 후에 새로운 커맨드를 기록하게 됩니다. + +## 서브패스 종속성(Subpass dependencies) + +렌더 패스의 서브패스들은 이미지 레이아웃 전환을 자동으로 처리한다는 것을 기억하세요. 이러한 전환은 서브패스 간의 메모리 및 실행 종속성을 지정하는 *서브패스 종속성*에 의해 제어됩니다. 현재는 단일 서브패스만 있지만, 이 서브패스 직전과 직후의 작업들도 암시적인 "서브패스"로 간주됩니다. + +렌더 패스 시작과 끝에서 전환을 처리하는 두 개의 내장 종속성이 있지만, 전자는 올바른 시점에 발생하지 않습니다. 이는 전환이 파이프라인의 시작에서 발생한다고 가정하지만, 그 시점에는 아직 이미지를 획득하지 못했습니다! 이 문제를 해결하는 두 가지 방법이 있습니다. `imageAvailableSemaphore`의 `waitStages`를 `VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT`로 변경하여 렌더 패스가 이미지가 사용 가능해질 때까지 시작되지 않도록 하거나, 렌더 패스가 `VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT` 단계를 기다리게 할 수 있습니다. 저는 여기서 두 번째 옵션을 선택했습니다. 왜냐하면 서브패스 종속성과 그것이 어떻게 작동하는지 살펴볼 좋은 기회이기 때문입니다. + +서브패스 종속성은 `VkSubpassDependency` 구조체에 명시됩니다. `createRenderPass` 함수로 가서 하나 추가합시다. ```c++ VkSubpassDependency dependency{}; @@ -437,47 +302,32 @@ dependency.srcSubpass = VK_SUBPASS_EXTERNAL; dependency.dstSubpass = 0; ``` -The first two fields specify the indices of the dependency and the dependent -subpass. The special value `VK_SUBPASS_EXTERNAL` refers to the implicit subpass -before or after the render pass depending on whether it is specified in -`srcSubpass` or `dstSubpass`. The index `0` refers to our subpass, which is the -first and only one. The `dstSubpass` must always be higher than `srcSubpass` to -prevent cycles in the dependency graph (unless one of the subpasses is -`VK_SUBPASS_EXTERNAL`). +처음 두 필드는 종속성과 종속되는 서브패스의 인덱스를 지정합니다. 특별한 값 `VK_SUBPASS_EXTERNAL`은 `srcSubpass`에 지정되었는지 `dstSubpass`에 지정되었는지에 따라 렌더 패스 전후의 암시적 서브패스를 나타냅니다. 인덱스 `0`은 우리의 서브패스를 가리키며, 이는 첫 번째이자 유일한 서브패스입니다. `dstSubpass`는 종속성 그래프에서 순환을 방지하기 위해 항상 `srcSubpass`보다 높아야 합니다(서브패스 중 하나가 `VK_SUBPASS_EXTERNAL`이 아닌 경우). ```c++ dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dependency.srcAccessMask = 0; ``` -The next two fields specify the operations to wait on and the stages in which -these operations occur. We need to wait for the swap chain to finish reading -from the image before we can access it. This can be accomplished by waiting on -the color attachment output stage itself. +다음 두 필드는 대기할 작업과 이러한 작업이 발생하는 단계를 지정합니다. 우리가 이미지에 접근하기 전에 스왑 체인이 이미지 읽기를 마칠 때까지 기다려야 합니다. 이는 컬러 어태치먼트 출력 단계 자체를 기다림으로써 달성할 수 있습니다. ```c++ dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; ``` -The operations that should wait on this are in the color attachment stage and -involve the writing of the color attachment. These settings will -prevent the transition from happening until it's actually necessary (and -allowed): when we want to start writing colors to it. +이것을 기다려야 하는 작업은 컬러 어태치먼트 단계에 있으며, 컬러 어태치먼트 쓰기를 포함합니다. 이러한 설정은 실제로 필요하고 허용될 때까지(즉, 우리가 색상을 쓰기 시작하고 싶을 때까지) 전환이 일어나지 않도록 막아줍니다. ```c++ renderPassInfo.dependencyCount = 1; renderPassInfo.pDependencies = &dependency; ``` -The `VkRenderPassCreateInfo` struct has two fields to specify an array of -dependencies. +`VkRenderPassCreateInfo` 구조체에는 종속성 배열을 지정하는 두 개의 필드가 있습니다. -## Presentation +## 프레젠테이션(Presentation) -The last step of drawing a frame is submitting the result back to the swap chain -to have it eventually show up on the screen. Presentation is configured through -a `VkPresentInfoKHR` structure at the end of the `drawFrame` function. +프레임을 그리는 마지막 단계는 결과를 다시 스왑 체인에 제출하여最终적으로 화면에 나타나게 하는 것입니다. 프레젠테이션은 `drawFrame` 함수 끝에서 `VkPresentInfoKHR` 구조체를 통해 구성됩니다. ```c++ VkPresentInfoKHR presentInfo{}; @@ -487,11 +337,7 @@ presentInfo.waitSemaphoreCount = 1; presentInfo.pWaitSemaphores = signalSemaphores; ``` -The first two parameters specify which semaphores to wait on before presentation -can happen, just like `VkSubmitInfo`. Since we want to wait on the command buffer -to finish execution, thus our triangle being drawn, we take the semaphores -which will be signalled and wait on them, thus we use `signalSemaphores`. - +처음 두 파라미터는 `VkSubmitInfo`와 마찬가지로 프레젠테이션이 일어나기 전에 대기할 세마포어를 지정합니다. 우리는 커맨드 버퍼의 실행이 끝나기를, 즉 삼각형이 그려지기를 기다리고 싶으므로 신호를 받을 세마포어를 가져와서 그것들을 기다립니다. 따라서 `signalSemaphores`를 사용합니다. ```c++ VkSwapchainKHR swapChains[] = {swapChain}; @@ -500,48 +346,33 @@ presentInfo.pSwapchains = swapChains; presentInfo.pImageIndices = &imageIndex; ``` -The next two parameters specify the swap chains to present images to and the -index of the image for each swap chain. This will almost always be a single one. +다음 두 파라미터는 이미지를 제시할 스왑 체인과 각 스왑 체인에 대한 이미지의 인덱스를 지정합니다. 이것은 거의 항상 단일 스왑 체인일 것입니다. ```c++ presentInfo.pResults = nullptr; // Optional ``` -There is one last optional parameter called `pResults`. It allows you to specify -an array of `VkResult` values to check for every individual swap chain if -presentation was successful. It's not necessary if you're only using a single -swap chain, because you can simply use the return value of the present function. +`pResults`라는 마지막 선택적 파라미터가 있습니다. 이를 통해 각 개별 스왑 체인에 대해 프레젠테이션이 성공했는지 확인할 `VkResult` 값의 배열을 지정할 수 있습니다. 단일 스왑 체인만 사용하는 경우에는 필요하지 않습니다. 왜냐하면 프레젠테이션 함수의 반환 값을 간단히 사용할 수 있기 때문입니다. ```c++ vkQueuePresentKHR(presentQueue, &presentInfo); ``` -The `vkQueuePresentKHR` function submits the request to present an image to the -swap chain. We'll add error handling for both `vkAcquireNextImageKHR` and -`vkQueuePresentKHR` in the next chapter, because their failure does not -necessarily mean that the program should terminate, unlike the functions we've -seen so far. +`vkQueuePresentKHR` 함수는 스왑 체인에 이미지를 제시하라는 요청을 제출합니다. 다음 장에서는 `vkAcquireNextImageKHR`와 `vkQueuePresentKHR`에 대한 오류 처리를 추가할 것입니다. 왜냐하면 지금까지 본 함수들과 달리 이 함수들의 실패가 반드시 프로그램 종료를 의미하지는 않기 때문입니다. -If you did everything correctly up to this point, then you should now see -something resembling the following when you run your program: +지금까지 모든 것을 올바르게 했다면, 프로그램을 실행했을 때 다음과 유사한 것을 보게 될 것입니다. ![](/images/triangle.png) ->This colored triangle may look a bit different from the one you're used to seeing in graphics tutorials. That's because this tutorial lets the shader interpolate in linear color space and converts to sRGB color space afterwards. See [this blog post](https://medium.com/@heypete/hello-triangle-meet-swift-and-wide-color-6f9e246616d9) for a discussion of the difference. +> 이 색상의 삼각형은 그래픽스 튜토리얼에서 흔히 보던 것과 약간 다를 수 있습니다. 이 튜토리얼은 셰이더가 선형(linear) 색 공간에서 보간하고 나중에 sRGB 색 공간으로 변환하기 때문입니다. 차이점에 대한 논의는 [이 블로그 포스트](https://medium.com/@heypete/hello-triangle-meet-swift-and-wide-color-6f9e246616d9)를 참조하세요. -Yay! Unfortunately, you'll see that when validation layers are enabled, the -program crashes as soon as you close it. The messages printed to the terminal -from `debugCallback` tell us why: +만세! 안타깝게도, 유효성 검사 레이어를 활성화하면 프로그램을 닫자마자 충돌하는 것을 보게 될 것입니다. `debugCallback`에서 터미널에 출력된 메시지가 그 이유를 알려줍니다. ![](/images/semaphore_in_use.png) -Remember that all of the operations in `drawFrame` are asynchronous. That means -that when we exit the loop in `mainLoop`, drawing and presentation operations -may still be going on. Cleaning up resources while that is happening is a bad -idea. +`drawFrame`의 모든 작업은 비동기적이라는 것을 기억하세요. 즉, `mainLoop`에서 루프를 빠져나올 때 드로잉 및 프레젠테이션 작업이 여전히 진행 중일 수 있습니다. 그 와중에 리소스를 정리하는 것은 좋지 않은 생각입니다. -To fix that problem, we should wait for the logical device to finish operations -before exiting `mainLoop` and destroying the window: +이 문제를 해결하려면 `mainLoop`를 종료하고 창을 파괴하기 전에 로지컬 디바이스가 작업을 마칠 때까지 기다려야 합니다. ```c++ void mainLoop() { @@ -554,24 +385,14 @@ void mainLoop() { } ``` -You can also wait for operations in a specific command queue to be finished with -`vkQueueWaitIdle`. These functions can be used as a very rudimentary way to -perform synchronization. You'll see that the program now exits without problems -when closing the window. +특정 커맨드 큐의 작업이 완료될 때까지 `vkQueueWaitIdle`로 기다릴 수도 있습니다. 이 함수들은 매우 초보적인 동기화 방법으로 사용될 수 있습니다. 이제 창을 닫을 때 프로그램이 문제없이 종료되는 것을 볼 수 있을 것입니다. -## Conclusion +## 결론 -A little over 900 lines of code later, we've finally gotten to the stage of seeing -something pop up on the screen! Bootstrapping a Vulkan program is definitely a -lot of work, but the take-away message is that Vulkan gives you an immense -amount of control through its explicitness. I recommend you to take some time -now to reread the code and build a mental model of the purpose of all of the -Vulkan objects in the program and how they relate to each other. We'll be -building on top of that knowledge to extend the functionality of the program -from this point on. +900줄이 넘는 코드를 작성한 끝에 마침내 화면에 무언가 나타나는 단계를 마쳤습니다! Vulkan 프로그램을 부트스트래핑하는 것은 확실히 많은 작업이지만, 여기서 얻을 수 있는 교훈은 Vulkan이 명시성을 통해 엄청난 제어권을 제공한다는 것입니다. 지금 시간을 내어 코드를 다시 읽어보고 프로그램의 모든 Vulkan 객체의 목적과 서로 어떻게 관련되어 있는지에 대한 정신 모델을 구축하는 것을 추천합니다. 이제부터 이 지식을 바탕으로 프로그램의 기능을 확장해 나갈 것입니다. -The next chapter will expand the render loop to handle multiple frames in flight. +다음 장에서는 여러 프레임을 동시에 처리하도록 렌더링 루프를 확장할 것입니다. -[C++ code](/code/15_hello_triangle.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +[C++ 코드](/code/15_hello_triangle.cpp) / +[버텍스 셰이더](/code/09_shader_base.vert) / +[프래그먼트 셰이더](/code/09_shader_base.frag) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md b/ko/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md index e2345e31..54b95e3d 100644 --- a/ko/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md +++ b/ko/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md @@ -1,35 +1,20 @@ -## Frames in flight +## 동시에 여러 프레임 렌더링하기 (Frames in flight) -Right now our render loop has one glaring flaw. We are required to wait on the -previous frame to finish before we can start rendering the next which results -in unnecessary idling of the host. +현재 우리의 렌더 루프에는 한 가지 명백한 결함이 있습니다. 이전 프레임의 렌더링이 끝나기를 기다려야만 다음 프레임의 렌더링을 시작할 수 있다는 점인데, 이는 호스트(CPU)의 불필요한 유휴 상태를 유발합니다. -The way to fix this is to allow multiple frames to be *in-flight* at once, that -is to say, allow the rendering of one frame to not interfere with the recording -of the next. How do we do this? Any resource that is accessed and modified -during rendering must be duplicated. Thus, we need multiple command buffers, -semaphores, and fences. In later chapters we will also add multiple instances -of other resources, so we will see this concept reappear. +이 문제를 해결하는 방법은 여러 프레임을 동시에 *작업 중(in-flight)* 상태로 두는 것입니다. 즉, 한 프레임의 렌더링이 다음 프레임의 기록을 방해하지 않도록 하는 것입니다. 어떻게 이렇게 할 수 있을까요? 렌더링 중에 접근하고 수정하는 모든 리소스를 복제해야 합니다. 따라서 여러 개의 커맨드 버퍼, 세마포어, 펜스가 필요합니다. 이후 챕터에서는 다른 리소스들의 여러 인스턴스도 추가할 것이므로, 이 개념은 다시 등장하게 될 것입니다. -Start by adding a constant at the top of the program that defines how many -frames should be processed concurrently: +먼저 프로그램 상단에 동시에 처리할 프레임 수를 정의하는 상수를 추가합니다. ```c++ const int MAX_FRAMES_IN_FLIGHT = 2; ``` -We choose the number 2 because we don't want the CPU to get *too* far ahead of -the GPU. With 2 frames in flight, the CPU and the GPU can be working on their -own tasks at the same time. If the CPU finishes early, it will wait till the -GPU finishes rendering before submitting more work. With 3 or more frames in -flight, the CPU could get ahead of the GPU, adding frames of latency. -Generally, extra latency isn't desired. But giving the application control over -the number of frames in flight is another example of Vulkan being explicit. +2를 선택한 이유는 CPU가 GPU보다 *너무* 앞서 나가는 것을 원치 않기 때문입니다. 2개의 프레임이 동시 실행되면, CPU와 GPU가 동시에 각자의 작업을 처리할 수 있습니다. 만약 CPU가 먼저 작업을 마치면, GPU가 렌더링을 마칠 때까지 기다렸다가 다음 작업을 제출합니다. 3개 이상의 프레임을 사용하면 CPU가 GPU를 앞질러 지연 시간(latency)을 추가할 수 있습니다. 일반적으로 추가적인 지연 시간은 바람직하지 않습니다. 하지만 애플리케이션에 동시 실행 프레임 수를 제어할 수 있는 권한을 주는 것은 Vulkan의 명시적인(explicit) 특성을 보여주는 또 다른 예시입니다. -Each frame should have its own command buffer, set of semaphores, and fence. -Rename and then change them to be `std::vector`s of the objects: +각 프레임은 자체적인 커맨드 버퍼, 세마포어 집합, 펜스를 가져야 합니다. 기존 객체들의 이름을 바꾸고 `std::vector`로 변경합니다. ```c++ std::vector commandBuffers; @@ -41,11 +26,7 @@ std::vector renderFinishedSemaphores; std::vector inFlightFences; ``` -Then we need to create multiple command buffers. Rename `createCommandBuffer` -to `createCommandBuffers`. Next we need to resize the command buffers vector -to the size of `MAX_FRAMES_IN_FLIGHT`, alter the `VkCommandBufferAllocateInfo` -to contain that many command buffers, and then change the destination to our -vector of command buffers: +다음으로 여러 개의 커맨드 버퍼를 생성해야 합니다. `createCommandBuffer`를 `createCommandBuffers`로 이름을 바꿉니다. 그리고 커맨드 버퍼 벡터의 크기를 `MAX_FRAMES_IN_FLIGHT`로 조절하고, `VkCommandBufferAllocateInfo`가 해당 개수만큼의 커맨드 버퍼를 담도록 수정하며, 할당 대상을 우리의 커맨드 버퍼 벡터로 변경해야 합니다. ```c++ void createCommandBuffers() { @@ -59,7 +40,7 @@ void createCommandBuffers() { } ``` -The `createSyncObjects` function should be changed to create all of the objects: +`createSyncObjects` 함수는 모든 동기화 객체들을 생성하도록 변경해야 합니다. ```c++ void createSyncObjects() { @@ -85,7 +66,7 @@ void createSyncObjects() { } ``` -Similarly, they should also all be cleaned up: +마찬가지로, 이 객체들도 모두 정리(cleanup)되어야 합니다. ```c++ void cleanup() { @@ -99,17 +80,15 @@ void cleanup() { } ``` -Remember, because command buffers are freed for us when we free the command -pool, there is nothing extra to do for command buffer cleanup. +커맨드 버퍼는 커맨드 풀이 해제될 때 자동으로 해제되므로, 커맨드 버퍼 정리를 위해 추가로 할 일은 없다는 점을 기억하세요. -To use the right objects every frame, we need to keep track of the current -frame. We will use a frame index for that purpose: +매 프레임마다 올바른 객체를 사용하기 위해, 현재 프레임을 추적해야 합니다. 이를 위해 프레임 인덱스를 사용하겠습니다. ```c++ uint32_t currentFrame = 0; ``` -The `drawFrame` function can now be modified to use the right objects: +이제 `drawFrame` 함수를 올바른 객체들을 사용하도록 수정할 수 있습니다. ```c++ void drawFrame() { @@ -141,7 +120,7 @@ void drawFrame() { } ``` -Of course, we shouldn't forget to advance to the next frame every time: +물론, 매번 다음 프레임으로 넘어가는 것을 잊지 말아야 합니다. ```c++ void drawFrame() { @@ -151,26 +130,17 @@ void drawFrame() { } ``` -By using the modulo (%) operator, we ensure that the frame index loops around -after every `MAX_FRAMES_IN_FLIGHT` enqueued frames. +나머지(%) 연산자를 사용함으로써, 프레임 인덱스는 `MAX_FRAMES_IN_FLIGHT` 만큼의 프레임이 큐에 쌓인 후 다시 순환하게 됩니다. -We've now implemented all the needed synchronization to ensure that there are -no more than `MAX_FRAMES_IN_FLIGHT` frames of work enqueued and that these -frames are not stepping over eachother. Note that it is fine for other parts of -the code, like the final cleanup, to rely on more rough synchronization like -`vkDeviceWaitIdle`. You should decide on which approach to use based on -performance requirements. +이제 우리는 최대 `MAX_FRAMES_IN_FLIGHT`개의 프레임만 작업 큐에 쌓이도록 하고, 이 프레임들이 서로를 침범하지 않도록 하는 데 필요한 모든 동기화를 구현했습니다. 최종 정리(cleanup)와 같은 코드의 다른 부분에서는 `vkDeviceWaitIdle`처럼 더 단순한 동기화에 의존해도 괜찮다는 점에 유의하세요. 성능 요구사항에 따라 어떤 접근 방식을 사용할지 결정해야 합니다. -To learn more about synchronization through examples, have a look at [this extensive overview](https://github.com/KhronosGroup/Vulkan-Docs/wiki/Synchronization-Examples#swapchain-image-acquire-and-present) by Khronos. +동기화에 대해 예제를 통해 더 배우고 싶다면, Khronos의 [이 광범위한 개요](https://github.com/KhronosGroup/Vulkan-Docs/wiki/Synchronization-Examples#swapchain-image-acquire-and-present)를 살펴보세요. +다음 챕터에서는 잘 동작하는 Vulkan 프로그램을 위해 필요한 또 다른 작은 사항을 다룰 것입니다. -In the next chapter we'll deal with one more small thing that is required for a -well-behaved Vulkan program. - - -[C++ code](/code/16_frames_in_flight.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +[C++ 코드](/code/16_frames_in_flight.cpp) / +[정점 셰이더](/code/09_shader_base.vert) / +[프래그먼트 셰이더](/code/09_shader_base.frag) \ No newline at end of file diff --git a/ko/03_Drawing_a_triangle/04_Swap_chain_recreation.md b/ko/03_Drawing_a_triangle/04_Swap_chain_recreation.md index ce58528b..54f5aa5b 100644 --- a/ko/03_Drawing_a_triangle/04_Swap_chain_recreation.md +++ b/ko/03_Drawing_a_triangle/04_Swap_chain_recreation.md @@ -1,16 +1,10 @@ -## Introduction +## 소개 -The application we have now successfully draws a triangle, but there are some -circumstances that it isn't handling properly yet. It is possible for the window -surface to change such that the swap chain is no longer compatible with it. One -of the reasons that could cause this to happen is the size of the window -changing. We have to catch these events and recreate the swap chain. +지금까지 우리가 만든 애플리케이션은 성공적으로 삼각형을 그리지만, 아직 제대로 처리하지 못하는 몇 가지 상황이 있습니다. 윈도우 서피스가 변경되어 스왑 체인이 더 이상 호환되지 않는 경우가 발생할 수 있습니다. 이런 상황이 발생하는 원인 중 하나는 윈도우 크기가 변경되는 것입니다. 우리는 이러한 이벤트를 감지하고 스왑 체인을 다시 만들어야 합니다. -## Recreating the swap chain +## 스왑 체인 재구성 -Create a new `recreateSwapChain` function that calls `createSwapChain` and all -of the creation functions for the objects that depend on the swap chain or the -window size. +`recreateSwapChain`이라는 새로운 함수를 만들어, `createSwapChain`과 스왑 체인 또는 윈도우 크기에 의존하는 모든 객체들의 생성 함수를 호출하도록 합시다. ```c++ void recreateSwapChain() { @@ -22,16 +16,9 @@ void recreateSwapChain() { } ``` -We first call `vkDeviceWaitIdle`, because just like in the last chapter, we -shouldn't touch resources that may still be in use. Obviously, we'll have to recreate -the swap chain itself. The image views need to be recreated because they are based -directly on the swap chain images. Finally, the framebuffers directly depend on the -swap chain images, and thus must be recreated as well. +먼저 `vkDeviceWaitIdle`을 호출하는데, 이는 이전 장에서와 마찬가지로 아직 사용 중일 수 있는 리소스에 접근해서는 안 되기 때문입니다. 당연히 스왑 체인 자체를 다시 만들어야 합니다. 이미지 뷰는 스왑 체인 이미지에 직접 기반하므로 다시 만들어야 합니다. 마지막으로, 프레임버퍼는 스왑 체인 이미지에 직접 의존하므로 다시 만들어야 합니다. -To make sure that the old versions of these objects are cleaned up before -recreating them, we should move some of the cleanup code to a separate function -that we can call from the `recreateSwapChain` function. Let's call it -`cleanupSwapChain`: +이러한 객체들의 이전 버전이 재생성되기 전에 확실히 정리되도록, 일부 정리 코드를 별도의 함수로 옮겨 `recreateSwapChain` 함수에서 호출하도록 합시다. 이 함수를 `cleanupSwapChain`이라고 부르겠습니다. ```c++ void cleanupSwapChain() { @@ -49,10 +36,9 @@ void recreateSwapChain() { } ``` -Note that we don't recreate the renderpass here for simplicity. In theory it can be possible for the swap chain image format to change during an applications' lifetime, e.g. when moving a window from a standard range to a high dynamic range monitor. This may require the application to recreate the renderpass to make sure the change between dynamic ranges is properly reflected. +여기서는 간단하게 하기 위해 렌더 패스를 다시 만들지 않는다는 점에 유의하세요. 이론적으로는 애플리케이션 실행 중에 스왑 체인 이미지 포맷이 변경될 수 있습니다. 예를 들어, 표준 다이나믹 레인지(SDR) 모니터에서 하이 다이나믹 레인지(HDR) 모니터로 창을 옮기는 경우가 그렇습니다. 이 경우 다이나믹 레인지 간의 변경이 올바르게 반영되도록 애플리케이션이 렌더 패스를 다시 만들어야 할 수도 있습니다. -We'll move the cleanup code of all objects that are recreated as part of a swap -chain refresh from `cleanup` to `cleanupSwapChain`: +스왑 체인 갱신의 일부로 재생성되는 모든 객체들의 정리 코드를 `cleanup`에서 `cleanupSwapChain`으로 옮기겠습니다. ```c++ void cleanupSwapChain() { @@ -98,30 +84,16 @@ void cleanup() { } ``` -Note that in `chooseSwapExtent` we already query the new window resolution to -make sure that the swap chain images have the (new) right size, so there's no -need to modify `chooseSwapExtent` (remember that we already had to use -`glfwGetFramebufferSize` to get the resolution of the surface in pixels when -creating the swap chain). +`chooseSwapExtent`에서는 이미 새로운 윈도우 해상도를 조회하여 스왑 체인 이미지가 (새로운) 올바른 크기를 갖도록 하고 있으므로, `chooseSwapExtent`를 수정할 필요는 없습니다 (스왑 체인을 만들 때 이미 서피스의 해상도를 픽셀 단위로 얻기 위해 `glfwGetFramebufferSize`를 사용해야 했던 것을 기억하세요). -That's all it takes to recreate the swap chain! However, the disadvantage of -this approach is that we need to stop all rendering before creating the new swap -chain. It is possible to create a new swap chain while drawing commands on an -image from the old swap chain are still in-flight. You need to pass the previous -swap chain to the `oldSwapChain` field in the `VkSwapchainCreateInfoKHR` struct -and destroy the old swap chain as soon as you've finished using it. +이것만으로도 스왑 체인을 재구성할 수 있습니다! 하지만 이 방법의 단점은 새로운 스왑 체인을 만들기 전에 모든 렌더링을 중단해야 한다는 것입니다. 이전 스왑 체인의 이미지에 대한 그리기 명령이 아직 실행 중인 상태에서 새로운 스왑 체인을 만드는 것도 가능합니다. 그러려면 `VkSwapchainCreateInfoKHR` 구조체의 `oldSwapChain` 필드에 이전 스왑 체인을 전달하고, 이전 스왑 체인 사용이 끝나는 즉시 파괴해야 합니다. -## Suboptimal or out-of-date swap chain +## 준최적(Suboptimal) 또는 오래된(out-of-date) 스왑 체인 -Now we just need to figure out when swap chain recreation is necessary and call -our new `recreateSwapChain` function. Luckily, Vulkan will usually just tell us that the swap chain is no longer adequate during presentation. The `vkAcquireNextImageKHR` and -`vkQueuePresentKHR` functions can return the following special values to -indicate this. +이제 스왑 체인 재구성이 언제 필요한지 파악하고 새로운 `recreateSwapChain` 함수를 호출하기만 하면 됩니다. 다행히도 Vulkan은 보통 프레젠테이션 중에 스왑 체인이 더 이상 적합하지 않다고 알려줍니다. `vkAcquireNextImageKHR`와 `vkQueuePresentKHR` 함수는 이를 나타내기 위해 다음과 같은 특별한 값들을 반환할 수 있습니다. -* `VK_ERROR_OUT_OF_DATE_KHR`: The swap chain has become incompatible with the -surface and can no longer be used for rendering. Usually happens after a window resize. -* `VK_SUBOPTIMAL_KHR`: The swap chain can still be used to successfully present -to the surface, but the surface properties are no longer matched exactly. +* `VK_ERROR_OUT_OF_DATE_KHR`: 스왑 체인이 서피스와 호환되지 않게 되어 더 이상 렌더링에 사용할 수 없습니다. 보통 윈도우 리사이즈 후에 발생합니다. +* `VK_SUBOPTIMAL_KHR`: 스왑 체인을 여전히 성공적으로 서피스에 표시할 수는 있지만, 서피스의 속성이 더 이상 정확하게 일치하지 않습니다. ```c++ VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); @@ -134,13 +106,9 @@ if (result == VK_ERROR_OUT_OF_DATE_KHR) { } ``` -If the swap chain turns out to be out of date when attempting to acquire an -image, then it is no longer possible to present to it. Therefore we should -immediately recreate the swap chain and try again in the next `drawFrame` call. +이미지를 가져오려고 할 때 스왑 체인이 오래된(out-of-date) 것으로 판명되면, 더 이상 프레젠테이션을 할 수 없습니다. 따라서 즉시 스왑 체인을 재구성하고 다음 `drawFrame` 호출에서 다시 시도해야 합니다. -You could also decide to do that if the swap chain is suboptimal, but I've -chosen to proceed anyway in that case because we've already acquired an image. -Both `VK_SUCCESS` and `VK_SUBOPTIMAL_KHR` are considered "success" return codes. +스왑 체인이 준최적(suboptimal)일 때도 재구성을 결정할 수 있지만, 여기서는 이미 이미지를 획득했기 때문에 그대로 진행하기로 했습니다. `VK_SUCCESS`와 `VK_SUBOPTIMAL_KHR` 모두 "성공" 반환 코드로 간주됩니다. ```c++ result = vkQueuePresentKHR(presentQueue, &presentInfo); @@ -154,26 +122,15 @@ if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; ``` -The `vkQueuePresentKHR` function returns the same values with the same meaning. -In this case we will also recreate the swap chain if it is suboptimal, because -we want the best possible result. +`vkQueuePresentKHR` 함수도 같은 의미로 동일한 값들을 반환합니다. 이 경우에는 최상의 결과를 원하기 때문에 준최적일 때도 스왑 체인을 재구성할 것입니다. -## Fixing a deadlock +## 데드락 해결하기 -If we try to run the code now, it is possible to encounter a deadlock. -Debugging the code, we find that the application reaches `vkWaitForFences` but -never continues past it. This is because when `vkAcquireNextImageKHR` returns -`VK_ERROR_OUT_OF_DATE_KHR`, we recreate the swapchain and then return from -`drawFrame`. But before that happens, the current frame's fence was waited upon -and reset. Since we return immediately, no work is submitted for execution and -the fence will never be signaled, causing `vkWaitForFences` to halt forever. +지금 코드를 실행하면 데드락이 발생할 수 있습니다. 코드를 디버깅해보면, 애플리케이션이 `vkWaitForFences`에 도달한 후 더 이상 진행하지 못하고 멈추는 것을 발견할 수 있습니다. 이는 `vkAcquireNextImageKHR`가 `VK_ERROR_OUT_OF_DATE_KHR`를 반환할 때, 우리가 스왑 체인을 재구성한 후 `drawFrame`에서 즉시 반환하기 때문입니다. 하지만 그 전에, 현재 프레임의 펜스는 대기 상태에 들어간 후 리셋되었습니다. 우리가 즉시 반환하므로 아무 작업도 제출되지 않고, 따라서 펜스는 절대 신호를 받지 못하게 되어 `vkWaitForFences`가 영원히 멈추게 됩니다. -There is a simple fix thankfully. Delay resetting the fence until after we -know for sure we will be submitting work with it. Thus, if we return early, the -fence is still signaled and `vkWaitForFences` wont deadlock the next time we -use the same fence object. +다행히 간단한 해결책이 있습니다. 펜스를 리셋하는 것을, 우리가 확실히 작업을 제출할 것이라는 것을 안 이후로 미루는 것입니다. 이렇게 하면, 우리가 일찍 반환하더라도 펜스는 여전히 신호를 받은 상태(signaled)로 남아있어, 다음에 같은 펜스 객체를 사용할 때 `vkWaitForFences`가 데드락을 일으키지 않을 것입니다. -The beginning of `drawFrame` should now look like this: +이제 `drawFrame` 함수의 시작 부분은 다음과 같아야 합니다: ```c++ vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); @@ -187,13 +144,13 @@ if (result == VK_ERROR_OUT_OF_DATE_KHR) { throw std::runtime_error("failed to acquire swap chain image!"); } -// Only reset the fence if we are submitting work +// 작업을 제출할 때만 펜스를 리셋합니다. vkResetFences(device, 1, &inFlightFences[currentFrame]); ``` -## Handling resizes explicitly +## 명시적으로 리사이즈 처리하기 -Although many drivers and platforms trigger `VK_ERROR_OUT_OF_DATE_KHR` automatically after a window resize, it is not guaranteed to happen. That's why we'll add some extra code to also handle resizes explicitly. First add a new member variable that flags that a resize has happened: +많은 드라이버와 플랫폼이 윈도우 리사이즈 후 자동으로 `VK_ERROR_OUT_OF_DATE_KHR`를 발생시키지만, 이것이 보장되지는 않습니다. 그래서 우리는 리사이즈를 명시적으로 처리하는 코드를 추가할 것입니다. 먼저 리사이즈가 발생했음을 알리는 플래그 멤버 변수를 추가합니다: ```c++ std::vector inFlightFences; @@ -201,7 +158,7 @@ std::vector inFlightFences; bool framebufferResized = false; ``` -The `drawFrame` function should then be modified to also check for this flag: +그런 다음 `drawFrame` 함수를 이 플래그도 확인하도록 수정해야 합니다: ```c++ if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) { @@ -212,7 +169,7 @@ if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebu } ``` -It is important to do this after `vkQueuePresentKHR` to ensure that the semaphores are in a consistent state, otherwise a signaled semaphore may never be properly waited upon. Now to actually detect resizes we can use the `glfwSetFramebufferSizeCallback` function in the GLFW framework to set up a callback: +세마포어들이 일관된 상태를 유지하도록 `vkQueuePresentKHR` 이후에 이 작업을 수행하는 것이 중요합니다. 그렇지 않으면 신호를 받은 세마포어가 제대로 대기 상태에 들어가지 못할 수 있습니다. 이제 실제로 리사이즈를 감지하기 위해 GLFW 프레임워크의 `glfwSetFramebufferSizeCallback` 함수를 사용하여 콜백을 설정할 수 있습니다: ```c++ void initWindow() { @@ -229,9 +186,9 @@ static void framebufferResizeCallback(GLFWwindow* window, int width, int height) } ``` -The reason that we're creating a `static` function as a callback is because GLFW does not know how to properly call a member function with the right `this` pointer to our `HelloTriangleApplication` instance. +콜백을 `static` 함수로 만드는 이유는 GLFW가 `HelloTriangleApplication` 인스턴스에 대한 올바른 `this` 포인터를 가지고 멤버 함수를 호출하는 방법을 모르기 때문입니다. -However, we do get a reference to the `GLFWwindow` in the callback and there is another GLFW function that allows you to store an arbitrary pointer inside of it: `glfwSetWindowUserPointer`: +하지만 콜백에서 `GLFWwindow`에 대한 참조를 얻을 수 있으며, 임의의 포인터를 저장할 수 있는 다른 GLFW 함수가 있습니다: `glfwSetWindowUserPointer`: ```c++ window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); @@ -239,7 +196,7 @@ glfwSetWindowUserPointer(window, this); glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); ``` -This value can now be retrieved from within the callback with `glfwGetWindowUserPointer` to properly set the flag: +이제 이 값은 `glfwGetWindowUserPointer`를 사용하여 콜백 내에서 가져와 플래그를 올바르게 설정할 수 있습니다: ```c++ static void framebufferResizeCallback(GLFWwindow* window, int width, int height) { @@ -248,11 +205,11 @@ static void framebufferResizeCallback(GLFWwindow* window, int width, int height) } ``` -Now try to run the program and resize the window to see if the framebuffer is indeed resized properly with the window. +이제 프로그램을 실행하고 윈도우 크기를 조절하여 프레임버퍼가 윈도우에 맞게 올바르게 리사이즈되는지 확인해 보세요. -## Handling minimization +## 창 최소화 처리하기 -There is another case where a swap chain may become out of date and that is a special kind of window resizing: window minimization. This case is special because it will result in a frame buffer size of `0`. In this tutorial we will handle that by pausing until the window is in the foreground again by extending the `recreateSwapChain` function: +스왑 체인이 오래될 수 있는 또 다른 경우는 특별한 종류의 윈도우 리사이즈인 창 최소화입니다. 이 경우는 프레임버퍼 크기가 `0`이 되기 때문에 특별합니다. 이 튜토리얼에서는 윈도우가 다시 전경에 올 때까지 일시 중지하는 방식으로 이 문제를 처리할 것입니다. `recreateSwapChain` 함수를 다음과 같이 확장합니다: ```c++ void recreateSwapChain() { @@ -269,12 +226,10 @@ void recreateSwapChain() { } ``` -The initial call to `glfwGetFramebufferSize` handles the case where the size is already correct and `glfwWaitEvents` would have nothing to wait on. +초기 `glfwGetFramebufferSize` 호출은 이미 크기가 올바르고 `glfwWaitEvents`가 기다릴 것이 없는 경우를 처리합니다. -Congratulations, you've now finished your very first well-behaved Vulkan -program! In the next chapter we're going to get rid of the hardcoded vertices in -the vertex shader and actually use a vertex buffer. +축하합니다, 여러분은 이제 최초의 잘 동작하는(well-behaved) Vulkan 프로그램을 완성했습니다! 다음 장에서는 버텍스 셰이더에 하드코딩된 정점들을 제거하고 실제로 정점 버퍼(vertex buffer)를 사용할 것입니다. -[C++ code](/code/17_swap_chain_recreation.cpp) / -[Vertex shader](/code/09_shader_base.vert) / -[Fragment shader](/code/09_shader_base.frag) +[C++ 코드](/code/17_swap_chain_recreation.cpp) / +[버텍스 셰이더](/code/09_shader_base.vert) / +[프래그먼트 셰이더](/code/09_shader_base.frag) \ No newline at end of file From aebd58e5461491044d70c0c64a0a096e93a3e779 Mon Sep 17 00:00:00 2001 From: erenengine Date: Sat, 21 Jun 2025 12:37:02 +0900 Subject: [PATCH 3/4] Add combined image sampler support and update shaders for texture mapping - Introduced combined image sampler descriptor in the Vulkan pipeline. - Updated descriptor set layout, pool, and sets to accommodate the new sampler. - Modified Vertex structure to include texture coordinates for proper mapping. - Adjusted fragment shader to sample colors from the texture using the new sampler. - Added visualizations for texture coordinates and demonstrated addressing modes. - Enhanced shader functionality to manipulate texture colors with vertex colors. --- .../00_Vertex_input_description.md | 262 ++-- .../01_Vertex_buffer_creation.md | 463 +++----- .../04_Vertex_buffers/02_Staging_buffer.md | 445 +++---- ko-rust/04_Vertex_buffers/03_Index_buffer.md | 290 +++-- .../00_Descriptor_set_layout_and_buffer.md | 546 ++++----- .../01_Descriptor_pool_and_sets.md | 510 ++++---- ko-rust/06_Texture_mapping/00_Images.md | 1052 ++++++----------- .../01_Image_view_and_sampler.md | 546 +++++---- .../02_Combined_image_sampler.md | 378 +++--- .../00_Vertex_input_description.md | 150 +-- .../01_Vertex_buffer_creation.md | 183 +-- ko/04_Vertex_buffers/02_Staging_buffer.md | 173 +-- ko/04_Vertex_buffers/03_Index_buffer.md | 107 +- .../00_Descriptor_set_layout_and_buffer.md | 207 +--- .../01_Descriptor_pool_and_sets.md | 176 +-- ko/06_Texture_mapping/00_Images.md | 432 ++----- .../01_Image_view_and_sampler.md | 177 +-- .../02_Combined_image_sampler.md | 131 +- 18 files changed, 2442 insertions(+), 3786 deletions(-) diff --git a/ko-rust/04_Vertex_buffers/00_Vertex_input_description.md b/ko-rust/04_Vertex_buffers/00_Vertex_input_description.md index e7da3e4f..c67f026d 100644 --- a/ko-rust/04_Vertex_buffers/00_Vertex_input_description.md +++ b/ko-rust/04_Vertex_buffers/00_Vertex_input_description.md @@ -1,16 +1,10 @@ -## Introduction +## 소개 -In the next few chapters, we're going to replace the hardcoded vertex data in -the vertex shader with a vertex buffer in memory. We'll start with the easiest -approach of creating a CPU visible buffer and using `memcpy` to copy the vertex -data into it directly, and after that we'll see how to use a staging buffer to -copy the vertex data to high performance memory. +다음 몇 개의 챕터에서는 버텍스 셰이더에 하드코딩된 정점 데이터를 메모리의 버텍스 버퍼로 교체할 것입니다. 가장 쉬운 접근법으로 시작하여, CPU에서 볼 수 있는(visible) 버퍼를 만들고 메모리 복사를 통해 정점 데이터를 직접 GPU로 전달하는 방법을 알아볼 것입니다. 그 후에는 스테이징 버퍼(staging buffer)를 사용해 정점 데이터를 고성능 메모리로 복사하는 방법도 살펴볼 것입니다. -## Vertex shader +## 버텍스 셰이더 -First change the vertex shader to no longer include the vertex data in the -shader code itself. The vertex shader takes input from a vertex buffer using the -`in` keyword. +먼저 버텍스 셰이더를 변경하여, 셰이더 코드 자체에 더 이상 정점 데이터를 포함하지 않도록 합니다. 버텍스 셰이더는 `in` 키워드를 사용하여 버텍스 버퍼로부터 입력을 받습니다. ```glsl #version 450 @@ -26,200 +20,142 @@ void main() { } ``` -The `inPosition` and `inColor` variables are *vertex attributes*. They're -properties that are specified per-vertex in the vertex buffer, just like we -manually specified a position and color per vertex using the two arrays. Make -sure to recompile the vertex shader! +`inPosition`과 `inColor` 변수는 **정점 속성(vertex attributes)**입니다. 이들은 우리가 이전에 수동으로 위치와 색상을 지정했던 것처럼, 버텍스 버퍼에서 정점 단위로 지정되는 속성입니다. 버텍스 셰이더를 다시 컴파일하는 것을 잊지 마세요! -Just like `fragColor`, the `layout(location = x)` annotations assign indices to -the inputs that we can later use to reference them. It is important to know that -some types, like `dvec3` 64 bit vectors, use multiple *slots*. That means that -the index after it must be at least 2 higher: +`fragColor`와 마찬가지로, `layout(location = x)` 어노테이션은 나중에 참조할 수 있도록 입력에 인덱스를 할당합니다. 64비트 벡터인 `dvec3` 같은 일부 타입은 여러 개의 **슬롯(slot)**을 사용한다는 점을 아는 것이 중요합니다. 즉, 그 다음의 인덱스는 최소 2 이상 커야 합니다. ```glsl layout(location = 0) in dvec3 inPosition; layout(location = 2) in vec3 inColor; ``` -You can find more info about the layout qualifier in the [OpenGL wiki](https://www.khronos.org/opengl/wiki/Layout_Qualifier_(GLSL)). +레이아웃 한정자(layout qualifier)에 대한 더 많은 정보는 [OpenGL 위키](https://www.khronos.org/opengl/wiki/Layout_Qualifier_(GLSL))에서 찾을 수 있습니다. -## Vertex data +## 정점 데이터 -We're moving the vertex data from the shader code to an array in the code of our -program. Start by including the GLM library, which provides us with linear -algebra related types like vectors and matrices. We're going to use these types -to specify the position and color vectors. +이제 정점 데이터를 셰이더 코드에서 우리 Rust 애플리케이션의 코드로 옮길 것입니다. 먼저 Rust에서 가장 널리 사용되는 선형대수 라이브러리인 `glam`을 `Cargo.toml`에 추가합니다. `glam`은 벡터나 행렬 같은 타입을 제공합니다. -```c++ -#include +```toml +[dependencies] +glam = "0.24" ``` -Create a new structure called `Vertex` with the two attributes that we're going -to use in the vertex shader inside it: +이제 버텍스 셰이더에서 사용할 두 속성을 포함하는 `Vertex` 구조체를 새로 정의합니다. `#[repr(C)]` 어트리뷰트는 Rust 컴파일러가 C와 호환되도록 필드 순서를 보장하게 만들어, 메모리 레이아웃을 예측 가능하게 합니다. 이는 Vulkan API와 상호작용할 때 필수적입니다. -```c++ +```rust +use glam::{Vec2, Vec3}; + +#[repr(C)] +#[derive(Clone, Debug, Copy)] struct Vertex { - glm::vec2 pos; - glm::vec3 color; -}; + pos: Vec2, + color: Vec3, +} ``` -GLM conveniently provides us with C++ types that exactly match the vector types -used in the shader language. - -```c++ -const std::vector vertices = { - {{0.0f, -0.5f}, {1.0f, 0.0f, 0.0f}}, - {{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}}, - {{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}} -}; -``` +`glam`은 셰이더 언어에서 사용되는 벡터 타입과 정확히 일치하는 Rust 타입을 편리하게 제공합니다. -Now use the `Vertex` structure to specify an array of vertex data. We're using -exactly the same position and color values as before, but now they're combined -into one array of vertices. This is known as *interleaving* vertex attributes. +이제 `Vertex` 구조체를 사용하여 정점 데이터 배열을 정의합니다. 이전과 정확히 같은 위치와 색상 값을 사용하지만, 이제는 하나의 정점 배열로 결합되었습니다. 이를 **인터리빙(interleaving)** 정점 속성이라고 합니다. -## Binding descriptions +```rust +const VERTICES: [Vertex; 3] = [ + Vertex { pos: Vec2::new(0.0, -0.5), color: Vec3::new(1.0, 0.0, 0.0) }, + Vertex { pos: Vec2::new(0.5, 0.5), color: Vec3::new(0.0, 1.0, 0.0) }, + Vertex { pos: Vec2::new(-0.5, 0.5), color: Vec3::new(0.0, 0.0, 1.0) }, +]; +``` -The next step is to tell Vulkan how to pass this data format to the vertex -shader once it's been uploaded into GPU memory. There are two types of -structures needed to convey this information. +## 바인딩 서술 (Binding descriptions) -The first structure is `VkVertexInputBindingDescription` and we'll add a member -function to the `Vertex` struct to populate it with the right data. +다음 단계는 이 데이터 포맷이 GPU 메모리에 업로드된 후, 버텍스 셰이더로 어떻게 전달될지를 Vulkan에게 알려주는 것입니다. 이를 위해 두 종류의 구조체를 설정해야 합니다. -```c++ -struct Vertex { - glm::vec2 pos; - glm::vec3 color; +첫 번째 구조체는 `ash::vk::VertexInputBindingDescription`입니다. `Vertex` 구조체에 대한 `impl` 블록 내에 연관 함수(associated function)를 추가하여 이 구조체를 생성하도록 하겠습니다. - static VkVertexInputBindingDescription getBindingDescription() { - VkVertexInputBindingDescription bindingDescription{}; +```rust +use ash::vk; - return bindingDescription; +impl Vertex { + pub fn get_binding_description() -> vk::VertexInputBindingDescription { + vk::VertexInputBindingDescription { + binding: 0, + stride: std::mem::size_of::() as u32, + input_rate: vk::VertexInputRate::VERTEX, + } } -}; -``` - -A vertex binding describes at which rate to load data from memory throughout the -vertices. It specifies the number of bytes between data entries and whether to -move to the next data entry after each vertex or after each instance. - -```c++ -VkVertexInputBindingDescription bindingDescription{}; -bindingDescription.binding = 0; -bindingDescription.stride = sizeof(Vertex); -bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; +} ``` -All of our per-vertex data is packed together in one array, so we're only going -to have one binding. The `binding` parameter specifies the index of the binding -in the array of bindings. The `stride` parameter specifies the number of bytes -from one entry to the next, and the `inputRate` parameter can have one of the -following values: - -* `VK_VERTEX_INPUT_RATE_VERTEX`: Move to the next data entry after each vertex -* `VK_VERTEX_INPUT_RATE_INSTANCE`: Move to the next data entry after each -instance - -We're not going to use instanced rendering, so we'll stick to per-vertex data. +정점 바인딩(vertex binding)은 정점들 전체에서 메모리로부터 데이터를 어떤 속도(rate)로 로드할지 서술합니다. 이는 데이터 항목 사이의 바이트 수와 각 정점 또는 각 인스턴스 이후에 다음 데이터 항목으로 이동할지 여부를 지정합니다. -## Attribute descriptions +* `binding`: 바인딩 배열에서의 인덱스를 지정합니다. 우리는 하나의 바인딩만 사용하므로 `0`입니다. +* `stride`: 한 정점 데이터에서 다음 정점 데이터까지의 바이트 거리입니다. `std::mem::size_of`를 사용하여 `Vertex` 구조체의 크기를 가져옵니다. +* `input_rate`: 다음 값 중 하나를 가집니다. + * `vk::VertexInputRate::VERTEX`: 각 정점마다 다음 데이터 항목으로 이동합니다. + * `vk::VertexInputRate::INSTANCE`: 각 인스턴스마다 다음 데이터 항목으로 이동합니다. -The second structure that describes how to handle vertex input is -`VkVertexInputAttributeDescription`. We're going to add another helper function -to `Vertex` to fill in these structs. +우리는 인스턴스 렌더링을 사용하지 않으므로, 정점별 데이터(`VERTEX`)를 사용합니다. -```c++ -#include +## 속성 서술 (Attribute descriptions) -... +정점 입력을 처리하는 방법을 서술하는 두 번째 구조체는 `ash::vk::VertexInputAttributeDescription`입니다. 이 구조체들을 채우기 위해 `Vertex`에 또 다른 연관 함수를 추가할 것입니다. -static std::array getAttributeDescriptions() { - std::array attributeDescriptions{}; +필드 오프셋을 안전하게 계산하기 위해 `memoffset` 크레이트가 필요합니다. `Cargo.toml`에 추가해 주세요. - return attributeDescriptions; -} +```toml +[dependencies] +memoffset = "0.9" ``` -As the function prototype indicates, there are going to be two of these -structures. An attribute description struct describes how to extract a vertex -attribute from a chunk of vertex data originating from a binding description. We -have two attributes, position and color, so we need two attribute description -structs. - -```c++ -attributeDescriptions[0].binding = 0; -attributeDescriptions[0].location = 0; -attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; -attributeDescriptions[0].offset = offsetof(Vertex, pos); +```rust +use memoffset::offset_of; + +impl Vertex { + //... get_binding_description() ... + + pub fn get_attribute_descriptions() -> [vk::VertexInputAttributeDescription; 2] { + [ + vk::VertexInputAttributeDescription { + binding: 0, + location: 0, + format: vk::Format::R32G32_SFLOAT, + offset: offset_of!(Vertex, pos) as u32, + }, + vk::VertexInputAttributeDescription { + binding: 0, + location: 1, + format: vk::Format::R32G32B32_SFLOAT, + offset: offset_of!(Vertex, color) as u32, + }, + ] + } +} ``` -The `binding` parameter tells Vulkan from which binding the per-vertex data -comes. The `location` parameter references the `location` directive of the -input in the vertex shader. The input in the vertex shader with location `0` is -the position, which has two 32-bit float components. - -The `format` parameter describes the type of data for the attribute. A bit -confusingly, the formats are specified using the same enumeration as color -formats. The following shader types and formats are commonly used together: - -* `float`: `VK_FORMAT_R32_SFLOAT` -* `vec2`: `VK_FORMAT_R32G32_SFLOAT` -* `vec3`: `VK_FORMAT_R32G32B32_SFLOAT` -* `vec4`: `VK_FORMAT_R32G32B32A32_SFLOAT` - -As you can see, you should use the format where the amount of color channels -matches the number of components in the shader data type. It is allowed to use -more channels than the number of components in the shader, but they will be -silently discarded. If the number of channels is lower than the number of -components, then the BGA components will use default values of `(0, 0, 1)`. The -color type (`SFLOAT`, `UINT`, `SINT`) and bit width should also match the type -of the shader input. See the following examples: - -* `ivec2`: `VK_FORMAT_R32G32_SINT`, a 2-component vector of 32-bit signed -integers -* `uvec4`: `VK_FORMAT_R32G32B32A32_UINT`, a 4-component vector of 32-bit -unsigned integers -* `double`: `VK_FORMAT_R64_SFLOAT`, a double-precision (64-bit) float - -The `format` parameter implicitly defines the byte size of attribute data and -the `offset` parameter specifies the number of bytes since the start of the -per-vertex data to read from. The binding is loading one `Vertex` at a time and -the position attribute (`pos`) is at an offset of `0` bytes from the beginning -of this struct. This is automatically calculated using the `offsetof` macro. - -```c++ -attributeDescriptions[1].binding = 0; -attributeDescriptions[1].location = 1; -attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; -attributeDescriptions[1].offset = offsetof(Vertex, color); -``` +속성 서술(attribute description)은 바인딩에서 제공된 정점 데이터 덩어리로부터 특정 속성을 어떻게 추출할지 설명합니다. 우리는 위치와 색상, 두 가지 속성을 가지고 있으므로 두 개의 속성 서술이 필요합니다. -The color attribute is described in much the same way. +* `binding`: 정점별 데이터가 어느 바인딩에서 오는지를 지정합니다. (`0`) +* `location`: 버텍스 셰이더의 `layout(location = ...)` 지시어에 해당합니다. `location 0`은 위치, `location 1`은 색상입니다. +* `format`: 속성의 데이터 타입을 서술합니다. 포맷은 색상 포맷과 동일한 열거형으로 지정됩니다. + * `Vec2`: `vk::Format::R32G32_SFLOAT` (2 x 32비트 부동소수점) + * `Vec3`: `vk::Format::R32G32B32_SFLOAT` (3 x 32비트 부동소수점) +* `offset`: 정점 데이터의 시작 지점으로부터 해당 속성까지의 바이트 오프셋입니다. `memoffset::offset_of!` 매크로를 사용하여 컴파일 타임에 안전하게 계산합니다. -## Pipeline vertex input +## 파이프라인 정점 입력 -We now need to set up the graphics pipeline to accept vertex data in this format -by referencing the structures in `createGraphicsPipeline`. Find the -`vertexInputInfo` struct and modify it to reference the two descriptions: +이제 `create_graphics_pipeline` 함수에서 이 정보들을 참조하여, 그래픽 파이프라인이 해당 포맷의 정점 데이터를 받도록 설정해야 합니다. `ash`가 제공하는 빌더(builder) 패턴을 사용하면 코드를 더 안전하고 간결하게 작성할 수 있습니다. -```c++ -auto bindingDescription = Vertex::getBindingDescription(); -auto attributeDescriptions = Vertex::getAttributeDescriptions(); +```rust +let binding_descriptions = [Vertex::get_binding_description()]; +let attribute_descriptions = Vertex::get_attribute_descriptions(); -vertexInputInfo.vertexBindingDescriptionCount = 1; -vertexInputInfo.vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()); -vertexInputInfo.pVertexBindingDescriptions = &bindingDescription; -vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data(); +let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::builder() + .vertex_binding_descriptions(&binding_descriptions) + .vertex_attribute_descriptions(&attribute_descriptions); ``` +`ash`의 빌더는 슬라이스(`&[...]`)를 인자로 받으므로, C++ 버전처럼 개수(count)와 포인터(pointer)를 수동으로 설정할 필요가 없습니다. 빌더가 내부적으로 처리해주기 때문에 메모리 안전성이 높고 코드가 깔끔해집니다. 이 `vertex_input_info` 빌더를 파이프라인 생성 정보에 전달하면 됩니다. -The pipeline is now ready to accept vertex data in the format of the `vertices` -container and pass it on to our vertex shader. If you run the program now with -validation layers enabled, you'll see that it complains that there is no vertex -buffer bound to the binding. The next step is to create a vertex buffer and move -the vertex data to it so the GPU is able to access it. +이제 파이프라인은 `VERTICES` 배열과 같은 포맷의 정점 데이터를 받아들여 우리 버텍스 셰이더로 전달할 준비가 되었습니다. 만약 지금 검증 레이어를 활성화한 상태로 프로그램을 실행하면, 바인딩에 연결된 버텍스 버퍼가 없다고 경고하는 것을 볼 수 있을 것입니다. 다음 단계는 버텍스 버퍼를 생성하고 정점 데이터를 그곳으로 옮겨 GPU가 접근할 수 있도록 하는 것입니다. -[C++ code](/code/18_vertex_input.cpp) / -[Vertex shader](/code/18_shader_vertexbuffer.vert) / -[Fragment shader](/code/18_shader_vertexbuffer.frag) +[Rust 코드](/rust_code/src/part18_vertex_input/main.rs) / +[버텍스 셰이더](/code/18_shader_vertexbuffer.vert) / +[프래그먼트 셰이더](/code/18_shader_vertexbuffer.frag) \ No newline at end of file diff --git a/ko-rust/04_Vertex_buffers/01_Vertex_buffer_creation.md b/ko-rust/04_Vertex_buffers/01_Vertex_buffer_creation.md index 77122c50..f31264d4 100644 --- a/ko-rust/04_Vertex_buffers/01_Vertex_buffer_creation.md +++ b/ko-rust/04_Vertex_buffers/01_Vertex_buffer_creation.md @@ -1,342 +1,269 @@ -## Introduction - -Buffers in Vulkan are regions of memory used for storing arbitrary data that can -be read by the graphics card. They can be used to store vertex data, which we'll -do in this chapter, but they can also be used for many other purposes that we'll -explore in future chapters. Unlike the Vulkan objects we've been dealing with so -far, buffers do not automatically allocate memory for themselves. The work from -the previous chapters has shown that the Vulkan API puts the programmer in -control of almost everything and memory management is one of those things. - -## Buffer creation - -Create a new function `createVertexBuffer` and call it from `initVulkan` right -before `createCommandBuffers`. - -```c++ -void initVulkan() { - createInstance(); - setupDebugMessenger(); - createSurface(); - pickPhysicalDevice(); - createLogicalDevice(); - createSwapChain(); - createImageViews(); - createRenderPass(); - createGraphicsPipeline(); - createFramebuffers(); - createCommandPool(); - createVertexBuffer(); - createCommandBuffers(); - createSyncObjects(); +## 소개 + +Vulkan에서 버퍼(Buffer)는 그래픽 카드가 읽을 수 있는 임의의 데이터를 저장하는 데 사용되는 메모리 영역입니다. 이번 장에서 다룰 정점 데이터(vertex data)를 저장하는 데 사용할 수도 있지만, 앞으로의 장에서 살펴볼 다른 많은 목적으로도 사용될 수 있습니다. 지금까지 다뤄온 다른 Vulkan 객체들과는 달리, 버퍼는 스스로 메모리를 할당하지 않습니다. 이전 장들에서 보았듯이 Vulkan API는 프로그래머가 거의 모든 것을 직접 제어하도록 하며, 메모리 관리도 그중 하나입니다. Rust에서도 `ash`와 같은 라이브러리를 사용하더라도 이 원칙은 동일하게 적용됩니다. + +## 버퍼 생성 + +`create_vertex_buffer`라는 새 함수를 만들고, `init_vulkan` 함수에서 `create_command_buffers` 바로 전에 호출하도록 합시다. + +```rust +// In `impl HelloTriangleApplication` +fn init_vulkan(&mut self) -> Result<(), Box> { + self.create_instance()?; + self.setup_debug_messenger()?; + self.create_surface()?; + self.pick_physical_device()?; + self.create_logical_device()?; + self.create_swapchain()?; + self.create_image_views()?; + self.create_render_pass()?; + self.create_graphics_pipeline()?; + self.create_framebuffers()?; + self.create_command_pool()?; + self.create_vertex_buffer()?; // <-- 새로운 호출 + self.create_command_buffers()?; + self.create_sync_objects()?; + Ok(()) } -... - -void createVertexBuffer() { +// ... +fn create_vertex_buffer(&mut self) -> Result<(), Box> { + // 여기에 구현 + Ok(()) } ``` -Creating a buffer requires us to fill a `VkBufferCreateInfo` structure. - -```c++ -VkBufferCreateInfo bufferInfo{}; -bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; -bufferInfo.size = sizeof(vertices[0]) * vertices.size(); -``` +버퍼를 생성하려면 `vk::BufferCreateInfo` 구조체를 채워야 합니다. Rust와 `ash`에서는 빌더(builder) 패턴을 사용하는 것이 일반적이며 훨씬 깔끔합니다. -The first field of the struct is `size`, which specifies the size of the buffer -in bytes. Calculating the byte size of the vertex data is straightforward with -`sizeof`. +```rust +// in create_vertex_buffer +let buffer_size = (std::mem::size_of::() * self.vertices.len()) as vk::DeviceSize; -```c++ -bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; +let buffer_info = vk::BufferCreateInfo::builder() + .size(buffer_size) + .usage(vk::BufferUsageFlags::VERTEX_BUFFER) + .sharing_mode(vk::SharingMode::EXCLUSIVE); ``` -The second field is `usage`, which indicates for which purposes the data in the -buffer is going to be used. It is possible to specify multiple purposes using a -bitwise or. Our use case will be a vertex buffer, we'll look at other types of -usage in future chapters. +`size` 필드는 버퍼의 크기를 바이트 단위로 지정합니다. Rust에서는 `std::mem::size_of`를 사용해 정점 구조체의 크기를 얻고, 이를 벡터의 길이와 곱하여 전체 크기를 계산할 수 있습니다. -```c++ -bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; -``` +`usage` 필드는 버퍼의 데이터가 어떤 목적으로 사용될지를 나타냅니다. 우리의 사용 사례는 정점 버퍼이므로 `vk::BufferUsageFlags::VERTEX_BUFFER`를 사용합니다. -Just like the images in the swap chain, buffers can also be owned by a specific -queue family or be shared between multiple at the same time. The buffer will -only be used from the graphics queue, so we can stick to exclusive access. +`sharing_mode`는 스왑 체인의 이미지처럼 버퍼가 특정 큐 패밀리에 의해 배타적으로 소유될지, 아니면 여러 큐 간에 공유될지를 결정합니다. 우리는 그래픽스 큐에서만 사용할 것이므로 `vk::SharingMode::EXCLUSIVE`으로 설정합니다. -The `flags` parameter is used to configure sparse buffer memory, which is not -relevant right now. We'll leave it at the default value of `0`. +이제 `device.create_buffer`를 사용해 버퍼를 생성할 수 있습니다. 버퍼 핸들과 할당된 메모리를 저장할 구조체 필드를 정의합니다. -We can now create the buffer with `vkCreateBuffer`. Define a class member to -hold the buffer handle and call it `vertexBuffer`. +```rust +// in `HelloTriangleApplication` struct +struct HelloTriangleApplication { + // ... + vertex_buffer: vk::Buffer, + vertex_buffer_memory: vk::DeviceMemory, + // ... +} -```c++ -VkBuffer vertexBuffer; +// in create_vertex_buffer +let vertex_buffer = unsafe { + self.device.create_buffer(&buffer_info, None)? +}; +self.vertex_buffer = vertex_buffer; +``` -... +`ash`의 생성 및 파괴 함수들은 대부분 `unsafe`로 표시되어 있습니다. 이는 개발자가 Vulkan 객체의 생명주기를 올바르게 관리할 책임이 있다는 것을 명시하기 위함입니다. 예를 들어, `device`가 파괴되기 전에 이 버퍼를 반드시 파괴해야 합니다. -void createVertexBuffer() { - VkBufferCreateInfo bufferInfo{}; - bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; - bufferInfo.size = sizeof(vertices[0]) * vertices.size(); - bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; - bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; +버퍼는 프로그램이 끝날 때까지 사용되므로, Rust의 `Drop` 트레잇을 구현하여 정리하는 것이 가장 관용적입니다. - if (vkCreateBuffer(device, &bufferInfo, nullptr, &vertexBuffer) != VK_SUCCESS) { - throw std::runtime_error("failed to create vertex buffer!"); +```rust +// in `impl Drop for HelloTriangleApplication` +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + // ... 다른 객체들 정리 + self.device.destroy_buffer(self.vertex_buffer, None); + // ... + } } } ``` -The buffer should be available for use in rendering commands until the end of -the program and it does not depend on the swap chain, so we'll clean it up in -the original `cleanup` function: - -```c++ -void cleanup() { - cleanupSwapChain(); +## 메모리 요구사항 - vkDestroyBuffer(device, vertexBuffer, nullptr); +버퍼는 생성되었지만 아직 실제 메모리가 할당되지 않았습니다. 버퍼에 메모리를 할당하는 첫 단계는 `get_buffer_memory_requirements` 함수를 사용하여 메모리 요구사항을 쿼리하는 것입니다. - ... -} +```rust +// in create_vertex_buffer, after create_buffer +let mem_requirements = unsafe { + self.device.get_buffer_memory_requirements(self.vertex_buffer) +}; ``` -## Memory requirements - -The buffer has been created, but it doesn't actually have any memory assigned to -it yet. The first step of allocating memory for the buffer is to query its -memory requirements using the aptly named `vkGetBufferMemoryRequirements` -function. +`ash`에서는 이 함수가 `vk::MemoryRequirements` 구조체를 직접 반환합니다. 이 구조체의 필드는 다음과 같습니다. + +* `size`: 필요한 메모리의 크기(바이트 단위). `buffer_info.size`와 다를 수 있습니다. +* `alignment`: 할당된 메모리 영역 내에서 버퍼가 시작되는 오프셋(바이트 단위). +* `memory_type_bits`: 버퍼에 적합한 메모리 타입들의 비트 필드. + +그래픽 카드는 다양한 종류의 메모리를 제공하며, 각각의 특성이 다릅니다. 버퍼의 요구사항과 애플리케이션의 요구사항을 결합하여 올바른 메모리 타입을 찾아야 합니다. 이를 위해 `find_memory_type` 함수를 만들어 봅시다. + +```rust +// In `impl HelloTriangleApplication` +fn find_memory_type(&self, type_filter: u32, properties: vk::MemoryPropertyFlags) -> u32 { + let mem_properties = unsafe { + self.instance.get_physical_device_memory_properties(self.physical_device) + }; + + for i in 0..mem_properties.memory_type_count { + if (type_filter & (1 << i)) != 0 + && mem_properties.memory_types[i as usize] + .property_flags + .contains(properties) + { + return i; + } + } -```c++ -VkMemoryRequirements memRequirements; -vkGetBufferMemoryRequirements(device, vertexBuffer, &memRequirements); + panic!("failed to find suitable memory type!"); +} ``` -The `VkMemoryRequirements` struct has three fields: +이 함수는 C++ 버전과 매우 유사하게 동작합니다. 먼저 `instance.get_physical_device_memory_properties`를 통해 물리 디바이스의 메모리 속성을 가져옵니다. -* `size`: The size of the required amount of memory in bytes, may differ from -`bufferInfo.size`. -* `alignment`: The offset in bytes where the buffer begins in the allocated -region of memory, depends on `bufferInfo.usage` and `bufferInfo.flags`. -* `memoryTypeBits`: Bit field of the memory types that are suitable for the -buffer. +루프 안에서는 `type_filter`를 통해 버퍼가 요구하는 메모리 타입인지 확인하고, `ash`의 비트플래그가 제공하는 `contains` 메서드를 사용해 우리가 필요로 하는 `properties`(예: CPU에서 접근 가능하고 일관성을 유지하는 속성)를 모두 포함하는지 검사합니다. -Graphics cards can offer different types of memory to allocate from. Each type -of memory varies in terms of allowed operations and performance characteristics. -We need to combine the requirements of the buffer and our own application -requirements to find the right type of memory to use. Let's create a new -function `findMemoryType` for this purpose. +## 메모리 할당 -```c++ -uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) { +이제 올바른 메모리 타입을 결정할 수 있으므로, `vk::MemoryAllocateInfo` 구조체를 채워 메모리를 할당할 수 있습니다. -} -``` +```rust +// in create_vertex_buffer +let memory_type_index = self.find_memory_type( + mem_requirements.memory_type_bits, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, +); -First we need to query info about the available types of memory using -`vkGetPhysicalDeviceMemoryProperties`. +let alloc_info = vk::MemoryAllocateInfo::builder() + .allocation_size(mem_requirements.size) + .memory_type_index(memory_type_index); -```c++ -VkPhysicalDeviceMemoryProperties memProperties; -vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties); +let vertex_buffer_memory = unsafe { + self.device.allocate_memory(&alloc_info, None)? +}; +self.vertex_buffer_memory = vertex_buffer_memory; ``` -The `VkPhysicalDeviceMemoryProperties` structure has two arrays `memoryTypes` -and `memoryHeaps`. Memory heaps are distinct memory resources like dedicated -VRAM and swap space in RAM for when VRAM runs out. The different types of memory -exist within these heaps. Right now we'll only concern ourselves with the type -of memory and not the heap it comes from, but you can imagine that this can -affect performance. +`find_memory_type`을 호출하여 적절한 메모리 타입의 인덱스를 찾고, `allocation_size`와 함께 `MemoryAllocateInfo`를 설정합니다. `HOST_VISIBLE` 속성은 CPU가 이 메모리에 접근(map)할 수 있게 하고, `HOST_COHERENT` 속성은 CPU의 쓰기가 별도의 명시적 플러시(flush) 작업 없이 GPU에 보이도록 보장합니다. -Let's first find a memory type that is suitable for the buffer itself: +메모리 할당에 성공했다면, `bind_buffer_memory`를 호출하여 이 메모리를 버퍼와 연결합니다. -```c++ -for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { - if (typeFilter & (1 << i)) { - return i; - } +```rust +// in create_vertex_buffer, after allocate_memory +unsafe { + self.device.bind_buffer_memory(self.vertex_buffer, self.vertex_buffer_memory, 0)?; } - -throw std::runtime_error("failed to find suitable memory type!"); ``` -The `typeFilter` parameter will be used to specify the bit field of memory types -that are suitable. That means that we can find the index of a suitable memory -type by simply iterating over them and checking if the corresponding bit is set -to `1`. - -However, we're not just interested in a memory type that is suitable for the -vertex buffer. We also need to be able to write our vertex data to that memory. -The `memoryTypes` array consists of `VkMemoryType` structs that specify the heap -and properties of each type of memory. The properties define special features -of the memory, like being able to map it so we can write to it from the CPU. -This property is indicated with `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT`, but we -also need to use the `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` property. We'll see -why when we map the memory. - -We can now modify the loop to also check for the support of this property: - -```c++ -for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { - if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { - return i; +오프셋은 `0`으로 설정합니다. 이 메모리는 이 버퍼만을 위해 할당되었기 때문입니다. + +할당된 메모리 역시 `Drop` 트레잇 내에서 해제해야 합니다. 버퍼가 파괴된 후에 메모리를 해제하는 것이 좋습니다. + +```rust +// in `impl Drop for HelloTriangleApplication` +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + // ... + self.device.destroy_buffer(self.vertex_buffer, None); + self.device.free_memory(self.vertex_buffer_memory, None); + // ... + } } } ``` -We may have more than one desirable property, so we should check if the result -of the bitwise AND is not just non-zero, but equal to the desired properties bit -field. If there is a memory type suitable for the buffer that also has all of -the properties we need, then we return its index, otherwise we throw an -exception. - -## Memory allocation +## 정점 버퍼 채우기 -We now have a way to determine the right memory type, so we can actually -allocate the memory by filling in the `VkMemoryAllocateInfo` structure. +이제 정점 데이터를 버퍼에 복사할 시간입니다. `device.map_memory`를 사용하여 버퍼 메모리를 CPU가 접근할 수 있는 메모리 공간에 맵핑합니다. -```c++ -VkMemoryAllocateInfo allocInfo{}; -allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; -allocInfo.allocationSize = memRequirements.size; -allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); +```rust +// in create_vertex_buffer, after bind_buffer_memory +let data_ptr = unsafe { + self.device.map_memory( + self.vertex_buffer_memory, + 0, + buffer_size, + vk::MemoryMapFlags::empty(), + )? +}; ``` -Memory allocation is now as simple as specifying the size and type, both of -which are derived from the memory requirements of the vertex buffer and the -desired property. Create a class member to store the handle to the memory and -allocate it with `vkAllocateMemory`. - -```c++ -VkBuffer vertexBuffer; -VkDeviceMemory vertexBufferMemory; - -... - -if (vkAllocateMemory(device, &allocInfo, nullptr, &vertexBufferMemory) != VK_SUCCESS) { - throw std::runtime_error("failed to allocate vertex buffer memory!"); +이 함수는 맵핑된 메모리 영역을 가리키는 원시 포인터(`*mut std::ffi::c_void`)를 반환합니다. 이제 이 포인터를 통해 데이터를 복사할 수 있습니다. Rust에서는 `std::ptr::copy_nonoverlapping`을 사용합니다. + +```rust +// in create_vertex_buffer, after map_memory +unsafe { + let mut align = ash::util::Align::new( + data_ptr, + std::mem::align_of::() as _, + buffer_size, + ); + align.copy_from_slice(&self.vertices); } ``` -If memory allocation was successful, then we can now associate this memory with -the buffer using `vkBindBufferMemory`: - -```c++ -vkBindBufferMemory(device, vertexBuffer, vertexBufferMemory, 0); -``` - -The first three parameters are self-explanatory and the fourth parameter is the -offset within the region of memory. Since this memory is allocated specifically -for this the vertex buffer, the offset is simply `0`. If the offset is non-zero, -then it is required to be divisible by `memRequirements.alignment`. - -Of course, just like dynamic memory allocation in C++, the memory should be -freed at some point. Memory that is bound to a buffer object may be freed once -the buffer is no longer used, so let's free it after the buffer has been -destroyed: - -```c++ -void cleanup() { - cleanupSwapChain(); - - vkDestroyBuffer(device, vertexBuffer, nullptr); - vkFreeMemory(device, vertexBufferMemory, nullptr); -``` +**참고**: `ash` 0.37.0부터는 `ash::util::Align`이라는 편리한 유틸리티가 제공됩니다. 이 유틸리티는 맵핑된 메모리의 정렬(alignment)을 처리하고 슬라이스에서 데이터를 안전하게 복사하는 작업을 도와줍니다. `memcpy`와 같은 저수준의 포인터 연산을 직접 사용하는 것보다 안전하고 편리합니다. -## Filling the vertex buffer +데이터 복사가 끝나면 `unmap_memory`를 호출하여 맵핑을 해제합니다. -It is now time to copy the vertex data to the buffer. This is done by [mapping -the buffer memory](https://en.wikipedia.org/wiki/Memory-mapped_I/O) into CPU -accessible memory with `vkMapMemory`. - -```c++ -void* data; -vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data); -``` - -This function allows us to access a region of the specified memory resource -defined by an offset and size. The offset and size here are `0` and -`bufferInfo.size`, respectively. It is also possible to specify the special -value `VK_WHOLE_SIZE` to map all of the memory. The second to last parameter can -be used to specify flags, but there aren't any available yet in the current API. -It must be set to the value `0`. The last parameter specifies the output for the -pointer to the mapped memory. - -```c++ -void* data; -vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data); - memcpy(data, vertices.data(), (size_t) bufferInfo.size); -vkUnmapMemory(device, vertexBufferMemory); +```rust +// in create_vertex_buffer, after copying data +unsafe { + self.device.unmap_memory(self.vertex_buffer_memory); +} ``` -You can now simply `memcpy` the vertex data to the mapped memory and unmap it -again using `vkUnmapMemory`. Unfortunately the driver may not immediately copy -the data into the buffer memory, for example because of caching. It is also -possible that writes to the buffer are not visible in the mapped memory yet. -There are two ways to deal with that problem: - -* Use a memory heap that is host coherent, indicated with -`VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` -* Call `vkFlushMappedMemoryRanges` after writing to the mapped memory, and -call `vkInvalidateMappedMemoryRanges` before reading from the mapped memory +우리는 `HOST_COHERENT` 메모리 타입을 사용했기 때문에, 드라이버가 CPU의 쓰기를 자동으로 GPU에 반영합니다. 이는 명시적으로 `flush`를 호출하는 것보다 약간의 성능 저하가 있을 수 있지만, 이 예제에서는 더 간단하고 충분합니다. -We went for the first approach, which ensures that the mapped memory always -matches the contents of the allocated memory. Do keep in mind that this may lead -to slightly worse performance than explicit flushing, but we'll see why that -doesn't matter in the next chapter. +## 정점 버퍼 바인딩 -Flushing memory ranges or using a coherent memory heap means that the driver will be aware of our writes to the buffer, but it doesn't mean that they are actually visible on the GPU yet. The transfer of data to the GPU is an operation that happens in the background and the specification simply [tells us](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-submission-host-writes) that it is guaranteed to be complete as of the next call to `vkQueueSubmit`. +이제 남은 일은 렌더링 과정에서 정점 버퍼를 바인딩하는 것입니다. `record_command_buffer` 함수를 수정합니다. -## Binding the vertex buffer +```rust +// in record_command_buffer +unsafe { + self.device.cmd_bind_pipeline( + command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.graphics_pipeline, + ); -All that remains now is binding the vertex buffer during rendering operations. -We're going to extend the `recordCommandBuffer` function to do that. + let vertex_buffers = [self.vertex_buffer]; + let offsets = [0]; + self.device.cmd_bind_vertex_buffers(command_buffer, 0, &vertex_buffers, &offsets); -```c++ -vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); - -VkBuffer vertexBuffers[] = {vertexBuffer}; -VkDeviceSize offsets[] = {0}; -vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets); - -vkCmdDraw(commandBuffer, static_cast(vertices.size()), 1, 0, 0); + self.device.cmd_draw(command_buffer, self.vertices.len() as u32, 1, 0, 0); +} ``` -The `vkCmdBindVertexBuffers` function is used to bind vertex buffers to -bindings, like the one we set up in the previous chapter. The first two -parameters, besides the command buffer, specify the offset and number of -bindings we're going to specify vertex buffers for. The last two parameters -specify the array of vertex buffers to bind and the byte offsets to start -reading vertex data from. You should also change the call to `vkCmdDraw` to pass -the number of vertices in the buffer as opposed to the hardcoded number `3`. +`cmd_bind_vertex_buffers` 함수는 정점 버퍼를 특정 바인딩 위치에 연결합니다. Rust에서는 배열 슬라이스(`&[...]`)를 사용하여 버퍼와 오프셋을 전달합니다. 또한, `cmd_draw` 호출에서 하드코딩된 정점 수 `3` 대신 `self.vertices.len()`을 사용하여 버퍼에 있는 실제 정점 수를 전달하도록 수정합니다. -Now run the program and you should see the familiar triangle again: +이제 프로그램을 실행하면 익숙한 삼각형이 다시 나타날 것입니다. ![](/images/triangle.png) -Try changing the color of the top vertex to white by modifying the `vertices` -array: +맨 위 정점의 색을 흰색으로 바꾸려면 `vertices` 배열을 수정해 보세요. -```c++ -const std::vector vertices = { - {{0.0f, -0.5f}, {1.0f, 1.0f, 1.0f}}, - {{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}}, - {{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}} -}; +```rust +// in main.rs or where Vertex is defined +const VERTICES: [Vertex; 3] = [ + Vertex { pos: [0.0, -0.5], color: [1.0, 1.0, 1.0] }, + Vertex { pos: [0.5, 0.5], color: [0.0, 1.0, 0.0] }, + Vertex { pos: [-0.5, 0.5], color: [0.0, 0.0, 1.0] }, +]; ``` -Run the program again and you should see the following: +프로그램을 다시 실행하면 다음과 같은 결과를 볼 수 있습니다. ![](/images/triangle_white.png) -In the next chapter we'll look at a different way to copy vertex data to a -vertex buffer that results in better performance, but takes some more work. - -[C++ code](/code/19_vertex_buffer.cpp) / -[Vertex shader](/code/18_shader_vertexbuffer.vert) / -[Fragment shader](/code/18_shader_vertexbuffer.frag) +다음 장에서는 더 나은 성능을 제공하지만 약간의 추가 작업이 필요한, 정점 데이터를 정점 버퍼로 복사하는 다른 방법을 살펴보겠습니다. \ No newline at end of file diff --git a/ko-rust/04_Vertex_buffers/02_Staging_buffer.md b/ko-rust/04_Vertex_buffers/02_Staging_buffer.md index 289e74d4..b96617e7 100644 --- a/ko-rust/04_Vertex_buffers/02_Staging_buffer.md +++ b/ko-rust/04_Vertex_buffers/02_Staging_buffer.md @@ -1,267 +1,294 @@ -## Introduction - -The vertex buffer we have right now works correctly, but the memory type that -allows us to access it from the CPU may not be the most optimal memory type for -the graphics card itself to read from. The most optimal memory has the -`VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT` flag and is usually not accessible by the -CPU on dedicated graphics cards. In this chapter we're going to create two -vertex buffers. One *staging buffer* in CPU accessible memory to upload the data -from the vertex array to, and the final vertex buffer in device local memory. -We'll then use a buffer copy command to move the data from the staging buffer to -the actual vertex buffer. - -## Transfer queue - -The buffer copy command requires a queue family that supports transfer -operations, which is indicated using `VK_QUEUE_TRANSFER_BIT`. The good news is -that any queue family with `VK_QUEUE_GRAPHICS_BIT` or `VK_QUEUE_COMPUTE_BIT` -capabilities already implicitly support `VK_QUEUE_TRANSFER_BIT` operations. The -implementation is not required to explicitly list it in `queueFlags` in those -cases. - -If you like a challenge, then you can still try to use a different queue family -specifically for transfer operations. It will require you to make the following -modifications to your program: - -* Modify `QueueFamilyIndices` and `findQueueFamilies` to explicitly look for a -queue family with the `VK_QUEUE_TRANSFER_BIT` bit, but not the -`VK_QUEUE_GRAPHICS_BIT`. -* Modify `createLogicalDevice` to request a handle to the transfer queue -* Create a second command pool for command buffers that are submitted on the -transfer queue family -* Change the `sharingMode` of resources to be `VK_SHARING_MODE_CONCURRENT` and -specify both the graphics and transfer queue families -* Submit any transfer commands like `vkCmdCopyBuffer` (which we'll be using in -this chapter) to the transfer queue instead of the graphics queue - -It's a bit of work, but it'll teach you a lot about how resources are shared -between queue families. - -## Abstracting buffer creation - -Because we're going to create multiple buffers in this chapter, it's a good idea -to move buffer creation to a helper function. Create a new function -`createBuffer` and move the code in `createVertexBuffer` (except mapping) to it. - -```c++ -void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, VkDeviceMemory& bufferMemory) { - VkBufferCreateInfo bufferInfo{}; - bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; - bufferInfo.size = size; - bufferInfo.usage = usage; - bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - - if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) { - throw std::runtime_error("failed to create buffer!"); - } +## 소개 + +지금 우리가 사용하는 정점 버퍼는 올바르게 작동하지만, CPU에서 접근할 수 있도록 하는 메모리 타입이 그래픽 카드 자체에서 읽기에 가장 최적의 메모리 타입은 아닐 수 있습니다. 가장 최적화된 메모리는 `vk::MemoryPropertyFlags::DEVICE_LOCAL` 플래그를 가지며, 보통 외장 그래픽 카드에서는 CPU가 접근할 수 없습니다. 이번 장에서는 두 개의 정점 버퍼를 만들 것입니다. 하나는 정점 배열의 데이터를 업로드하기 위한 CPU 접근 가능 메모리의 *스테이징 버퍼(staging buffer)*이고, 다른 하나는 디바이스 로컬 메모리에 있는 최종 정점 버퍼입니다. 그런 다음 버퍼 복사 명령을 사용해 스테이징 버퍼의 데이터를 실제 정점 버퍼로 이동시킬 것입니다. + +## 전송 큐 (Transfer queue) + +버퍼 복사 명령은 `vk::QueueFlags::TRANSFER`를 지원하는 큐 패밀리(queue family)를 필요로 합니다. 좋은 소식은 `vk::QueueFlags::GRAPHICS`나 `vk::QueueFlags::COMPUTE` 기능을 가진 모든 큐 패밀리는 이미 암시적으로 `vk::QueueFlags::TRANSFER` 연산을 지원한다는 것입니다. 이런 경우 구현체는 `queue_flags`에 이 비트를 명시적으로 표시하지 않아도 됩니다. + +만약 도전해보고 싶다면, 전송 연산만을 위한 별도의 큐 패밀리를 사용해볼 수도 있습니다. 이를 위해서는 프로그램에 다음과 같은 수정이 필요합니다. + +* `QueueFamilyIndices` 구조체와 `find_queue_families` 함수를 수정하여, `GRAPHICS` 플래그는 없지만 `TRANSFER` 플래그를 가진 큐 패밀리를 명시적으로 찾도록 합니다. +* `create_logical_device`를 수정하여 전송 큐에 대한 핸들을 요청합니다. +* 전송 큐 패밀리에서 제출될 커맨드 버퍼를 위한 두 번째 커맨드 풀(command pool)을 생성합니다. +* 리소스의 `sharing_mode`를 `vk::SharingMode::CONCURRENT`로 변경하고, 큐 패밀리 인덱스 슬라이스(`&[u32]`)에 그래픽 큐와 전송 큐 패밀리를 모두 지정합니다. +* `cmd_copy_buffer`와 같은 모든 전송 명령을 그래픽 큐가 아닌 전송 큐에 제출합니다. + +약간의 작업이 필요하지만, 이를 통해 큐 패밀리 간에 리소스를 어떻게 공유하는지에 대해 많은 것을 배울 수 있을 것입니다. + +## 버퍼 생성 추상화 + +이번 장에서는 여러 버퍼를 생성할 것이므로, 버퍼 생성을 헬퍼 함수로 옮기는 것이 좋습니다. `create_buffer`라는 새 함수를 만들고, `create_vertex_buffer`에 있던 코드(매핑 제외)를 이 함수로 옮기세요. Rust에서는 출력 매개변수 대신 튜플을 반환하는 것이 일반적입니다. - VkMemoryRequirements memRequirements; - vkGetBufferMemoryRequirements(device, buffer, &memRequirements); +```rust +fn create_buffer( + &self, + size: vk::DeviceSize, + usage: vk::BufferUsageFlags, + properties: vk::MemoryPropertyFlags, +) -> (vk::Buffer, vk::DeviceMemory) { + let buffer_info = vk::BufferCreateInfo::builder() + .size(size) + .usage(usage) + .sharing_mode(vk::SharingMode::EXCLUSIVE); - VkMemoryAllocateInfo allocInfo{}; - allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - allocInfo.allocationSize = memRequirements.size; - allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); + let buffer = unsafe { + self.device + .create_buffer(&buffer_info, None) + .expect("failed to create buffer!") + }; - if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) { - throw std::runtime_error("failed to allocate buffer memory!"); + let mem_requirements = unsafe { self.device.get_buffer_memory_requirements(buffer) }; + + let alloc_info = vk::MemoryAllocateInfo::builder() + .allocation_size(mem_requirements.size) + .memory_type_index(self.find_memory_type(mem_requirements.memory_type_bits, properties)); + + let buffer_memory = unsafe { + self.device + .allocate_memory(&alloc_info, None) + .expect("failed to allocate buffer memory!") + }; + + unsafe { + self.device + .bind_buffer_memory(buffer, buffer_memory, 0) + .expect("failed to bind buffer memory!"); } - vkBindBufferMemory(device, buffer, bufferMemory, 0); + (buffer, buffer_memory) } ``` -Make sure to add parameters for the buffer size, memory properties and usage so -that we can use this function to create many different types of buffers. The -last two parameters are output variables to write the handles to. +다양한 종류의 버퍼를 생성하는 데 이 함수를 사용할 수 있도록 버퍼 크기, 메모리 속성, 사용 목적을 매개변수로 받습니다. 함수는 생성된 버퍼와 메모리 핸들을 튜플로 반환합니다. + +이제 `create_vertex_buffer`에서 버퍼 생성 및 메모리 할당 코드를 제거하고, 대신 `create_buffer`를 호출할 수 있습니다. + +```rust +fn create_vertex_buffer(&mut self) { + let buffer_size = (std::mem::size_of::() * self.vertices.len()) as vk::DeviceSize; + + let (vertex_buffer, vertex_buffer_memory) = self.create_buffer( + buffer_size, + vk::BufferUsageFlags::VERTEX_BUFFER, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + ); + self.vertex_buffer = vertex_buffer; + self.vertex_buffer_memory = vertex_buffer_memory; + + let data_ptr = unsafe { + self.device + .map_memory( + self.vertex_buffer_memory, + 0, + buffer_size, + vk::MemoryMapFlags::empty(), + ) + .expect("failed to map vertex buffer memory!") + }; + + unsafe { + let mut align = ash::util::Align::new( + data_ptr, + std::mem::align_of::() as u64, + buffer_size, + ); + align.copy_from_slice(&self.vertices); + self.device.unmap_memory(self.vertex_buffer_memory); + } +} +``` -You can now remove the buffer creation and memory allocation code from -`createVertexBuffer` and just call `createBuffer` instead: +프로그램을 실행하여 정점 버퍼가 여전히 제대로 작동하는지 확인하세요. -```c++ -void createVertexBuffer() { - VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); - createBuffer(bufferSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, vertexBuffer, vertexBufferMemory); +## 스테이징 버퍼 사용하기 - void* data; - vkMapMemory(device, vertexBufferMemory, 0, bufferSize, 0, &data); - memcpy(data, vertices.data(), (size_t) bufferSize); - vkUnmapMemory(device, vertexBufferMemory); -} -``` +이제 `create_vertex_buffer` 함수를 수정하여, 호스트 가시성 버퍼는 임시 버퍼로만 사용하고, 디바이스 로컬 버퍼를 실제 정점 버퍼로 사용하도록 변경하겠습니다. -Run your program to make sure that the vertex buffer still works properly. +```rust +fn create_vertex_buffer(&mut self) { + let buffer_size = (std::mem::size_of::() * self.vertices.len()) as vk::DeviceSize; -## Using a staging buffer + let (staging_buffer, staging_buffer_memory) = self.create_buffer( + buffer_size, + vk::BufferUsageFlags::TRANSFER_SRC, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + ); -We're now going to change `createVertexBuffer` to only use a host visible buffer -as temporary buffer and use a device local one as actual vertex buffer. + unsafe { + let data_ptr = self.device.map_memory( + staging_buffer_memory, + 0, + buffer_size, + vk::MemoryMapFlags::empty(), + ).expect("failed to map staging buffer memory!"); -```c++ -void createVertexBuffer() { - VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); + let mut align = ash::util::Align::new( + data_ptr, + std::mem::align_of::() as u64, + buffer_size, + ); + align.copy_from_slice(&self.vertices); - VkBuffer stagingBuffer; - VkDeviceMemory stagingBufferMemory; - createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); + self.device.unmap_memory(staging_buffer_memory); + } - void* data; - vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data); - memcpy(data, vertices.data(), (size_t) bufferSize); - vkUnmapMemory(device, stagingBufferMemory); + let (vertex_buffer, vertex_buffer_memory) = self.create_buffer( + buffer_size, + vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::VERTEX_BUFFER, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + ); + self.vertex_buffer = vertex_buffer; + self.vertex_buffer_memory = vertex_buffer_memory; - createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory); + // ... 다음 단계에서 복사 로직 추가 ... } ``` -We're now using a new `stagingBuffer` with `stagingBufferMemory` for mapping and -copying the vertex data. In this chapter we're going to use two new buffer usage -flags: +이제 정점 데이터를 매핑하고 복사하기 위해 `staging_buffer`와 `staging_buffer_memory`를 사용합니다. 이번 장에서는 두 개의 새로운 버퍼 사용 플래그를 사용합니다: -* `VK_BUFFER_USAGE_TRANSFER_SRC_BIT`: Buffer can be used as source in a memory -transfer operation. -* `VK_BUFFER_USAGE_TRANSFER_DST_BIT`: Buffer can be used as destination in a -memory transfer operation. +* `vk::BufferUsageFlags::TRANSFER_SRC`: 버퍼가 메모리 전송 연산의 원본(source)으로 사용될 수 있습니다. +* `vk::BufferUsageFlags::TRANSFER_DST`: 버퍼가 메모리 전송 연산의 대상(destination)으로 사용될 수 있습니다. -The `vertexBuffer` is now allocated from a memory type that is device local, -which generally means that we're not able to use `vkMapMemory`. However, we can -copy data from the `stagingBuffer` to the `vertexBuffer`. We have to indicate -that we intend to do that by specifying the transfer source flag for the -`stagingBuffer` and the transfer destination flag for the `vertexBuffer`, along -with the vertex buffer usage flag. +`vertex_buffer`는 이제 디바이스 로컬 메모리 타입으로 할당됩니다. 이는 일반적으로 우리가 `map_memory`를 사용할 수 없다는 것을 의미합니다. 하지만 `staging_buffer`에서 `vertex_buffer`로 데이터를 복사할 수는 있습니다. 이를 위해 `staging_buffer`에는 전송 원본 플래그를, `vertex_buffer`에는 정점 버퍼 사용 플래그와 함께 전송 대상 플래그를 지정해야 합니다. -We're now going to write a function to copy the contents from one buffer to -another, called `copyBuffer`. - -```c++ -void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { +이제 한 버퍼의 내용을 다른 버퍼로 복사하는 `copy_buffer` 함수를 작성하겠습니다. +```rust +fn copy_buffer(&self, src_buffer: vk::Buffer, dst_buffer: vk::Buffer, size: vk::DeviceSize) { + // ... } ``` -Memory transfer operations are executed using command buffers, just like drawing -commands. Therefore we must first allocate a temporary command buffer. You may -wish to create a separate command pool for these kinds of short-lived buffers, -because the implementation may be able to apply memory allocation optimizations. -You should use the `VK_COMMAND_POOL_CREATE_TRANSIENT_BIT` flag during command -pool generation in that case. - -```c++ -void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { - VkCommandBufferAllocateInfo allocInfo{}; - allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocInfo.commandPool = commandPool; - allocInfo.commandBufferCount = 1; - - VkCommandBuffer commandBuffer; - vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); +메모리 전송 연산은 그리기 명령과 마찬가지로 커맨드 버퍼를 사용하여 실행됩니다. 따라서 먼저 임시 커맨드 버퍼를 할당해야 합니다. 이런 종류의 단기 버퍼를 위해 `vk::CommandPoolCreateFlags::TRANSIENT` 플래그를 사용하여 별도의 커맨드 풀을 만드는 것을 고려할 수 있습니다. + +```rust +fn copy_buffer(&self, src_buffer: vk::Buffer, dst_buffer: vk::Buffer, size: vk::DeviceSize) { + let alloc_info = vk::CommandBufferAllocateInfo::builder() + .level(vk::CommandBufferLevel::PRIMARY) + .command_pool(self.command_pool) + .command_buffer_count(1); + + let command_buffer = unsafe { + self.device + .allocate_command_buffers(&alloc_info) + .expect("failed to allocate command buffers!")[0] + }; } ``` -And immediately start recording the command buffer: +그리고 즉시 커맨드 버퍼 기록을 시작합니다. `ash`에서는 빌더 패턴을 사용합니다. -```c++ -VkCommandBufferBeginInfo beginInfo{}; -beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; -beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; +```rust +let begin_info = vk::CommandBufferBeginInfo::builder() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); -vkBeginCommandBuffer(commandBuffer, &beginInfo); +unsafe { + self.device + .begin_command_buffer(command_buffer, &begin_info) + .expect("failed to begin recording command buffer!"); +} ``` -We're only going to use the command buffer once and wait with returning from the function until the copy -operation has finished executing. It's good practice to tell the driver about -our intent using `VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT`. +우리는 이 커맨드 버퍼를 한 번만 사용할 것이므로 `vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT` 플래그를 사용하여 드라이버에 최적화 힌트를 줍니다. + +```rust +let copy_region = vk::BufferCopy::builder() + .src_offset(0) // Optional + .dst_offset(0) // Optional + .size(size) + .build(); -```c++ -VkBufferCopy copyRegion{}; -copyRegion.srcOffset = 0; // Optional -copyRegion.dstOffset = 0; // Optional -copyRegion.size = size; -vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region); +unsafe { + self.device + .cmd_copy_buffer(command_buffer, src_buffer, dst_buffer, &[copy_region]); +} ``` -Contents of buffers are transferred using the `vkCmdCopyBuffer` command. It -takes the source and destination buffers as arguments, and an array of regions -to copy. The regions are defined in `VkBufferCopy` structs and consist of a -source buffer offset, destination buffer offset and size. It is not possible to -specify `VK_WHOLE_SIZE` here, unlike the `vkMapMemory` command. +버퍼 내용은 `cmd_copy_buffer` 명령을 통해 전송됩니다. 이 함수는 원본과 대상 버퍼, 그리고 복사할 영역들을 담은 슬라이스(`&[vk::BufferCopy]`)를 인자로 받습니다. -```c++ -vkEndCommandBuffer(commandBuffer); +```rust +unsafe { + self.device + .end_command_buffer(command_buffer) + .expect("failed to record command buffer!"); +} ``` -This command buffer only contains the copy command, so we can stop recording -right after that. Now execute the command buffer to complete the transfer: +이 커맨드 버퍼는 복사 명령만 포함하므로, 바로 기록을 중단합니다. 이제 커맨드 버퍼를 실행하여 전송을 완료합니다. -```c++ -VkSubmitInfo submitInfo{}; -submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; -submitInfo.commandBufferCount = 1; -submitInfo.pCommandBuffers = &commandBuffer; +```rust +let submit_info = vk::SubmitInfo::builder() + .command_buffers(&[command_buffer]) + .build(); -vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); -vkQueueWaitIdle(graphicsQueue); +unsafe { + self.device + .queue_submit(self.graphics_queue, &[submit_info], vk::Fence::null()) + .expect("failed to submit draw command buffer!"); + self.device + .queue_wait_idle(self.graphics_queue) + .expect("queue wait idle failed!"); +} ``` -Unlike the draw commands, there are no events we need to wait on this time. We -just want to execute the transfer on the buffers immediately. There are again -two possible ways to wait on this transfer to complete. We could use a fence and -wait with `vkWaitForFences`, or simply wait for the transfer queue to become -idle with `vkQueueWaitIdle`. A fence would allow you to schedule multiple -transfers simultaneously and wait for all of them complete, instead of executing -one at a time. That may give the driver more opportunities to optimize. +`queue_wait_idle`을 사용하여 전송이 완료될 때까지 동기적으로 기다립니다. 펜스를 사용하면 여러 전송을 동시에 스케줄링하고 한 번에 기다리는 비동기적인 방식도 가능합니다. -```c++ -vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); +```rust +unsafe { + self.device + .free_command_buffers(self.command_pool, &[command_buffer]); +} ``` -Don't forget to clean up the command buffer used for the transfer operation. +전송 작업에 사용된 커맨드 버퍼를 정리하는 것을 잊지 마세요. -We can now call `copyBuffer` from the `createVertexBuffer` function to move the -vertex data to the device local buffer: +이제 `create_vertex_buffer` 함수에서 `copy_buffer`를 호출하여 정점 데이터를 디바이스 로컬 버퍼로 옮기고, 스테이징 버퍼를 정리합니다. -```c++ -createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory); +```rust +fn create_vertex_buffer(&mut self) { + let buffer_size = (std::mem::size_of::() * self.vertices.len()) as vk::DeviceSize; -copyBuffer(stagingBuffer, vertexBuffer, bufferSize); -``` + let (staging_buffer, staging_buffer_memory) = self.create_buffer( + buffer_size, + vk::BufferUsageFlags::TRANSFER_SRC, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + ); + + // ... (Map, copy, unmap logic from before) ... + unsafe { + let data_ptr = self.device.map_memory(staging_buffer_memory, 0, buffer_size, vk::MemoryMapFlags::empty()).unwrap(); + let mut align = ash::util::Align::new(data_ptr, std::mem::align_of::() as u64, buffer_size); + align.copy_from_slice(&self.vertices); + self.device.unmap_memory(staging_buffer_memory); + } -After copying the data from the staging buffer to the device buffer, we should -clean it up: -```c++ - ... + let (vertex_buffer, vertex_buffer_memory) = self.create_buffer( + buffer_size, + vk::BufferUsageFlags::TRANSFER_DST | vk::BufferUsageFlags::VERTEX_BUFFER, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + ); + self.vertex_buffer = vertex_buffer; + self.vertex_buffer_memory = vertex_buffer_memory; - copyBuffer(stagingBuffer, vertexBuffer, bufferSize); + self.copy_buffer(staging_buffer, self.vertex_buffer, buffer_size); - vkDestroyBuffer(device, stagingBuffer, nullptr); - vkFreeMemory(device, stagingBufferMemory, nullptr); + unsafe { + self.device.destroy_buffer(staging_buffer, None); + self.device.free_memory(staging_buffer_memory, None); + } } ``` -Run your program to verify that you're seeing the familiar triangle again. The -improvement may not be visible right now, but its vertex data is now being -loaded from high performance memory. This will matter when we're going to start -rendering more complex geometry. - -## Conclusion - -It should be noted that in a real world application, you're not supposed to -actually call `vkAllocateMemory` for every individual buffer. The maximum number -of simultaneous memory allocations is limited by the `maxMemoryAllocationCount` -physical device limit, which may be as low as `4096` even on high end hardware -like an NVIDIA GTX 1080. The right way to allocate memory for a large number of -objects at the same time is to create a custom allocator that splits up a single -allocation among many different objects by using the `offset` parameters that -we've seen in many functions. - -You can either implement such an allocator yourself, or use the -[VulkanMemoryAllocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator) -library provided by the GPUOpen initiative. However, for this tutorial it's okay -to use a separate allocation for every resource, because we won't come close to -hitting any of these limits for now. - -[C++ code](/code/20_staging_buffer.cpp) / -[Vertex shader](/code/18_shader_vertexbuffer.vert) / -[Fragment shader](/code/18_shader_vertexbuffer.frag) +프로그램을 실행하여 익숙한 삼각형이 다시 보이는지 확인하세요. 성능 향상이 지금 당장은 눈에 보이지 않을 수 있지만, 이제 정점 데이터는 고성능 메모리에서 로드되고 있습니다. 이는 앞으로 더 복잡한 지오메트리를 렌더링하기 시작할 때 중요해질 것입니다. + +## 결론 + +실제 애플리케이션에서는 모든 개별 버퍼에 대해 `allocate_memory`를 호출해서는 안 된다는 점에 유의해야 합니다. 동시 메모리 할당의 최대 수는 `max_memory_allocation_count` 물리 디바이스 제한에 의해 제한됩니다. 다수의 객체에 대해 동시에 메모리를 할당하는 올바른 방법은, 단일 할당을 여러 객체에 나누어 사용하는 커스텀 할당자를 만드는 것입니다. + +이러한 할당자를 직접 구현하거나, Rust 생태계에서 널리 사용되는 [gpu-allocator](https://github.com/Traverse-Research/gpu-allocator)나 [vk-mem-rs](https://github.com/gwihlidal/vk-mem-rs) 같은 라이브러리를 사용할 수 있습니다. 이들은 C++의 `VulkanMemoryAllocator`에 해당하는 훌륭한 대안입니다. 하지만 이 튜토리얼에서는 지금 당장 이러한 제한에 도달할 일이 없으므로 모든 리소스에 대해 별도의 할당을 사용하는 것이 괜찮습니다. + +[Rust 코드](/code/20_staging_buffer) / +[정점 셰이더](/code/18_shader_vertexbuffer.vert) / +[프래그먼트 셰이더](/code/18_shader_vertexbuffer.frag) \ No newline at end of file diff --git a/ko-rust/04_Vertex_buffers/03_Index_buffer.md b/ko-rust/04_Vertex_buffers/03_Index_buffer.md index 088263db..34faf107 100644 --- a/ko-rust/04_Vertex_buffers/03_Index_buffer.md +++ b/ko-rust/04_Vertex_buffers/03_Index_buffer.md @@ -1,179 +1,175 @@ -## Introduction +## 소개 -The 3D meshes you'll be rendering in a real world application will often share -vertices between multiple triangles. This already happens even with something -simple like drawing a rectangle: +실제 애플리케이션에서 렌더링할 3D 메시는 여러 삼각형 간에 정점을 공유하는 경우가 많습니다. 이는 사각형을 그리는 것처럼 간단한 작업에서도 이미 발생합니다. ![](/images/vertex_vs_index.svg) -Drawing a rectangle takes two triangles, which means that we need a vertex -buffer with 6 vertices. The problem is that the data of two vertices needs to be -duplicated resulting in 50% redundancy. It only gets worse with more complex -meshes, where vertices are reused in an average number of 3 triangles. The -solution to this problem is to use an *index buffer*. - -An index buffer is essentially an array of pointers into the vertex buffer. It -allows you to reorder the vertex data, and reuse existing data for multiple -vertices. The illustration above demonstrates what the index buffer would look -like for the rectangle if we have a vertex buffer containing each of the four -unique vertices. The first three indices define the upper-right triangle and the -last three indices define the vertices for the bottom-left triangle. - -## Index buffer creation - -In this chapter we're going to modify the vertex data and add index data to -draw a rectangle like the one in the illustration. Modify the vertex data to -represent the four corners: - -```c++ -const std::vector vertices = { - {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}}, - {{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}}, - {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}}, - {{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}} -}; -``` - -The top-left corner is red, top-right is green, bottom-right is blue and the -bottom-left is white. We'll add a new array `indices` to represent the contents -of the index buffer. It should match the indices in the illustration to draw the -upper-right triangle and bottom-left triangle. +사각형을 그리려면 두 개의 삼각형이 필요하며, 이는 6개의 정점으로 구성된 정점 버퍼가 필요하다는 것을 의미합니다. 문제는 두 정점의 데이터가 중복되어 50%의 중복이 발생한다는 것입니다. 더 복잡한 메시에서는 정점이 평균 3개의 삼각형에서 재사용되므로 이 문제는 더욱 심각해집니다. 이 문제에 대한 해결책은 *인덱스 버퍼(index buffer)*를 사용하는 것입니다. -```c++ -const std::vector indices = { - 0, 1, 2, 2, 3, 0 -}; -``` +인덱스 버퍼는 본질적으로 정점 버퍼에 대한 포인터 배열입니다. 인덱스 버퍼를 사용하면 정점 데이터의 순서를 바꾸고, 여러 정점에 대해 기존 데이터를 재사용할 수 있습니다. 위 그림은 4개의 고유한 정점을 포함하는 정점 버퍼가 있을 때, 사각형을 위한 인덱스 버퍼가 어떻게 보일지를 보여줍니다. 처음 세 개의 인덱스는 오른쪽 위 삼각형을 정의하고, 마지막 세 개의 인덱스는 왼쪽 아래 삼각형의 정점을 정의합니다. -It is possible to use either `uint16_t` or `uint32_t` for your index buffer -depending on the number of entries in `vertices`. We can stick to `uint16_t` for -now because we're using less than 65535 unique vertices. +## 인덱스 버퍼 생성 -Just like the vertex data, the indices need to be uploaded into a `VkBuffer` for -the GPU to be able to access them. Define two new class members to hold the -resources for the index buffer: +이번 장에서는 정점 데이터를 수정하고 인덱스 데이터를 추가하여 그림과 같은 사각형을 그려보겠습니다. 네 개의 모서리를 나타내도록 정점 데이터를 수정합니다. Rust에서는 상수로 정의하는 것이 일반적입니다. -```c++ -VkBuffer vertexBuffer; -VkDeviceMemory vertexBufferMemory; -VkBuffer indexBuffer; -VkDeviceMemory indexBufferMemory; +```rust +const VERTICES: [Vertex; 4] = [ + Vertex { pos: [-0.5, -0.5], color: [1.0, 0.0, 0.0] }, + Vertex { pos: [0.5, -0.5], color: [0.0, 1.0, 0.0] }, + Vertex { pos: [0.5, 0.5], color: [0.0, 0.0, 1.0] }, + Vertex { pos: [-0.5, 0.5], color: [1.0, 1.0, 1.0] }, +]; ``` -The `createIndexBuffer` function that we'll add now is almost identical to -`createVertexBuffer`: - -```c++ -void initVulkan() { - ... - createVertexBuffer(); - createIndexBuffer(); - ... -} - -void createIndexBuffer() { - VkDeviceSize bufferSize = sizeof(indices[0]) * indices.size(); - - VkBuffer stagingBuffer; - VkDeviceMemory stagingBufferMemory; - createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); +왼쪽 아래 모서리는 빨간색, 오른쪽 아래는 녹색, 오른쪽 위는 파란색, 왼쪽 위는 흰색입니다. `INDICES`라는 새 상수를 추가하여 인덱스 버퍼의 내용을 나타냅니다. 이 배열은 그림의 인덱스와 일치시켜 오른쪽 위 삼각형과 왼쪽 아래 삼각형을 그려야 합니다. - void* data; - vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data); - memcpy(data, indices.data(), (size_t) bufferSize); - vkUnmapMemory(device, stagingBufferMemory); +```rust +const INDICES: [u16; 6] = [0, 1, 2, 2, 3, 0]; +``` - createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, indexBuffer, indexBufferMemory); +`VERTICES` 슬라이스의 길이에 따라 인덱스 버퍼에 `u16` 또는 `u32`를 사용할 수 있습니다. 65535개 미만의 고유 정점을 사용하므로 지금은 `u16`를 사용하겠습니다. - copyBuffer(stagingBuffer, indexBuffer, bufferSize); +정점 데이터와 마찬가지로, 인덱스도 GPU가 접근할 수 있도록 `ash::vk::Buffer`에 업로드해야 합니다. 애플리케이션의 주 구조체에 인덱스 버퍼 리소스를 저장할 두 개의 새로운 필드를 정의합니다. - vkDestroyBuffer(device, stagingBuffer, nullptr); - vkFreeMemory(device, stagingBufferMemory, nullptr); +```rust +struct HelloTriangleApplication { + // ... + vertex_buffer: ash::vk::Buffer, + vertex_buffer_memory: ash::vk::DeviceMemory, + index_buffer: ash::vk::Buffer, + index_buffer_memory: ash::vk::DeviceMemory, + // ... } ``` -There are only two notable differences. The `bufferSize` is now equal to the -number of indices times the size of the index type, either `uint16_t` or -`uint32_t`. The usage of the `indexBuffer` should be -`VK_BUFFER_USAGE_INDEX_BUFFER_BIT` instead of -`VK_BUFFER_USAGE_VERTEX_BUFFER_BIT`, which makes sense. Other than that, the -process is exactly the same. We create a staging buffer to copy the contents of -`indices` to and then copy it to the final device local index buffer. - -The index buffer should be cleaned up at the end of the program, just like the -vertex buffer: - -```c++ -void cleanup() { - cleanupSwapChain(); - - vkDestroyBuffer(device, indexBuffer, nullptr); - vkFreeMemory(device, indexBufferMemory, nullptr); - - vkDestroyBuffer(device, vertexBuffer, nullptr); - vkFreeMemory(device, vertexBufferMemory, nullptr); - - ... +이제 추가할 `create_index_buffer` 함수는 `create_vertex_buffer`와 거의 동일합니다. + +```rust +impl HelloTriangleApplication { + fn init_vulkan(&mut self) -> anyhow::Result<()> { + // ... + self.create_vertex_buffer()?; + self.create_index_buffer()?; + // ... + Ok(()) + } + + fn create_index_buffer(&mut self) -> anyhow::Result<()> { + let buffer_size = (std::mem::size_of::() * INDICES.len()) as ash::vk::DeviceSize; + + let (staging_buffer, staging_buffer_memory) = self.create_buffer( + buffer_size, + ash::vk::BufferUsageFlags::TRANSFER_SRC, + ash::vk::MemoryPropertyFlags::HOST_VISIBLE | ash::vk::MemoryPropertyFlags::HOST_COHERENT, + )?; + + unsafe { + let data_ptr = self.device.map_memory( + staging_buffer_memory, + 0, + buffer_size, + ash::vk::MemoryMapFlags::empty(), + )?; + + // Rust에서는 memcpy 대신 슬라이스를 사용하여 더 안전하게 복사할 수 있습니다. + let mut align = ash::util::Align::new(data_ptr, std::mem::align_of::() as u64, buffer_size); + align.copy_from_slice(&INDICES); + + self.device.unmap_memory(staging_buffer_memory); + } + + let (index_buffer, index_buffer_memory) = self.create_buffer( + buffer_size, + ash::vk::BufferUsageFlags::TRANSFER_DST | ash::vk::BufferUsageFlags::INDEX_BUFFER, + ash::vk::MemoryPropertyFlags::DEVICE_LOCAL, + )?; + self.index_buffer = index_buffer; + self.index_buffer_memory = index_buffer_memory; + + self.copy_buffer(staging_buffer, self.index_buffer, buffer_size)?; + + unsafe { + self.device.destroy_buffer(staging_buffer, None); + self.device.free_memory(staging_buffer_memory, None); + } + + Ok(()) + } } ``` -## Using an index buffer +주목할 만한 차이점은 두 가지뿐입니다. `buffer_size`는 이제 인덱스 수에 인덱스 타입(`u16` 또는 `u32`)의 크기를 곱한 값과 같습니다. `index_buffer`의 `usage`는 `ash::vk::BufferUsageFlags::VERTEX_BUFFER` 대신 `ash::vk::BufferUsageFlags::INDEX_BUFFER`이어야 하는데, 이는 타당한 설정입니다. 그 외의 과정은 `create_buffer`와 `copy_buffer` 헬퍼 함수를 재사용하여 정확히 동일하게 진행됩니다. 스테이징 버퍼를 만들어 `INDICES` 배열의 내용을 복사하고, 그 내용을 최종 장치 로컬 인덱스 버퍼로 복사합니다. -Using an index buffer for drawing involves two changes to -`recordCommandBuffer`. We first need to bind the index buffer, just like we did -for the vertex buffer. The difference is that you can only have a single index -buffer. It's unfortunately not possible to use different indices for each vertex -attribute, so we do still have to completely duplicate vertex data even if just -one attribute varies. +인덱스 버퍼는 정점 버퍼와 마찬가지로 프로그램이 끝날 때 정리해야 합니다. Rust에서는 `Drop` 트레잇을 구현하여 이 작업을 자동으로 처리하는 것이 가장 이상적입니다. -```c++ -vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets); +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + // ... + self.device.destroy_buffer(self.index_buffer, None); + self.device.free_memory(self.index_buffer_memory, None); -vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT16); + self.device.destroy_buffer(self.vertex_buffer, None); + self.device.free_memory(self.vertex_buffer_memory, None); + // ... + } + } +} ``` -An index buffer is bound with `vkCmdBindIndexBuffer` which has the index buffer, -a byte offset into it, and the type of index data as parameters. As mentioned -before, the possible types are `VK_INDEX_TYPE_UINT16` and -`VK_INDEX_TYPE_UINT32`. - -Just binding an index buffer doesn't change anything yet, we also need to change -the drawing command to tell Vulkan to use the index buffer. Remove the -`vkCmdDraw` line and replace it with `vkCmdDrawIndexed`: +## 인덱스 버퍼 사용하기 + +그리기에 인덱스 버퍼를 사용하는 것은 `record_command_buffer`에 두 가지 변경 사항을 수반합니다. 먼저 정점 버퍼에 했던 것처럼 인덱스 버퍼를 바인딩해야 합니다. 차이점은 인덱스 버퍼는 하나만 가질 수 있다는 것입니다. 아쉽게도 각 정점 속성에 대해 서로 다른 인덱스를 사용하는 것은 불가능하므로, 속성 하나만 다르더라도 정점 데이터를 완전히 복제해야 합니다. + +```rust +// In record_command_buffer... +unsafe { + // ... + let vertex_buffers = [self.vertex_buffer]; + let offsets = [0]; + self.device.cmd_bind_vertex_buffers(command_buffer, 0, &vertex_buffers, &offsets); + + self.device.cmd_bind_index_buffer( + command_buffer, + self.index_buffer, + 0, + ash::vk::IndexType::UINT16, + ); + // ... +} +``` -```c++ -vkCmdDrawIndexed(commandBuffer, static_cast(indices.size()), 1, 0, 0, 0); +`cmd_bind_index_buffer`를 사용하여 인덱스 버퍼를 바인딩하며, 이 함수는 커맨드 버퍼, 인덱스 버퍼, 버퍼 내의 바이트 오프셋, 그리고 인덱스 데이터의 타입을 매개변수로 받습니다. `ash`에서는 이 모든 `cmd_` 함수가 `unsafe` 블록 안에서 호출되어야 합니다. 앞서 언급했듯이, 가능한 타입은 `ash::vk::IndexType::UINT16`과 `ash::vk::IndexType::UINT32`입니다. + +인덱스 버퍼를 바인딩하는 것만으로는 아직 아무것도 바뀌지 않으며, Vulkan에 인덱스 버퍼를 사용하도록 지시하기 위해 그리기 명령도 변경해야 합니다. `cmd_draw` 호출을 `cmd_draw_indexed`로 교체합니다. + +```rust +// In record_command_buffer, inside the same unsafe block... +unsafe { + // ... + self.device.cmd_draw_indexed( + command_buffer, + INDICES.len() as u32, // index_count + 1, // instance_count + 0, // first_index + 0, // vertex_offset + 0, // first_instance + ); + // ... +} ``` -A call to this function is very similar to `vkCmdDraw`. The first two parameters -specify the number of indices and the number of instances. We're not using -instancing, so just specify `1` instance. The number of indices represents the -number of vertices that will be passed to the vertex shader. The next parameter -specifies an offset into the index buffer, using a value of `1` would cause the -graphics card to start reading at the second index. The second to last parameter -specifies an offset to add to the indices in the index buffer. The final -parameter specifies an offset for instancing, which we're not using. +이 함수 호출은 `cmd_draw`와 매우 유사합니다. 첫 번째와 두 번째 매개변수는 인덱스의 수와 인스턴스의 수를 지정합니다. 인스턴싱은 사용하지 않으므로 인스턴스는 `1`로 지정합니다. 인덱스의 수(`INDICES.len() as u32`)는 정점 셰이더로 전달될 정점의 수를 나타냅니다. 다음 매개변수는 인덱스 버퍼로의 오프셋을 지정하며, `first_index` 값으로 `1`을 사용하면 그래픽 카드가 두 번째 인덱스부터 읽기 시작합니다. 그 다음 매개변수인 `vertex_offset`은 인덱스 버퍼의 인덱스에 더할 오프셋을 지정합니다. 마지막 매개변수는 인스턴싱을 위한 오프셋을 지정하는데, 우리는 사용하지 않습니다. -Now run your program and you should see the following: +이제 프로그램을 실행하면 다음과 같은 결과가 나타나야 합니다. ![](/images/indexed_rectangle.png) -You now know how to save memory by reusing vertices with index buffers. This -will become especially important in a future chapter where we're going to load -complex 3D models. - -The previous chapter already mentioned that you should allocate multiple -resources like buffers from a single memory allocation, but in fact you should -go a step further. [Driver developers recommend](https://developer.nvidia.com/vulkan-memory-management) -that you also store multiple buffers, like the vertex and index buffer, into a -single `VkBuffer` and use offsets in commands like `vkCmdBindVertexBuffers`. The -advantage is that your data is more cache friendly in that case, because it's -closer together. It is even possible to reuse the same chunk of memory for -multiple resources if they are not used during the same render operations, -provided that their data is refreshed, of course. This is known as *aliasing* -and some Vulkan functions have explicit flags to specify that you want to do -this. - -[C++ code](/code/21_index_buffer.cpp) / -[Vertex shader](/code/18_shader_vertexbuffer.vert) / -[Fragment shader](/code/18_shader_vertexbuffer.frag) +이제 인덱스 버퍼를 사용하여 정점을 재사용함으로써 메모리를 절약하는 방법을 알게 되었습니다. 이는 나중에 복잡한 3D 모델을 로드할 장에서 특히 중요해질 것입니다. + +이전 장에서 이미 여러 리소스(예: 버퍼)를 단일 메모리 할당에서 할당해야 한다고 언급했지만, 사실은 한 단계 더 나아가야 합니다. [드라이버 개발자들은](https://developer.nvidia.com/vulkan-memory-management) 정점 버퍼와 인덱스 버퍼 같은 여러 버퍼를 단일 `ash::vk::Buffer`에 저장하고 `cmd_bind_vertex_buffers`와 같은 명령어에서 오프셋을 사용할 것을 권장합니다. 이렇게 하면 데이터가 더 가깝게 모여있기 때문에 캐시 친화적(cache friendly)이라는 장점이 있습니다. 동일한 렌더링 작업 중에 사용되지 않는 여러 리소스에 대해 동일한 메모리 청크를 재사용하는 것도 가능합니다. 물론 데이터는 새로고침되어야 합니다. 이를 *에일리어싱(aliasing)*이라고 하며, 일부 Vulkan 함수에는 이를 원한다고 명시적으로 지정하는 플래그가 있습니다. + +[Rust 코드](/code/21_index_buffer.rs) / +[정점 셰이더](/code/18_shader_vertexbuffer.vert) / +[프래그먼트 셰이더](/code/18_shader_vertexbuffer.frag) \ No newline at end of file diff --git a/ko-rust/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md b/ko-rust/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md index 2bdcc2dc..5176fe09 100644 --- a/ko-rust/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md +++ b/ko-rust/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md @@ -1,45 +1,28 @@ -## Introduction - -We're now able to pass arbitrary attributes to the vertex shader for each -vertex, but what about global variables? We're going to move on to 3D graphics -from this chapter on and that requires a model-view-projection matrix. We could -include it as vertex data, but that's a waste of memory and it would require us -to update the vertex buffer whenever the transformation changes. The -transformation could easily change every single frame. - -The right way to tackle this in Vulkan is to use *resource descriptors*. A -descriptor is a way for shaders to freely access resources like buffers and -images. We're going to set up a buffer that contains the transformation matrices -and have the vertex shader access them through a descriptor. Usage of -descriptors consists of three parts: - -* Specify a descriptor set layout during pipeline creation -* Allocate a descriptor set from a descriptor pool -* Bind the descriptor set during rendering - -The *descriptor set layout* specifies the types of resources that are going to be -accessed by the pipeline, just like a render pass specifies the types of -attachments that will be accessed. A *descriptor set* specifies the actual -buffer or image resources that will be bound to the descriptors, just like a -framebuffer specifies the actual image views to bind to render pass attachments. -The descriptor set is then bound for the drawing commands just like the vertex -buffers and framebuffer. - -There are many types of descriptors, but in this chapter we'll work with uniform -buffer objects (UBO). We'll look at other types of descriptors in future -chapters, but the basic process is the same. Let's say we have the data we want -the vertex shader to have in a C struct like this: - -```c++ +## 서론 + +이제 우리는 각 정점마다 임의의 어트리뷰트를 버텍스 셰이더로 전달할 수 있게 되었습니다. 하지만 전역 변수는 어떨까요? 이번 장부터는 3D 그래픽스로 넘어가게 되는데, 여기에는 모델-뷰-프로젝션(MVP) 행렬이 필요합니다. 이 행렬을 정점 데이터에 포함시킬 수도 있겠지만, 이는 메모리 낭비이며 변환이 변경될 때마다 정점 버퍼를 업데이트해야 합니다. 변환은 매 프레임마다 쉽게 바뀔 수 있습니다. + +Vulkan에서 이 문제를 해결하는 올바른 방법은 *리소스 디스크립터(resource descriptor)*를 사용하는 것입니다. 디스크립터는 셰이더가 버퍼나 이미지 같은 리소스에 자유롭게 접근할 수 있게 해주는 방법입니다. 우리는 변환 행렬들을 담고 있는 버퍼를 설정하고, 버텍스 셰이더가 디스크립터를 통해 이들에 접근하도록 할 것입니다. 디스크립터 사용은 세 부분으로 구성됩니다: + +* 파이프라인 생성 시 디스크립터 셋 레이아웃 명시 +* 디스크립터 풀에서 디스크립터 셋 할당 +* 렌더링 시 디스크립터 셋 바인딩 + +*디스크립터 셋 레이아웃*은 렌더 패스가 접근할 어태치먼트의 타입을 명시하는 것과 유사하게, 파이프라인이 접근할 리소스의 타입을 명시합니다. *디스크립터 셋*은 프레임버퍼가 렌더 패스 어태치먼트에 바인딩할 실제 이미지 뷰를 지정하는 것과 유사하게, 디스크립터에 바인딩될 실제 버퍼나 이미지 리소스를 지정합니다. 그 후 디스크립터 셋은 정점 버퍼나 프레임버퍼처럼 드로우 커맨드를 위해 바인딩됩니다. + +많은 종류의 디스크립터가 있지만, 이번 장에서는 uniform buffer object (UBO)를 다룰 것입니다. 다른 종류의 디스크립터는 다음 장들에서 살펴보겠지만, 기본적인 과정은 동일합니다. 버텍스 셰이더가 사용하길 원하는 데이터를 다음과 같은 Rust 구조체에 담는다고 가정해 봅시다: + +```rust +#[repr(C)] +#[derive(Copy, Clone, Debug)] struct UniformBufferObject { - glm::mat4 model; - glm::mat4 view; - glm::mat4 proj; -}; + model: Matrix4, + view: Matrix4, + proj: Matrix4, +} ``` -Then we can copy the data to a `VkBuffer` and access it through a uniform buffer -object descriptor from the vertex shader like this: +그러면 우리는 이 데이터를 `vk::Buffer`로 복사하고, 버텍스 셰이더에서 uniform buffer object 디스크립터를 통해 다음과 같이 접근할 수 있습니다: ```glsl layout(binding = 0) uniform UniformBufferObject { @@ -54,15 +37,11 @@ void main() { } ``` -We're going to update the model, view and projection matrices every frame to -make the rectangle from the previous chapter spin around in 3D. +우리는 이전 장의 사각형을 3D 공간에서 회전시키기 위해 매 프레임마다 모델, 뷰, 프로젝션 행렬을 업데이트할 것입니다. -## Vertex shader +## 버텍스 셰이더 -Modify the vertex shader to include the uniform buffer object like it was -specified above. I will assume that you are familiar with MVP transformations. -If you're not, see [the resource](https://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/) -mentioned in the first chapter. +위에서 명시된 것처럼 uniform buffer object를 포함하도록 버텍스 셰이더를 수정하세요. MVP 변환에 대해서는 이미 익숙하다고 가정하겠습니다. 만약 익숙하지 않다면, 첫 장에서 언급된 [참고 자료](https://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/)를 확인하세요. ```glsl #version 450 @@ -84,333 +63,288 @@ void main() { } ``` -Note that the order of the `uniform`, `in` and `out` declarations doesn't -matter. The `binding` directive is similar to the `location` directive for -attributes. We're going to reference this binding in the descriptor set layout. The -line with `gl_Position` is changed to use the transformations to compute the -final position in clip coordinates. Unlike the 2D triangles, the last component -of the clip coordinates may not be `1`, which will result in a division when -converted to the final normalized device coordinates on the screen. This is used -in perspective projection as the *perspective division* and is essential for -making closer objects look larger than objects that are further away. +`uniform`, `in`, `out` 선언 순서는 중요하지 않습니다. `binding` 지시어는 어트리뷰트의 `location` 지시어와 유사합니다. 우리는 디스크립터 셋 레이아웃에서 이 바인딩을 참조할 것입니다. `gl_Position`을 계산하는 줄은 변환 행렬들을 사용하여 최종 클립 좌표(clip coordinates)를 계산하도록 변경되었습니다. 2D 삼각형과 달리, 클립 좌표의 마지막 성분은 `1`이 아닐 수 있으며, 이는 화면의 최종 정규화된 장치 좌표(normalized device coordinates)로 변환될 때 나눗셈을 유발합니다. 이것은 원근 투영에서 *원근 분할(perspective division)*로 사용되며, 가까운 물체가 멀리 있는 물체보다 더 크게 보이게 하는 데 필수적입니다. + +## 디스크립터 셋 레이아웃 -## Descriptor set layout +다음 단계는 Rust 측에서 UBO를 정의하고, 버텍스 셰이더의 이 디스크립터에 대해 Vulkan에 알려주는 것입니다. 행렬 연산을 위해 `cgmath` crate를 사용합니다. -The next step is to define the UBO on the C++ side and to tell Vulkan about this -descriptor in the vertex shader. +```rust +use cgmath::{Matrix4, Point3, Vector3}; -```c++ +#[repr(C)] +#[derive(Copy, Clone, Debug)] struct UniformBufferObject { - glm::mat4 model; - glm::mat4 view; - glm::mat4 proj; -}; + model: Matrix4, + view: Matrix4, + proj: Matrix4, +} ``` -We can exactly match the definition in the shader using data types in GLM. The -data in the matrices is binary compatible with the way the shader expects it, so -we can later just `memcpy` a `UniformBufferObject` to a `VkBuffer`. - -We need to provide details about every descriptor binding used in the shaders -for pipeline creation, just like we had to do for every vertex attribute and its -`location` index. We'll set up a new function to define all of this information -called `createDescriptorSetLayout`. It should be called right before pipeline -creation, because we're going to need it there. - -```c++ -void initVulkan() { - ... - createDescriptorSetLayout(); - createGraphicsPipeline(); - ... -} +`#[repr(C)]` 어트리뷰트는 Rust 컴파일러에게 C와 같은 메모리 레이아웃을 사용하도록 지시하여 셰이더의 기대와 일치하게 만듭니다. `cgmath`의 `Matrix4` 타입은 셰이더의 `mat4`와 바이너리 호환됩니다. -... +모든 디스크립터 바인딩에 대한 세부 정보를 파이프라인 생성 시 제공해야 합니다. 이 정보를 정의하기 위해 `create_descriptor_set_layout` 함수를 설정할 것입니다. 이 함수는 파이프라인 레이아웃이 필요하므로 `init_vulkan` 내에서 파이프라인 생성 직전에 호출되어야 합니다. + +```rust +impl HelloTriangleApplication { + fn init_vulkan(&mut self) { + // ... + self.create_descriptor_set_layout(); + self.create_graphics_pipeline(); + // ... + } -void createDescriptorSetLayout() { + // ... + fn create_descriptor_set_layout(&mut self) { + // ... + } } ``` -Every binding needs to be described through a `VkDescriptorSetLayoutBinding` -struct. +먼저, `HelloTriangleApplication` 구조체에 `descriptor_set_layout` 멤버를 추가합니다. -```c++ -void createDescriptorSetLayout() { - VkDescriptorSetLayoutBinding uboLayoutBinding{}; - uboLayoutBinding.binding = 0; - uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - uboLayoutBinding.descriptorCount = 1; +```rust +struct HelloTriangleApplication { + // ... + pipeline_layout: vk::PipelineLayout, + descriptor_set_layout: vk::DescriptorSetLayout, + // ... } ``` -The first two fields specify the `binding` used in the shader and the type of -descriptor, which is a uniform buffer object. It is possible for the shader -variable to represent an array of uniform buffer objects, and `descriptorCount` -specifies the number of values in the array. This could be used to specify a -transformation for each of the bones in a skeleton for skeletal animation, for -example. Our MVP transformation is in a single uniform buffer object, so we're -using a `descriptorCount` of `1`. +이제 `create_descriptor_set_layout` 함수를 구현합니다. 각 바인딩은 `vk::DescriptorSetLayoutBinding` 구조체로 기술됩니다. `ash`의 빌더 패턴을 사용하면 코드를 더 명확하게 만들 수 있습니다. -```c++ -uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; -``` +```rust +fn create_descriptor_set_layout(&mut self) { + let ubo_layout_binding = vk::DescriptorSetLayoutBinding::builder() + .binding(0) + .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::VERTEX) + .build(); -We also need to specify in which shader stages the descriptor is going to be -referenced. The `stageFlags` field can be a combination of `VkShaderStageFlagBits` values -or the value `VK_SHADER_STAGE_ALL_GRAPHICS`. In our case, we're only referencing -the descriptor from the vertex shader. + let bindings = [ubo_layout_binding]; + let layout_info = vk::DescriptorSetLayoutCreateInfo::builder() + .bindings(&bindings); -```c++ -uboLayoutBinding.pImmutableSamplers = nullptr; // Optional + self.descriptor_set_layout = unsafe { + self.device + .create_descriptor_set_layout(&layout_info, None) + } + .expect("Failed to create descriptor set layout!"); +} ``` -The `pImmutableSamplers` field is only relevant for image sampling related -descriptors, which we'll look at later. You can leave this to its default value. +`binding(0)`은 셰이더의 `layout(binding = 0)`에 해당합니다. `descriptor_type`은 uniform 버퍼임을 명시합니다. `descriptor_count`는 배열의 크기이며, 우리는 단일 UBO만 사용하므로 1입니다. `stage_flags`는 이 디스크립터가 버텍스 셰이더에서 사용됨을 나타냅니다. -All of the descriptor bindings are combined into a single -`VkDescriptorSetLayout` object. Define a new class member above -`pipelineLayout`: +그런 다음, 파이프라인 레이아웃을 생성할 때 이 디스크립터 셋 레이아웃을 참조하도록 `create_pipeline_layout` (혹은 `create_graphics_pipeline` 내의 관련 부분)을 수정해야 합니다. -```c++ -VkDescriptorSetLayout descriptorSetLayout; -VkPipelineLayout pipelineLayout; +```rust +// In create_graphics_pipeline or a create_pipeline_layout helper +let set_layouts = [self.descriptor_set_layout]; +let pipeline_layout_info = vk::PipelineLayoutCreateInfo::builder() + .set_layouts(&set_layouts); +// ... +self.pipeline_layout = unsafe { + self.device.create_pipeline_layout(&pipeline_layout_info, None) +}.expect("Failed to create pipeline layout!"); ``` -We can then create it using `vkCreateDescriptorSetLayout`. This function accepts -a simple `VkDescriptorSetLayoutCreateInfo` with the array of bindings: +`set_layouts` 슬라이스를 통해 하나 이상의 디스크립터 셋 레이아웃을 파이프라인에 바인딩할 수 있습니다. -```c++ -VkDescriptorSetLayoutCreateInfo layoutInfo{}; -layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; -layoutInfo.bindingCount = 1; -layoutInfo.pBindings = &uboLayoutBinding; +마지막으로, 애플리케이션 종료 시 리소스를 정리합니다. -if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) { - throw std::runtime_error("failed to create descriptor set layout!"); +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + // ... + self.device.destroy_descriptor_set_layout(self.descriptor_set_layout, None); + // ... + } + } } ``` -We need to specify the descriptor set layout during pipeline creation to tell -Vulkan which descriptors the shaders will be using. Descriptor set layouts are -specified in the pipeline layout object. Modify the `VkPipelineLayoutCreateInfo` -to reference the layout object: - -```c++ -VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; -pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; -pipelineLayoutInfo.setLayoutCount = 1; -pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout; -``` - -You may be wondering why it's possible to specify multiple descriptor set -layouts here, because a single one already includes all of the bindings. We'll -get back to that in the next chapter, where we'll look into descriptor pools and -descriptor sets. +## Uniform 버퍼 -The descriptor set layout should stick around while we may create new graphics -pipelines i.e. until the program ends: +다음으로, 셰이더를 위한 UBO 데이터를 담을 버퍼를 생성해야 합니다. 매 프레임 UBO를 업데이트할 것이므로 스테이징 버퍼는 불필요한 오버헤드를 유발할 수 있습니다. -```c++ -void cleanup() { - cleanupSwapChain(); +여러 프레임이 동시에 처리 중(in-flight)일 수 있으므로, 이전 프레임이 사용 중인 버퍼를 덮어쓰는 것을 방지하기 위해 각 프레임마다 별도의 uniform 버퍼를 가져야 합니다. `HelloTriangleApplication` 구조체에 새로운 멤버들을 추가합니다. - vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); +```rust +struct HelloTriangleApplication { + // ... + index_buffer: vk::Buffer, + index_buffer_memory: vk::DeviceMemory, - ... + uniform_buffers: Vec, + uniform_buffers_memory: Vec, + uniform_buffers_mapped: Vec<*mut std::ffi::c_void>, + // ... } ``` -## Uniform buffer - -In the next chapter we'll specify the buffer that contains the UBO data for the -shader, but we need to create this buffer first. We're going to copy new data to -the uniform buffer every frame, so it doesn't really make any sense to have a -staging buffer. It would just add extra overhead in this case and likely degrade -performance instead of improving it. - -We should have multiple buffers, because multiple frames may be in flight at the same -time and we don't want to update the buffer in preparation of the next frame while a -previous one is still reading from it! Thus, we need to have as many uniform buffers -as we have frames in flight, and write to a uniform buffer that is not currently -being read by the GPU. - -To that end, add new class members for `uniformBuffers`, and `uniformBuffersMemory`: - -```c++ -VkBuffer indexBuffer; -VkDeviceMemory indexBufferMemory; - -std::vector uniformBuffers; -std::vector uniformBuffersMemory; -std::vector uniformBuffersMapped; -``` - -Similarly, create a new function `createUniformBuffers` that is called after -`createIndexBuffer` and allocates the buffers: - -```c++ -void initVulkan() { - ... - createVertexBuffer(); - createIndexBuffer(); - createUniformBuffers(); - ... -} - -... - -void createUniformBuffers() { - VkDeviceSize bufferSize = sizeof(UniformBufferObject); - - uniformBuffers.resize(MAX_FRAMES_IN_FLIGHT); - uniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT); - uniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT); +`create_uniform_buffers` 함수를 만들어 `create_index_buffer` 다음에 호출되도록 합니다. - for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { - createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, uniformBuffers[i], uniformBuffersMemory[i]); - - vkMapMemory(device, uniformBuffersMemory[i], 0, bufferSize, 0, &uniformBuffersMapped[i]); +```rust +impl HelloTriangleApplication { + fn init_vulkan(&mut self) { + // ... + self.create_vertex_buffer(); + self.create_index_buffer(); + self.create_uniform_buffers(); + // ... + } + + // ... + + fn create_uniform_buffers(&mut self) { + let buffer_size = std::mem::size_of::() as vk::DeviceSize; + + self.uniform_buffers.resize(MAX_FRAMES_IN_FLIGHT, vk::Buffer::null()); + self.uniform_buffers_memory.resize(MAX_FRAMES_IN_FLIGHT, vk::DeviceMemory::null()); + self.uniform_buffers_mapped.resize(MAX_FRAMES_IN_FLIGHT, std::ptr::null_mut()); + + for i in 0..MAX_FRAMES_IN_FLIGHT { + let (buffer, memory) = self.create_buffer( + buffer_size, + vk::BufferUsageFlags::UNIFORM_BUFFER, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + ); + self.uniform_buffers[i] = buffer; + self.uniform_buffers_memory[i] = memory; + + self.uniform_buffers_mapped[i] = unsafe { + self.device.map_memory( + self.uniform_buffers_memory[i], + 0, + buffer_size, + vk::MemoryMapFlags::empty(), + ) + } + .expect("Failed to map uniform buffer memory!"); + } } } ``` - -We map the buffer right after creation using `vkMapMemory` to get a pointer to which we can write the data later on. The buffer stays mapped to this pointer for the application's whole lifetime. This technique is called **"persistent mapping"** and works on all Vulkan implementations. Not having to map the buffer every time we need to update it increases performances, as mapping is not free. - -The uniform data will be used for all draw calls, so the buffer containing it should only be destroyed when we stop rendering. - -```c++ -void cleanup() { - ... - - for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { - vkDestroyBuffer(device, uniformBuffers[i], nullptr); - vkFreeMemory(device, uniformBuffersMemory[i], nullptr); +버퍼를 생성한 직후 `map_memory`를 호출하여 CPU에서 접근 가능한 포인터를 얻습니다. 이 포인터는 애플리케이션 수명 동안 유효하며, 이를 **"영구적 매핑(persistent mapping)"**이라고 합니다. 매번 데이터를 업데이트할 때마다 매핑/언매핑을 반복하지 않아도 되므로 성능에 이점이 있습니다. + +`drop` 함수에서 이 버퍼들과 메모리를 해제합니다. + +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + // ... + for i in 0..MAX_FRAMES_IN_FLIGHT { + self.device.destroy_buffer(self.uniform_buffers[i], None); + self.device.free_memory(self.uniform_buffers_memory[i], None); + } + + self.device.destroy_descriptor_set_layout(self.descriptor_set_layout, None); + // ... + } } - - vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); - - ... - } ``` -## Updating uniform data - -Create a new function `updateUniformBuffer` and add a call to it from the `drawFrame` function before submitting the next frame: - -```c++ -void drawFrame() { - ... - - updateUniformBuffer(currentFrame); - - ... - - VkSubmitInfo submitInfo{}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - - ... -} +## Uniform 데이터 업데이트 -... +`draw_frame` 함수 내에서 커맨드 버퍼를 제출하기 전에 uniform 데이터를 업데이트하는 `update_uniform_buffer` 함수를 호출합니다. -void updateUniformBuffer(uint32_t currentImage) { +```rust +impl HelloTriangleApplication { + fn draw_frame(&mut self) { + // ... + self.update_uniform_buffer(self.current_frame); + // ... + // Submit command buffer + } + fn update_uniform_buffer(&mut self, current_image_index: usize) { + // ... + } } ``` -This function will generate a new transformation every frame to make the -geometry spin around. We need to include two new headers to implement this -functionality: +시간에 따라 회전하는 애니메이션을 구현하기 위해 `cgmath`와 `std::time`을 사용합니다. `Cargo.toml`에 `cgmath`와 `chrono` (또는 표준 라이브러리의 `std::time`)가 포함되어 있는지 확인하세요. -```c++ -#define GLM_FORCE_RADIANS -#include -#include +`HelloTriangleApplication`에 시간을 추적할 필드를 추가합니다. -#include +```rust +struct HelloTriangleApplication { + // ... + start_time: std::time::Instant, + // ... +} ``` -The `glm/gtc/matrix_transform.hpp` header exposes functions that can be used to -generate model transformations like `glm::rotate`, view transformations like -`glm::lookAt` and projection transformations like `glm::perspective`. The -`GLM_FORCE_RADIANS` definition is necessary to make sure that functions like -`glm::rotate` use radians as arguments, to avoid any possible confusion. - -The `chrono` standard library header exposes functions to do precise -timekeeping. We'll use this to make sure that the geometry rotates 90 degrees -per second regardless of frame rate. - -```c++ -void updateUniformBuffer(uint32_t currentImage) { - static auto startTime = std::chrono::high_resolution_clock::now(); - - auto currentTime = std::chrono::high_resolution_clock::now(); - float time = std::chrono::duration(currentTime - startTime).count(); +`new` 함수에서 `start_time`을 초기화합니다. + +```rust +impl HelloTriangleApplication { + pub fn new(window: &winit::window::Window) -> Self { + // ... + let mut app = Self { + // ... + start_time: std::time::Instant::now(), + }; + // ... + } } ``` -The `updateUniformBuffer` function will start out with some logic to calculate -the time in seconds since rendering has started with floating point accuracy. - -We will now define the model, view and projection transformations in the -uniform buffer object. The model rotation will be a simple rotation around the -Z-axis using the `time` variable: +이제 `update_uniform_buffer` 함수를 구현합니다. -```c++ -UniformBufferObject ubo{}; -ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f)); -``` - -The `glm::rotate` function takes an existing transformation, rotation angle and -rotation axis as parameters. The `glm::mat4(1.0f)` constructor returns an -identity matrix. Using a rotation angle of `time * glm::radians(90.0f)` -accomplishes the purpose of rotation 90 degrees per second. +```rust +use cgmath::{Deg, Matrix4, Point3, Rad, Vector3}; -```c++ -ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)); -``` +// ... -For the view transformation I've decided to look at the geometry from above at a -45 degree angle. The `glm::lookAt` function takes the eye position, center -position and up axis as parameters. +fn update_uniform_buffer(&mut self, current_image_index: usize) { + let time = self.start_time.elapsed().as_secs_f32(); -```c++ -ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f); -``` + let model = Matrix4::from_axis_angle( + Vector3::new(0.0, 0.0, 1.0), + Deg(90.0 * time), + ); -I've chosen to use a perspective projection with a 45 degree vertical -field-of-view. The other parameters are the aspect ratio, near and far -view planes. It is important to use the current swap chain extent to calculate -the aspect ratio to take into account the new width and height of the window -after a resize. + let view = Matrix4::look_at_rh( + Point3::new(2.0, 2.0, 2.0), + Point3::new(0.0, 0.0, 0.0), + Vector3::new(0.0, 0.0, 1.0), + ); -```c++ -ubo.proj[1][1] *= -1; -``` + let mut proj = cgmath::perspective( + Deg(45.0), + self.swapchain_extent.width as f32 / self.swapchain_extent.height as f32, + 0.1, + 10.0, + ); -GLM was originally designed for OpenGL, where the Y coordinate of the clip -coordinates is inverted. The easiest way to compensate for that is to flip the -sign on the scaling factor of the Y axis in the projection matrix. If you don't -do this, then the image will be rendered upside down. + // cgmath는 OpenGL의 클립 좌표계를 기준으로 설계되었습니다. + // Vulkan은 Y좌표가 반대이므로, 프로젝션 행렬의 Y 스케일링 요소의 부호를 뒤집어줍니다. + proj[1][1] *= -1.0; -All of the transformations are defined now, so we can copy the data in the -uniform buffer object to the current uniform buffer. This happens in exactly the same -way as we did for vertex buffers, except without a staging buffer. As noted earlier, we only map the uniform buffer once, so we can directly write to it without having to map again: + let ubo = UniformBufferObject { model, view, proj }; -```c++ -memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); + // 매핑된 메모리에 데이터 복사 + unsafe { + let data_ptr = self.uniform_buffers_mapped[current_image_index]; + std::ptr::copy_nonoverlapping(&ubo, data_ptr as *mut UniformBufferObject, 1); + } +} ``` +**설명:** +1. `time`: 애플리케이션 시작 후 경과 시간을 초 단위로 계산합니다. +2. `model`: Z축을 기준으로 초당 90도 회전하는 변환 행렬을 생성합니다. +3. `view`: 45도 각도 위에서 (2, 2, 2) 위치에서 원점을 바라보는 뷰 행렬을 생성합니다. Vulkan은 오른손 좌표계를 사용하므로 `look_at_rh`를 사용합니다. +4. `proj`: 45도 시야각을 가진 원근 투영 행렬을 생성합니다. 창 크기 변경에 대응하기 위해 현재 스왑체인의 종횡비를 사용합니다. +5. `proj[1][1] *= -1.0`: GLM/cgmath는 OpenGL의 클립 공간(Y축이 아래로)을 기준으로 하므로, Vulkan의 클립 공간(Y축이 위로)에 맞추기 위해 Y축을 뒤집어 줍니다. +6. `std::ptr::copy_nonoverlapping`: 계산된 UBO 구조체 데이터를 이전에 매핑해 둔 버퍼 메모리 위치로 복사합니다. 이는 `unsafe` 블록 내에서 수행되어야 합니다. -Using a UBO this way is not the most efficient way to pass frequently changing -values to the shader. A more efficient way to pass a small buffer of data to -shaders are *push constants*. We may look at these in a future chapter. - -In the next chapter we'll look at descriptor sets, which will actually bind the -`VkBuffer`s to the uniform buffer descriptors so that the shader can access this -transformation data. +자주 변경되는 작은 데이터를 셰이더에 전달하는 데 UBO를 사용하는 것이 항상 가장 효율적인 방법은 아닙니다. 더 효율적인 방법으로는 *푸시 상수(push constants)*가 있으며, 이는 추후 튜토리얼에서 다룰 수 있습니다. -[C++ code](/code/22_descriptor_set_layout.cpp) / -[Vertex shader](/code/22_shader_ubo.vert) / -[Fragment shader](/code/22_shader_ubo.frag) +다음 장에서는 디스크립터 셋에 대해 알아보고, 우리가 생성한 `vk::Buffer`를 uniform 버퍼 디스크립터에 실제로 바인딩하여 셰이더가 이 변환 데이터에 접근할 수 있도록 하는 방법을 배울 것입니다. \ No newline at end of file diff --git a/ko-rust/05_Uniform_buffers/01_Descriptor_pool_and_sets.md b/ko-rust/05_Uniform_buffers/01_Descriptor_pool_and_sets.md index b204db24..632d992b 100644 --- a/ko-rust/05_Uniform_buffers/01_Descriptor_pool_and_sets.md +++ b/ko-rust/05_Uniform_buffers/01_Descriptor_pool_and_sets.md @@ -1,267 +1,227 @@ -## Introduction +## 소개 -The descriptor set layout from the previous chapter describes the type of -descriptors that can be bound. In this chapter we're going to create -a descriptor set for each `VkBuffer` resource to bind it to the -uniform buffer descriptor. +이전 장에서 다룬 디스크립터 셋 레이아웃은 바인딩할 수 있는 디스크립터의 유형을 설명합니다. 이번 장에서는 각 `vk::Buffer` 리소스마다 디스크립터 셋을 만들어서 유니폼 버퍼 디스크립터에 바인딩할 것입니다. -## Descriptor pool +## 디스크립터 풀 (Descriptor Pool) -Descriptor sets can't be created directly, they must be allocated from a pool -like command buffers. The equivalent for descriptor sets is unsurprisingly -called a *descriptor pool*. We'll write a new function `createDescriptorPool` -to set it up. +디스크립터 셋은 직접 생성할 수 없으며, 커맨드 버퍼처럼 풀(pool)에서 할당해야 합니다. 디스크립터 셋을 위한 이러한 풀은 *디스크립터 풀(descriptor pool)*이라고 불립니다. 이를 설정하기 위해 새로운 함수 `create_descriptor_pool`을 작성하겠습니다. -```c++ -void initVulkan() { - ... - createUniformBuffers(); - createDescriptorPool(); - ... -} - -... +```rust +impl VulkanApp { + fn init_vulkan(&mut self) -> Result<()> { + ... + self.create_uniform_buffers()?; + self.create_descriptor_pool()?; + ... + } -void createDescriptorPool() { + ... + fn create_descriptor_pool(&mut self) -> Result<()> { + // 이 함수 내용을 채워나갑니다. + Ok(()) + } } ``` -We first need to describe which descriptor types our descriptor sets are going -to contain and how many of them, using `VkDescriptorPoolSize` structures. - -```c++ -VkDescriptorPoolSize poolSize{}; -poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; -poolSize.descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); -``` +먼저 `vk::DescriptorPoolSize` 구조체를 사용해 우리 디스크립터 셋이 어떤 유형의 디스크립터를 얼마나 포함할지 기술해야 합니다. -We will allocate one of these descriptors for every frame. This -pool size structure is referenced by the main `VkDescriptorPoolCreateInfo`: - -```c++ -VkDescriptorPoolCreateInfo poolInfo{}; -poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; -poolInfo.poolSizeCount = 1; -poolInfo.pPoolSizes = &poolSize; +```rust +let pool_size = vk::DescriptorPoolSize { + ty: vk::DescriptorType::UNIFORM_BUFFER, + descriptor_count: MAX_FRAMES_IN_FLIGHT as u32, +}; ``` -Aside from the maximum number of individual descriptors that are available, we -also need to specify the maximum number of descriptor sets that may be -allocated: +우리는 프레임마다 하나씩 이 디스크립터를 할당할 것입니다. 이 풀 크기 구조체는 메인 `vk::DescriptorPoolCreateInfo`에서 참조됩니다. `ash`의 빌더 패턴을 사용하면 코드가 더 깔끔해집니다. -```c++ -poolInfo.maxSets = static_cast(MAX_FRAMES_IN_FLIGHT); +```rust +let pool_sizes = [pool_size]; +let pool_info = vk::DescriptorPoolCreateInfo::builder() + .pool_sizes(&pool_sizes) + // ... ``` -The structure has an optional flag similar to command pools that determines if -individual descriptor sets can be freed or not: -`VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT`. We're not going to touch -the descriptor set after creating it, so we don't need this flag. You can leave -`flags` to its default value of `0`. +개별 디스크립터의 최대 개수 외에도, 할당될 수 있는 디스크립터 셋의 최대 개수도 지정해야 합니다. -```c++ -VkDescriptorPool descriptorPool; - -... - -if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) { - throw std::runtime_error("failed to create descriptor pool!"); -} +```rust + .max_sets(MAX_FRAMES_IN_FLIGHT as u32); ``` -Add a new class member to store the handle of the descriptor pool and call -`vkCreateDescriptorPool` to create it. - -## Descriptor set +이 구조체는 커맨드 풀과 유사한 선택적 플래그 `vk::DescriptorPoolCreateFlags::FREE_DESCRIPTOR_SET`를 가집니다. 이 플래그는 개별 디스크립터 셋을 해제할 수 있는지 여부를 결정합니다. 우리는 디스크립터 셋을 생성한 후에는 수정하지 않을 것이므로 이 플래그는 필요 없습니다. `flags`는 기본값으로 둘 수 있습니다. -We can now allocate the descriptor sets themselves. Add a `createDescriptorSets` -function for that purpose: +애플리케이션 구조체에 디스크립터 풀 핸들을 저장할 필드를 추가하고 `create_descriptor_pool`을 호출하여 생성합니다. -```c++ -void initVulkan() { +```rust +// 구조체 정의 +struct VulkanApp { ... - createDescriptorPool(); - createDescriptorSets(); + descriptor_pool: vk::DescriptorPool, ... } -... - -void createDescriptorSets() { - -} -``` +// create_descriptor_pool 함수 내부 +let pool_info = vk::DescriptorPoolCreateInfo::builder() + .pool_sizes(&pool_sizes) + .max_sets(MAX_FRAMES_IN_FLIGHT as u32); -A descriptor set allocation is described with a `VkDescriptorSetAllocateInfo` -struct. You need to specify the descriptor pool to allocate from, the number of -descriptor sets to allocate, and the descriptor set layout to base them on: - -```c++ -std::vector layouts(MAX_FRAMES_IN_FLIGHT, descriptorSetLayout); -VkDescriptorSetAllocateInfo allocInfo{}; -allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; -allocInfo.descriptorPool = descriptorPool; -allocInfo.descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT); -allocInfo.pSetLayouts = layouts.data(); +self.descriptor_pool = unsafe { + self.device + .create_descriptor_pool(&pool_info, None) +}?; ``` -In our case we will create one descriptor set for each frame in flight, all with the same layout. -Unfortunately we do need all the copies of the layout because the next function expects an array matching the number of sets. +## 디스크립터 셋 (Descriptor Set) -Add a class member to hold the descriptor set handles and allocate them with -`vkAllocateDescriptorSets`: - -```c++ -VkDescriptorPool descriptorPool; -std::vector descriptorSets; +이제 디스크립터 셋 자체를 할당할 수 있습니다. 이를 위해 `create_descriptor_sets` 함수를 추가합시다. +```rust +// init_vulkan 함수 내부 +... +self.create_descriptor_pool()?; +self.create_descriptor_sets()?; ... -descriptorSets.resize(MAX_FRAMES_IN_FLIGHT); -if (vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()) != VK_SUCCESS) { - throw std::runtime_error("failed to allocate descriptor sets!"); +// VulkanApp impl 블록 내부 +fn create_descriptor_sets(&mut self) -> Result<()> { + // 이 함수 내용을 채워나갑니다. + Ok(()) } ``` -You don't need to explicitly clean up descriptor sets, because they will be -automatically freed when the descriptor pool is destroyed. The call to -`vkAllocateDescriptorSets` will allocate descriptor sets, each with one uniform -buffer descriptor. +`vk::DescriptorSetAllocateInfo` 구조체로 디스크립터 셋 할당을 기술합니다. 할당할 디스크립터 풀, 할당할 디스크립터 셋의 개수, 그리고 기반으로 할 디스크립터 셋 레이아웃을 지정해야 합니다. -```c++ -void cleanup() { - ... - vkDestroyDescriptorPool(device, descriptorPool, nullptr); - - vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); - ... -} +```rust +// create_descriptor_sets 함수 내부 +let layouts = vec![self.descriptor_set_layout; MAX_FRAMES_IN_FLIGHT]; +let alloc_info = vk::DescriptorSetAllocateInfo::builder() + .descriptor_pool(self.descriptor_pool) + .set_layouts(&layouts); ``` -The descriptor sets have been allocated now, but the descriptors within still need -to be configured. We'll now add a loop to populate every descriptor: +우리의 경우, 각 프레임마다 하나의 디스크립터 셋을 생성하며, 모두 동일한 레이아웃을 가집니다. `ash`를 사용하면 `allocate_descriptor_sets` 함수가 `Vec`을 반환하므로 미리 벡터 크기를 조절할 필요가 없습니다. -```c++ -for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { +구조체에 디스크립터 셋 핸들을 담을 필드를 추가하고 `allocate_descriptor_sets`로 할당합니다. +```rust +// 구조체 정의 +struct VulkanApp { + ... + descriptor_sets: Vec, + ... } -``` -Descriptors that refer to buffers, like our uniform buffer -descriptor, are configured with a `VkDescriptorBufferInfo` struct. This -structure specifies the buffer and the region within it that contains the data -for the descriptor. - -```c++ -for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { - VkDescriptorBufferInfo bufferInfo{}; - bufferInfo.buffer = uniformBuffers[i]; - bufferInfo.offset = 0; - bufferInfo.range = sizeof(UniformBufferObject); -} +// create_descriptor_sets 함수 내부 +self.descriptor_sets = unsafe { self.device.allocate_descriptor_sets(&alloc_info) }?; ``` -If you're overwriting the whole buffer, like we are in this case, then it is also possible to use the `VK_WHOLE_SIZE` value for the range. The configuration of descriptors is updated using the `vkUpdateDescriptorSets` -function, which takes an array of `VkWriteDescriptorSet` structs as parameter. +디스크립터 풀이 파괴될 때 자동으로 해제되므로, 디스크립터 셋을 명시적으로 정리할 필요는 없습니다. `allocate_descriptor_sets` 호출은 각각 하나의 유니폼 버퍼 디스크립터를 가진 디스크립터 셋들을 할당할 것입니다. `cleanup` (또는 `Drop` 구현)에서는 디스크립터 풀만 파괴하면 됩니다. -```c++ -VkWriteDescriptorSet descriptorWrite{}; -descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; -descriptorWrite.dstSet = descriptorSets[i]; -descriptorWrite.dstBinding = 0; -descriptorWrite.dstArrayElement = 0; +```rust +// cleanup 함수 또는 Drop 트레이트 구현 내부 +... +unsafe { + self.device.destroy_descriptor_pool(self.descriptor_pool, None); + self.device.destroy_descriptor_set_layout(self.descriptor_set_layout, None); +} +... ``` -The first two fields specify the descriptor set to update and the binding. We -gave our uniform buffer binding index `0`. Remember that descriptors can be -arrays, so we also need to specify the first index in the array that we want to -update. We're not using an array, so the index is simply `0`. +이제 디스크립터 셋은 할당되었지만, 그 안의 디스크립터들은 아직 설정이 필요합니다. 이제 모든 디스크립터를 채우기 위한 루프를 추가합니다. -```c++ -descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; -descriptorWrite.descriptorCount = 1; +우리의 유니폼 버퍼 디스크립터처럼 버퍼를 참조하는 디스크립터는 `vk::DescriptorBufferInfo` 구조체로 설정합니다. 이 구조체는 버퍼와 디스크립터 데이터를 포함하는 버퍼 내의 영역을 지정합니다. + +```rust +// create_descriptor_sets 함수 내부, 할당 후 +for i in 0..MAX_FRAMES_IN_FLIGHT { + let buffer_info = vk::DescriptorBufferInfo { + buffer: self.uniform_buffers[i], + offset: 0, + range: std::mem::size_of::() as vk::DeviceSize, + }; ``` -We need to specify the type of descriptor again. It's possible to update -multiple descriptors at once in an array, starting at index `dstArrayElement`. -The `descriptorCount` field specifies how many array elements you want to -update. +우리처럼 버퍼 전체를 덮어쓰는 경우, `range`에 `vk::WHOLE_SIZE` 값을 사용하는 것도 가능합니다. 디스크립터 설정은 `vk::WriteDescriptorSet` 구조체의 배열을 파라미터로 받는 `update_descriptor_sets` 함수를 사용하여 업데이트됩니다. `ash`의 빌더 패턴을 사용합시다. -```c++ -descriptorWrite.pBufferInfo = &bufferInfo; -descriptorWrite.pImageInfo = nullptr; // Optional -descriptorWrite.pTexelBufferView = nullptr; // Optional +```rust + let buffer_infos = [buffer_info]; + let descriptor_write = vk::WriteDescriptorSet::builder() + .dst_set(self.descriptor_sets[i]) + .dst_binding(0) + .dst_array_element(0) + .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) + .buffer_info(&buffer_infos); ``` -The last field references an array with `descriptorCount` structs that actually -configure the descriptors. It depends on the type of descriptor which one of the -three you actually need to use. The `pBufferInfo` field is used for descriptors -that refer to buffer data, `pImageInfo` is used for descriptors that refer to -image data, and `pTexelBufferView` is used for descriptors that refer to buffer -views. Our descriptor is based on buffers, so we're using `pBufferInfo`. +`ash`의 `update_descriptor_sets` 함수는 쓰기(write)와 복사(copy)를 위한 슬라이스를 받습니다. 우리는 쓰기만 할 것입니다. -```c++ -vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr); +```rust + unsafe { + self.device.update_descriptor_sets(&[descriptor_write.build()], &[]); + } +} ``` -The updates are applied using `vkUpdateDescriptorSets`. It accepts two kinds of -arrays as parameters: an array of `VkWriteDescriptorSet` and an array of -`VkCopyDescriptorSet`. The latter can be used to copy descriptors to each other, -as its name implies. - -## Using descriptor sets +`update_descriptor_sets`는 두 종류의 배열 슬라이스를 파라미터로 받습니다: `&[vk::WriteDescriptorSet]`와 `&[vk::CopyDescriptorSet]`입니다. 후자는 이름에서 알 수 있듯이 디스크립터를 서로 복사하는 데 사용할 수 있습니다. + +## 디스크립터 셋 사용하기 + +이제 `record_command_buffer` 함수를 업데이트하여 `cmd_draw_indexed` 호출 전에 `cmd_bind_descriptor_sets`로 셰이더의 디스크립터에 각 프레임에 맞는 디스크립터 셋을 실제로 바인딩해야 합니다. + +```rust +// record_command_buffer 함수 내부 +unsafe { + self.device.cmd_bind_descriptor_sets( + command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.pipeline_layout, + 0, + &[self.descriptor_sets[self.current_frame]], + &[], + ); + self.device.cmd_draw_indexed( + command_buffer, + self.indices.len() as u32, + 1, + 0, + 0, + 0, + ); +} +``` -We now need to update the `recordCommandBuffer` function to actually bind the -right descriptor set for each frame to the descriptors in the shader with `vkCmdBindDescriptorSets`. This needs to be done before the `vkCmdDrawIndexed` call: +정점 및 인덱스 버퍼와 달리, 디스크립터 셋은 그래픽스 파이프라인에만 국한되지 않습니다. 따라서 디스크립터 셋을 그래픽스 파이프라인에 바인딩할지, 컴퓨트 파이프라인에 바인딩할지 지정해야 합니다. 다음 파라미터는 디스크립터가 기반으로 하는 레이아웃입니다. 그 다음 세 파라미터는 첫 번째 디스크립터 셋의 인덱스, 바인딩할 셋의 개수, 그리고 바인딩할 셋의 슬라이스를 지정합니다. 마지막 파라미터는 동적 디스크립터에 사용되는 오프셋 슬라이스이며, 이는 다음 장에서 살펴보겠습니다. -```c++ -vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSets[currentFrame], 0, nullptr); -vkCmdDrawIndexed(commandBuffer, static_cast(indices.size()), 1, 0, 0, 0); -``` +지금 프로그램을 실행해보면 안타깝게도 아무것도 보이지 않을 수 있습니다. 문제는 투영 행렬에서 Y축을 뒤집었기 때문에, 정점들이 시계 방향 대신 반시계 방향으로 그려진다는 것입니다. 이로 인해 후면 컬링(backface culling)이 작동하여 지오메트리가 그려지지 않게 됩니다. `create_graphics_pipeline` 함수로 가서 래스터화 상태의 `front_face`를 수정하여 이를 바로잡습니다. -Unlike vertex and index buffers, descriptor sets are not unique to graphics -pipelines. Therefore we need to specify if we want to bind descriptor sets to -the graphics or compute pipeline. The next parameter is the layout that the -descriptors are based on. The next three parameters specify the index of the -first descriptor set, the number of sets to bind, and the array of sets to bind. -We'll get back to this in a moment. The last two parameters specify an array of -offsets that are used for dynamic descriptors. We'll look at these in a future -chapter. - -If you run your program now, then you'll notice that unfortunately nothing is -visible. The problem is that because of the Y-flip we did in the projection -matrix, the vertices are now being drawn in counter-clockwise order instead of -clockwise order. This causes backface culling to kick in and prevents -any geometry from being drawn. Go to the `createGraphicsPipeline` function and -modify the `frontFace` in `VkPipelineRasterizationStateCreateInfo` to correct -this: - -```c++ -rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; -rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; +```rust +// create_graphics_pipeline 함수 내부 +let rasterizer = vk::PipelineRasterizationStateCreateInfo::builder() + ... + .cull_mode(vk::CullModeFlags::BACK) + .front_face(vk::FrontFace::COUNTER_CLOCKWISE); ``` -Run your program again and you should now see the following: +프로그램을 다시 실행하면 다음과 같은 화면을 볼 수 있습니다. ![](/images/spinning_quad.png) -The rectangle has changed into a square because the projection matrix now -corrects for aspect ratio. The `updateUniformBuffer` takes care of screen -resizing, so we don't need to recreate the descriptor set in -`recreateSwapChain`. +투영 행렬이 이제 화면 비율을 보정하기 때문에 직사각형이 정사각형으로 변경되었습니다. `update_uniform_buffer`가 화면 크기 조정을 처리하므로 `recreate_swapchain`에서 디스크립터 셋을 다시 만들 필요는 없습니다. -## Alignment requirements +## 정렬 요구사항 (Alignment Requirements) -One thing we've glossed over so far is how exactly the data in the C++ structure should match with the uniform definition in the shader. It seems obvious enough to simply use the same types in both: +지금까지 간과한 한 가지는 Rust 구조체의 데이터가 셰이더의 유니폼 정의와 정확히 어떻게 일치해야 하는가입니다. 예를 들어, 수학 라이브러리로 `glam`을 사용한다고 가정합시다. -```c++ +```rust +// #[repr(C)]는 필드 순서를 보장합니다. +#[repr(C)] struct UniformBufferObject { - glm::mat4 model; - glm::mat4 view; - glm::mat4 proj; -}; + model: glam::Mat4, + view: glam::Mat4, + proj: glam::Mat4, +} +// 셰이더 layout(binding = 0) uniform UniformBufferObject { mat4 model; mat4 view; @@ -269,123 +229,107 @@ layout(binding = 0) uniform UniformBufferObject { } ubo; ``` -However, that's not all there is to it. For example, try modifying the struct and shader to look like this: +하지만 이게 전부가 아닙니다. 예를 들어, 구조체와 셰이더를 다음과 같이 수정해보세요. -```c++ +```rust +#[repr(C)] struct UniformBufferObject { - glm::vec2 foo; - glm::mat4 model; - glm::mat4 view; - glm::mat4 proj; -}; - -layout(binding = 0) uniform UniformBufferObject { - vec2 foo; - mat4 model; - mat4 view; - mat4 proj; -} ubo; + foo: glam::Vec2, + model: glam::Mat4, + view: glam::Mat4, + proj: glam::Mat4, +} ``` -Recompile your shader and your program and run it and you'll find that the colorful square you worked so far has disappeared! That's because we haven't taken into account the *alignment requirements*. - -Vulkan expects the data in your structure to be aligned in memory in a specific way, for example: - -* Scalars have to be aligned by N (= 4 bytes given 32 bit floats). -* A `vec2` must be aligned by 2N (= 8 bytes) -* A `vec3` or `vec4` must be aligned by 4N (= 16 bytes) -* A nested structure must be aligned by the base alignment of its members rounded up to a multiple of 16. -* A `mat4` matrix must have the same alignment as a `vec4`. +셰이더와 프로그램을 다시 컴파일하고 실행하면, 다채로운 사각형이 사라질 것입니다! 이는 우리가 *정렬 요구사항(alignment requirements)*을 고려하지 않았기 때문입니다. -You can find the full list of alignment requirements in [the specification](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap15.html#interfaces-resources-layout). +Vulkan은 구조체의 데이터가 메모리에서 특정 방식으로 정렬되기를 기대합니다. 예를 들면 다음과 같습니다: -Our original shader with just three `mat4` fields already met the alignment requirements. As each `mat4` is 4 x 4 x 4 = 64 bytes in size, `model` has an offset of `0`, `view` has an offset of 64 and `proj` has an offset of 128. All of these are multiples of 16 and that's why it worked fine. +* 스칼라는 N(32비트 부동소수점의 경우 4바이트)으로 정렬되어야 합니다. +* `vec2`는 2N(8바이트)으로 정렬되어야 합니다. +* `vec3` 또는 `vec4`는 4N(16바이트)으로 정렬되어야 합니다. +* 중첩 구조체는 멤버의 기본 정렬을 16의 배수로 올림한 값으로 정렬되어야 합니다. +* `mat4` 행렬은 `vec4`와 동일한 정렬을 가져야 합니다. -The new structure starts with a `vec2` which is only 8 bytes in size and therefore throws off all of the offsets. Now `model` has an offset of `8`, `view` an offset of `72` and `proj` an offset of `136`, none of which are multiples of 16. To fix this problem we can use the [`alignas`](https://en.cppreference.com/w/cpp/language/alignas) specifier introduced in C++11: +`glam::Mat4`는 이미 16바이트 정렬이 되어 있어 원래 구조체는 문제가 없었습니다. 그러나 `glam::Vec2`는 8바이트 정렬을 가지므로, `model` 필드의 오프셋이 8이 되어 16의 배수가 아니게 됩니다. 이 문제를 해결하기 위해 Rust에서는 `#[repr(C, align(N))]` 속성을 사용할 수 있습니다. -```c++ +```rust +#[repr(C)] struct UniformBufferObject { - glm::vec2 foo; - alignas(16) glm::mat4 model; - glm::mat4 view; - glm::mat4 proj; -}; + foo: glam::Vec2, + #[repr(align(16))] + model: glam::Mat4, + view: glam::Mat4, + proj: glam::Mat4, +} ``` -If you now compile and run your program again you should see that the shader correctly receives its matrix values once again. +하지만 이 방법은 필드별로 적용하기 번거롭습니다. 더 나은 방법은 구조체 전체에 정렬을 적용하는 것입니다. -Luckily there is a way to not have to think about these alignment requirements *most* of the time. We can define `GLM_FORCE_DEFAULT_ALIGNED_GENTYPES` right before including GLM: - -```c++ -#define GLM_FORCE_RADIANS -#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES -#include +```rust +#[repr(C, align(16))] +struct UniformBufferObject { + foo: glam::Vec2, + model: glam::Mat4, + view: glam::Mat4, + proj: glam::Mat4, +} ``` -This will force GLM to use a version of `vec2` and `mat4` that has the alignment requirements already specified for us. If you add this definition then you can remove the `alignas` specifier and your program should still work. +이 코드는 작동하지 않습니다. `foo` 필드 뒤에 `model` 필드가 올바르게 정렬되도록 컴파일러가 패딩을 추가해야 하는데, `#[repr(C)]`는 이를 보장하지 않을 수 있습니다. 가장 안전하고 명확한 방법은 정렬이 필요한 각 멤버에 명시적으로 `align`을 지정하는 대신, `glam`과 같은 라이브러리가 제공하는 이미 정렬된 타입을 사용하는 것입니다. 다행히 `glam::Mat4`, `glam::Vec4` 등은 기본적으로 16바이트 정렬이 되어 있습니다. 문제가 발생한 `Vec2` 같은 타입의 경우, 수동으로 패딩을 추가하거나 구조를 변경해야 할 수 있습니다. -Unfortunately this method can break down if you start using nested structures. Consider the following definition in the C++ code: +이러한 함정을 피하기 위해, 항상 정렬에 대해 명시적인 것이 좋습니다. 최종적으로 우리의 UBO 구조체는 다음과 같이 명시적으로 정렬을 보장하는 것이 가장 안전합니다. -```c++ +```rust +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct UniformBufferObject { + pub model: glam::Mat4, + pub view: glam::Mat4, + pub proj: glam::Mat4, +} +``` +위 `glam`의 기본 타입들은 이미 16바이트 정렬이 되어 있어 추가적인 `align` 속성 없이도 대부분의 경우 잘 동작합니다. 하지만 중첩 구조체에서는 문제가 발생할 수 있습니다. +```rust +#[repr(C)] struct Foo { - glm::vec2 v; -}; + v: glam::Vec2, // 8바이트 정렬 +} +#[repr(C)] struct UniformBufferObject { - Foo f1; - Foo f2; -}; + f1: Foo, + f2: Foo, // 이 필드의 오프셋은 8이 되며, 16이어야 합니다. +} ``` - -And the following shader definition: - -```c++ +이런 경우, 명시적으로 정렬을 지정해야 합니다. +```rust +#[repr(C, align(16))] struct Foo { - vec2 v; -}; - -layout(binding = 0) uniform UniformBufferObject { - Foo f1; - Foo f2; -} ubo; -``` - -In this case `f2` will have an offset of `8` whereas it should have an offset of `16` since it is a nested structure. In this case you must specify the alignment yourself: + v: glam::Vec2, +} -```c++ +#[repr(C)] struct UniformBufferObject { - Foo f1; - alignas(16) Foo f2; -}; + f1: Foo, + f2: Foo, // 이제 f1과 f2 모두 16바이트 경계에 정렬됩니다. +} ``` -These gotchas are a good reason to always be explicit about alignment. That way you won't be caught offguard by the strange symptoms of alignment errors. - -```c++ -struct UniformBufferObject { - alignas(16) glm::mat4 model; - alignas(16) glm::mat4 view; - alignas(16) glm::mat4 proj; -}; -``` +정렬 오류는 디버깅하기 매우 까다로우므로, 유니폼 버퍼로 사용할 구조체는 `#[repr(C)]`와 함께 필요에 따라 `align` 속성을 명시하는 것이 좋습니다. -Don't forget to recompile your shader after removing the `foo` field. +우리의 원래 `UniformBufferObject`로 돌아가 `foo` 필드를 제거하고 셰이더를 다시 컴파일하는 것을 잊지 마세요. -## Multiple descriptor sets +## 여러 개의 디스크립터 셋 (Multiple Descriptor Sets) -As some of the structures and function calls hinted at, it is actually possible -to bind multiple descriptor sets simultaneously. You need to specify a descriptor set layout for -each descriptor set when creating the pipeline layout. Shaders can then -reference specific descriptor sets like this: +일부 구조체와 함수 호출에서 암시되었듯이, 여러 디스크립터 셋을 동시에 바인딩하는 것도 가능합니다. 파이프라인 레이아웃을 생성할 때 각 디스크립터 셋에 대한 디스크립터 셋 레이아웃을 지정해야 합니다. 그러면 셰이더는 다음과 같이 특정 디스크립터 셋을 참조할 수 있습니다. -```c++ +```glsl layout(set = 0, binding = 0) uniform UniformBufferObject { ... } ``` -You can use this feature to put descriptors that vary per-object and descriptors -that are shared into separate descriptor sets. In that case you avoid rebinding -most of the descriptors across draw calls which is potentially more efficient. +이 기능을 사용하면 객체별로 다른 디스크립터와 공유되는 디스크립터를 별도의 디스크립터 셋에 넣을 수 있습니다. 이 경우 드로우 콜 간에 대부분의 디스크립터를 다시 바인딩하는 것을 피할 수 있어 잠재적으로 더 효율적입니다. -[C++ code](/code/23_descriptor_sets.cpp) / -[Vertex shader](/code/22_shader_ubo.vert) / -[Fragment shader](/code/22_shader_ubo.frag) +[Rust 코드](/path/to/your/code.rs) / +[정점 셰이더](/code/22_shader_ubo.vert) / +[프래그먼트 셰이더](/code/22_shader_ubo.frag) \ No newline at end of file diff --git a/ko-rust/06_Texture_mapping/00_Images.md b/ko-rust/06_Texture_mapping/00_Images.md index 8c9967f6..63b94115 100644 --- a/ko-rust/06_Texture_mapping/00_Images.md +++ b/ko-rust/06_Texture_mapping/00_Images.md @@ -1,769 +1,477 @@ -## Introduction - -The geometry has been colored using per-vertex colors so far, which is a rather -limited approach. In this part of the tutorial we're going to implement texture -mapping to make the geometry look more interesting. This will also allow us to -load and draw basic 3D models in a future chapter. - -Adding a texture to our application will involve the following steps: - -* Create an image object backed by device memory -* Fill it with pixels from an image file -* Create an image sampler -* Add a combined image sampler descriptor to sample colors from the texture - -We've already worked with image objects before, but those were automatically -created by the swap chain extension. This time we'll have to create one by -ourselves. Creating an image and filling it with data is similar to vertex -buffer creation. We'll start by creating a staging resource and filling it with -pixel data and then we copy this to the final image object that we'll use for -rendering. Although it is possible to create a staging image for this purpose, -Vulkan also allows you to copy pixels from a `VkBuffer` to an image and the API -for this is actually [faster on some hardware](https://developer.nvidia.com/vulkan-memory-management). -We'll first create this buffer and fill it with pixel values, and then we'll -create an image to copy the pixels to. Creating an image is not very different -from creating buffers. It involves querying the memory requirements, allocating -device memory and binding it, just like we've seen before. - -However, there is something extra that we'll have to take care of when working -with images. Images can have different *layouts* that affect how the pixels are -organized in memory. Due to the way graphics hardware works, simply storing the -pixels row by row may not lead to the best performance, for example. When -performing any operation on images, you must make sure that they have the layout -that is optimal for use in that operation. We've actually already seen some of -these layouts when we specified the render pass: - -* `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`: Optimal for presentation -* `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`: Optimal as attachment for writing -colors from the fragment shader -* `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`: Optimal as source in a transfer -operation, like `vkCmdCopyImageToBuffer` -* `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`: Optimal as destination in a transfer -operation, like `vkCmdCopyBufferToImage` -* `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`: Optimal for sampling from a shader - -One of the most common ways to transition the layout of an image is a *pipeline -barrier*. Pipeline barriers are primarily used for synchronizing access to -resources, like making sure that an image was written to before it is read, but -they can also be used to transition layouts. In this chapter we'll see how -pipeline barriers are used for this purpose. Barriers can additionally be used -to transfer queue family ownership when using `VK_SHARING_MODE_EXCLUSIVE`. - -## Image library - -There are many libraries available for loading images, and you can even write -your own code to load simple formats like BMP and PPM. In this tutorial we'll be -using the stb_image library from the [stb collection](https://github.com/nothings/stb). -The advantage of it is that all of the code is in a single file, so it doesn't -require any tricky build configuration. Download `stb_image.h` and store it in a -convenient location, like the directory where you saved GLFW and GLM. Add the -location to your include path. - -**Visual Studio** - -Add the directory with `stb_image.h` in it to the `Additional Include -Directories` paths. - -![](/images/include_dirs_stb.png) - -**Makefile** - -Add the directory with `stb_image.h` to the include directories for GCC: - -```text -VULKAN_SDK_PATH = /home/user/VulkanSDK/x.x.x.x/x86_64 -STB_INCLUDE_PATH = /home/user/libraries/stb - -... - -CFLAGS = -std=c++17 -I$(VULKAN_SDK_PATH)/include -I$(STB_INCLUDE_PATH) -``` - -## Loading an image - -Include the image library like this: - -```c++ -#define STB_IMAGE_IMPLEMENTATION -#include -``` - -The header only defines the prototypes of the functions by default. One code -file needs to include the header with the `STB_IMAGE_IMPLEMENTATION` definition -to include the function bodies, otherwise we'll get linking errors. - -```c++ -void initVulkan() { - ... - createCommandPool(); - createTextureImage(); - createVertexBuffer(); - ... -} - -... - -void createTextureImage() { - -} -``` +## 서론 -Create a new function `createTextureImage` where we'll load an image and upload -it into a Vulkan image object. We're going to use command buffers, so it should -be called after `createCommandPool`. +지금까지는 정점별(per-vertex) 색상을 사용해 지오메트리를 색칠해왔는데, 이는 다소 제한적인 방법입니다. 이번 장에서는 텍스처 매핑을 구현하여 지오메트리가 더 흥미롭게 보이도록 만들 것입니다. 이를 통해 다음 장에서는 기본적인 3D 모델을 로드하고 그릴 수도 있게 됩니다. -Create a new directory `textures` next to the `shaders` directory to store -texture images in. We're going to load an image called `texture.jpg` from that -directory. I've chosen to use the following -[CC0 licensed image](https://pixabay.com/en/statue-sculpture-fig-historically-1275469/) -resized to 512 x 512 pixels, but feel free to pick any image you want. The -library supports most common image file formats, like JPEG, PNG, BMP and GIF. +애플리케이션에 텍스처를 추가하는 작업은 다음 단계를 포함합니다. -![](/images/texture.jpg) +* 디바이스 메모리를 기반으로 하는 이미지 객체 생성 +* 이미지 파일의 픽셀로 채우기 +* 이미지 샘플러 생성 +* 텍스처에서 색상을 샘플링하기 위한 결합 이미지 샘플러 디스크립터 추가 -Loading an image with this library is really easy: +이전에도 이미지 객체를 다룬 적이 있지만, 그것들은 스왑체인 확장에 의해 자동으로 생성되었습니다. 이번에는 직접 하나를 만들어야 합니다. 이미지를 생성하고 데이터를 채우는 과정은 정점 버퍼 생성과 유사합니다. 먼저 스테이징 리소스를 생성하고 픽셀 데이터로 채운 다음, 렌더링에 사용할 최종 이미지 객체로 복사합니다. 이 목적으로 스테이징 이미지를 생성할 수도 있지만, Vulkan은 `VkBuffer`에서 이미지로 픽셀을 복사하는 것도 허용하며, 이 API는 [일부 하드웨어에서 실제로 더 빠릅니다](https://developer.nvidia.com/vulkan-memory-management). 우리는 먼저 이 버퍼를 생성하고 픽셀 값으로 채운 다음, 픽셀을 복사할 이미지를 생성할 것입니다. 이미지 생성은 버퍼 생성과 크게 다르지 않습니다. 이전에 보았듯이 메모리 요구사항을 쿼리하고, 디바이스 메모리를 할당하고, 바인딩하는 과정이 포함됩니다. -```c++ -void createTextureImage() { - int texWidth, texHeight, texChannels; - stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); - VkDeviceSize imageSize = texWidth * texHeight * 4; +하지만 이미지 작업 시에는 추가적으로 신경 써야 할 것이 있습니다. 이미지는 메모리에서 픽셀이 구성되는 방식에 영향을 미치는 다양한 *레이아웃(layouts)*을 가질 수 있습니다. 그래픽 하드웨어의 작동 방식 때문에, 단순히 픽셀을 행 단위로 저장하는 것이 최상의 성능으로 이어지지 않을 수 있습니다. 이미지에 대한 어떠한 작업을 수행할 때든, 해당 작업에 최적화된 레이아웃을 가지고 있는지 확인해야 합니다. 렌더 패스를 지정할 때 이미 이러한 레이아웃 중 일부를 본 적이 있습니다: - if (!pixels) { - throw std::runtime_error("failed to load texture image!"); - } -} -``` +* `vk::ImageLayout::PRESENT_SRC_KHR`: 화면 표시에 최적화 +* `vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL`: 프래그먼트 셰이더에서 색상을 쓰는 어태치먼트로 최적화 +* `vk::ImageLayout::TRANSFER_SRC_OPTIMAL`: `vkCmdCopyImageToBuffer`와 같은 전송 작업의 소스로 최적화 +* `vk::ImageLayout::TRANSFER_DST_OPTIMAL`: `vkCmdCopyBufferToImage`와 같은 전송 작업의 대상으로 최적화 +* `vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL`: 셰이더에서 샘플링하기에 최적화 -The `stbi_load` function takes the file path and number of channels to load as -arguments. The `STBI_rgb_alpha` value forces the image to be loaded with an -alpha channel, even if it doesn't have one, which is nice for consistency with -other textures in the future. The middle three parameters are outputs for the -width, height and actual number of channels in the image. The pointer that is -returned is the first element in an array of pixel values. The pixels are laid -out row by row with 4 bytes per pixel in the case of `STBI_rgb_alpha` for a -total of `texWidth * texHeight * 4` values. +이미지의 레이아웃을 전환하는 가장 일반적인 방법 중 하나는 *파이프라인 배리어(pipeline barrier)*입니다. 파이프라인 배리어는 주로 리소스 접근을 동기화하는 데 사용됩니다. 예를 들어, 이미지를 읽기 전에 쓰기가 완료되었는지 확인하는 것과 같지만, 레이아웃을 전환하는 데에도 사용할 수 있습니다. 이번 장에서는 파이프라인 배리어가 이 목적으로 어떻게 사용되는지 볼 것입니다. 배리어는 `vk::SharingMode::EXCLUSIVE`를 사용할 때 큐 패밀리 소유권을 이전하는 데에도 추가적으로 사용될 수 있습니다. -## Staging buffer +## 이미지 라이브러리 -We're now going to create a buffer in host visible memory so that we can use -`vkMapMemory` and copy the pixels to it. Add variables for this temporary buffer -to the `createTextureImage` function: +이미지를 로드하기 위한 많은 라이브러리가 있으며, Rust에서는 `image` 크레이트가 가장 대중적이고 강력한 선택지입니다. `stb_image`와 마찬가지로 다양한 이미지 형식을 지원하며 사용하기 매우 쉽습니다. -```c++ -VkBuffer stagingBuffer; -VkDeviceMemory stagingBufferMemory; -``` +`Cargo.toml` 파일의 `[dependencies]` 섹션에 `image` 크레이트를 추가하세요. -The buffer should be in host visible memory so that we can map it and it should -be usable as a transfer source so that we can copy it to an image later on: - -```c++ -createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); +```toml +[dependencies] +ash = "0.37" +# ... other dependencies +image = "0.24" ``` -We can then directly copy the pixel values that we got from the image loading -library to the buffer: - -```c++ -void* data; -vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data); - memcpy(data, pixels, static_cast(imageSize)); -vkUnmapMemory(device, stagingBufferMemory); -``` +## 이미지 로딩하기 -Don't forget to clean up the original pixel array now: +이제 `image` 크레이트를 사용하여 텍스처를 로드할 수 있습니다. 먼저 애플리케이션 구조체에 관련 필드를 추가해야 합니다. -```c++ -stbi_image_free(pixels); +```rust +// In VulkanApp struct +struct VulkanApp { + // ... + texture_image: vk::Image, + texture_image_memory: vk::DeviceMemory, + // ... +} ``` -## Texture Image - -Although we could set up the shader to access the pixel values in the buffer, -it's better to use image objects in Vulkan for this purpose. Image objects will -make it easier and faster to retrieve colors by allowing us to use 2D -coordinates, for one. Pixels within an image object are known as texels and -we'll use that name from this point on. Add the following new class members: +그리고 `init_vulkan` 함수에서 `create_texture_image` 함수를 호출하도록 순서를 조정합니다. 커맨드 버퍼를 사용하므로 커맨드 풀 생성 이후에 호출되어야 합니다. -```c++ -VkImage textureImage; -VkDeviceMemory textureImageMemory; -``` +```rust +impl VulkanApp { + pub fn init_vulkan(&mut self) -> Result<(), Box> { + // ... + self.create_command_pool()?; + self.create_texture_image()?; + self.create_vertex_buffer()?; + // ... + Ok(()) + } -The parameters for an image are specified in a `VkImageCreateInfo` struct: - -```c++ -VkImageCreateInfo imageInfo{}; -imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; -imageInfo.imageType = VK_IMAGE_TYPE_2D; -imageInfo.extent.width = static_cast(texWidth); -imageInfo.extent.height = static_cast(texHeight); -imageInfo.extent.depth = 1; -imageInfo.mipLevels = 1; -imageInfo.arrayLayers = 1; -``` + // ... -The image type, specified in the `imageType` field, tells Vulkan with what kind -of coordinate system the texels in the image are going to be addressed. It is -possible to create 1D, 2D and 3D images. One dimensional images can be used to -store an array of data or gradient, two dimensional images are mainly used for -textures, and three dimensional images can be used to store voxel volumes, for -example. The `extent` field specifies the dimensions of the image, basically how -many texels there are on each axis. That's why `depth` must be `1` instead of -`0`. Our texture will not be an array and we won't be using mipmapping for now. - -```c++ -imageInfo.format = VK_FORMAT_R8G8B8A8_SRGB; + fn create_texture_image(&mut self) -> Result<(), Box> { + // ... + Ok(()) + } +} ``` -Vulkan supports many possible image formats, but we should use the same format -for the texels as the pixels in the buffer, otherwise the copy operation will -fail. +이제 `create_texture_image` 함수에서 이미지를 로드합니다. `shaders` 디렉토리 옆에 `textures` 디렉토리를 만들고, 이 튜토리얼에서 사용할 512x512 픽셀 크기의 `texture.jpg` 이미지를 저장하세요. -```c++ -imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; -``` - -The `tiling` field can have one of two values: +![](/images/texture.jpg) -* `VK_IMAGE_TILING_LINEAR`: Texels are laid out in row-major order like our -`pixels` array -* `VK_IMAGE_TILING_OPTIMAL`: Texels are laid out in an implementation defined -order for optimal access +`image` 크레이트로 이미지를 로드하는 것은 매우 간단합니다. -Unlike the layout of an image, the tiling mode cannot be changed at a later -time. If you want to be able to directly access texels in the memory of the -image, then you must use `VK_IMAGE_TILING_LINEAR`. We will be using a staging -buffer instead of a staging image, so this won't be necessary. We will be using -`VK_IMAGE_TILING_OPTIMAL` for efficient access from the shader. +```rust +use image::GenericImageView; +// ... inside create_texture_image +fn create_texture_image(&mut self) -> Result<(), Box> { + let image_object = image::open("textures/texture.jpg")?; + let (tex_width, tex_height) = image_object.dimensions(); + let image_data = image_object.to_rgba8().into_raw(); + let image_size = (tex_width * tex_height * 4) as vk::DeviceSize; -```c++ -imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + // ... rest of the function + Ok(()) +} ``` -There are only two possible values for the `initialLayout` of an image: - -* `VK_IMAGE_LAYOUT_UNDEFINED`: Not usable by the GPU and the very first -transition will discard the texels. -* `VK_IMAGE_LAYOUT_PREINITIALIZED`: Not usable by the GPU, but the first -transition will preserve the texels. - -There are few situations where it is necessary for the texels to be preserved -during the first transition. One example, however, would be if you wanted to use -an image as a staging image in combination with the `VK_IMAGE_TILING_LINEAR` -layout. In that case, you'd want to upload the texel data to it and then -transition the image to be a transfer source without losing the data. In our -case, however, we're first going to transition the image to be a transfer -destination and then copy texel data to it from a buffer object, so we don't -need this property and can safely use `VK_IMAGE_LAYOUT_UNDEFINED`. +`image::open`은 파일 경로에서 이미지를 로드합니다. `.to_rgba8()`는 이미지를 RGBA 형식으로 변환하여 알파 채널이 없는 이미지도 일관성 있게 처리합니다. `.into_raw()`는 이 이미지 데이터를 `Vec` 형태로 반환합니다. 이 벡터는 `tex_width * tex_height * 4` 바이트 크기를 가집니다. -```c++ -imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; -``` +## 스테이징 버퍼 -The `usage` field has the same semantics as the one during buffer creation. The -image is going to be used as destination for the buffer copy, so it should be -set up as a transfer destination. We also want to be able to access the image -from the shader to color our mesh, so the usage should include -`VK_IMAGE_USAGE_SAMPLED_BIT`. +이제 호스트 가시성(host-visible) 메모리에 버퍼를 생성하여 `vkMapMemory`를 사용하고 픽셀 데이터를 복사할 수 있도록 합니다. -```c++ -imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; -``` +```rust +// ... inside create_texture_image +let (staging_buffer, staging_buffer_memory) = self.create_buffer( + image_size, + vk::BufferUsageFlags::TRANSFER_SRC, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, +)?; -The image will only be used by one queue family: the one that supports graphics -(and therefore also) transfer operations. +unsafe { + let data_ptr = self.device.map_memory( + staging_buffer_memory, + 0, + image_size, + vk::MemoryMapFlags::empty(), + )? as *mut u8; -```c++ -imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; -imageInfo.flags = 0; // Optional -``` + data_ptr.copy_from_nonoverlapping(image_data.as_ptr(), image_data.len()); -The `samples` flag is related to multisampling. This is only relevant for images -that will be used as attachments, so stick to one sample. There are some -optional flags for images that are related to sparse images. Sparse images are -images where only certain regions are actually backed by memory. If you were -using a 3D texture for a voxel terrain, for example, then you could use this to -avoid allocating memory to store large volumes of "air" values. We won't be -using it in this tutorial, so leave it to its default value of `0`. - -```c++ -if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) { - throw std::runtime_error("failed to create image!"); + self.device.unmap_memory(staging_buffer_memory); } ``` -The image is created using `vkCreateImage`, which doesn't have any particularly -noteworthy parameters. It is possible that the `VK_FORMAT_R8G8B8A8_SRGB` format -is not supported by the graphics hardware. You should have a list of acceptable -alternatives and go with the best one that is supported. However, support for -this particular format is so widespread that we'll skip this step. Using -different formats would also require annoying conversions. We will get back to -this in the depth buffer chapter, where we'll implement such a system. - -```c++ -VkMemoryRequirements memRequirements; -vkGetImageMemoryRequirements(device, textureImage, &memRequirements); - -VkMemoryAllocateInfo allocInfo{}; -allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; -allocInfo.allocationSize = memRequirements.size; -allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); - -if (vkAllocateMemory(device, &allocInfo, nullptr, &textureImageMemory) != VK_SUCCESS) { - throw std::runtime_error("failed to allocate image memory!"); -} +`create_buffer` 헬퍼 함수를 사용하여 스테이징 버퍼를 생성합니다. 이 버퍼는 호스트에서 접근 가능해야 하고(`HOST_VISIBLE`), 전송 소스로 사용될 수 있어야 합니다(`TRANSFER_SRC`). `HOST_COHERENT` 플래그는 매핑된 메모리에 쓴 내용이 자동으로 디바이스에 보이도록 보장합니다. -vkBindImageMemory(device, textureImage, textureImageMemory, 0); -``` +`map_memory`로 메모리 포인터를 얻은 후, `copy_from_nonoverlapping`을 사용하여 이미지 픽셀 데이터를 버퍼로 복사합니다. 작업이 끝나면 `unmap_memory`를 호출합니다. Rust의 `image_data`는 `Vec`이므로 범위를 벗어나면 자동으로 메모리가 해제됩니다. C++의 `stbi_image_free`처럼 수동으로 해제할 필요가 없습니다. -Allocating memory for an image works in exactly the same way as allocating -memory for a buffer. Use `vkGetImageMemoryRequirements` instead of -`vkGetBufferMemoryRequirements`, and use `vkBindImageMemory` instead of -`vkBindBufferMemory`. - -This function is already getting quite large and there'll be a need to create -more images in later chapters, so we should abstract image creation into a -`createImage` function, like we did for buffers. Create the function and move -the image object creation and memory allocation to it: - -```c++ -void createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) { - VkImageCreateInfo imageInfo{}; - imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; - imageInfo.imageType = VK_IMAGE_TYPE_2D; - imageInfo.extent.width = width; - imageInfo.extent.height = height; - imageInfo.extent.depth = 1; - imageInfo.mipLevels = 1; - imageInfo.arrayLayers = 1; - imageInfo.format = format; - imageInfo.tiling = tiling; - imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - imageInfo.usage = usage; - imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; - imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; - - if (vkCreateImage(device, &imageInfo, nullptr, &image) != VK_SUCCESS) { - throw std::runtime_error("failed to create image!"); - } +## 텍스처 이미지 - VkMemoryRequirements memRequirements; - vkGetImageMemoryRequirements(device, image, &memRequirements); +셰이더가 버퍼의 픽셀 값에 접근하도록 설정할 수도 있지만, Vulkan에서는 이미지 객체를 사용하는 것이 더 좋습니다. 이미지 객체는 2D 좌표를 사용할 수 있게 하여 색상을 더 쉽고 빠르게 가져올 수 있게 해줍니다. 이미지 객체 내의 픽셀은 텍셀(texel)이라고 하며, 지금부터 이 용어를 사용하겠습니다. - VkMemoryAllocateInfo allocInfo{}; - allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; - allocInfo.allocationSize = memRequirements.size; - allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); +`createTextureImage` 함수의 나머지 부분에서 텍스처 이미지를 생성합니다. - if (vkAllocateMemory(device, &allocInfo, nullptr, &imageMemory) != VK_SUCCESS) { - throw std::runtime_error("failed to allocate image memory!"); - } +```rust +// ... inside create_texture_image +let (texture_image, texture_image_memory) = self.create_image( + tex_width, + tex_height, + vk::Format::R8G8B8A8_SRGB, + vk::ImageTiling::OPTIMAL, + vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED, + vk::MemoryPropertyFlags::DEVICE_LOCAL, +)?; - vkBindImageMemory(device, image, imageMemory, 0); -} +self.texture_image = texture_image; +self.texture_image_memory = texture_image_memory; ``` -I've made the width, height, format, tiling mode, usage, and memory properties -parameters, because these will all vary between the images we'll be creating -throughout this tutorial. +버퍼 생성 로직을 `create_buffer`로 리팩토링했듯이, 이미지 생성 로직도 `create_image`라는 헬퍼 함수로 추상화하는 것이 좋습니다. -The `createTextureImage` function can now be simplified to: +```rust +impl VulkanApp { + fn create_image( + &self, + width: u32, + height: u32, + format: vk::Format, + tiling: vk::ImageTiling, + usage: vk::ImageUsageFlags, + properties: vk::MemoryPropertyFlags, + ) -> Result<(vk::Image, vk::DeviceMemory), Box> { + let image_info = vk::ImageCreateInfo::builder() + .image_type(vk::ImageType::TYPE_2D) + .extent(vk::Extent3D { width, height, depth: 1 }) + .mip_levels(1) + .array_layers(1) + .format(format) + .tiling(tiling) + .initial_layout(vk::ImageLayout::UNDEFINED) + .usage(usage) + .samples(vk::SampleCountFlags::TYPE_1) + .sharing_mode(vk::SharingMode::EXCLUSIVE); -```c++ -void createTextureImage() { - int texWidth, texHeight, texChannels; - stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); - VkDeviceSize imageSize = texWidth * texHeight * 4; + let image = unsafe { self.device.create_image(&image_info, None)? }; - if (!pixels) { - throw std::runtime_error("failed to load texture image!"); - } + let mem_requirements = unsafe { self.device.get_image_memory_requirements(image) }; - VkBuffer stagingBuffer; - VkDeviceMemory stagingBufferMemory; - createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); + let alloc_info = vk::MemoryAllocateInfo::builder() + .allocation_size(mem_requirements.size) + .memory_type_index(self.find_memory_type( + mem_requirements.memory_type_bits, + properties, + )?); - void* data; - vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data); - memcpy(data, pixels, static_cast(imageSize)); - vkUnmapMemory(device, stagingBufferMemory); + let image_memory = unsafe { self.device.allocate_memory(&alloc_info, None)? }; - stbi_image_free(pixels); + unsafe { self.device.bind_image_memory(image, image_memory, 0)? }; - createImage(texWidth, texHeight, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory); + Ok((image, image_memory)) + } } ``` -## Layout transitions - -The function we're going to write now involves recording and executing a command -buffer again, so now's a good time to move that logic into a helper function or -two: +`create_image` 함수는 너비, 높이, 포맷, 타일링, 사용법, 메모리 속성을 인자로 받습니다. +* `image_type`: 2D 텍스처이므로 `TYPE_2D`입니다. +* `extent`: 이미지의 크기를 지정합니다. 2D 이미지이므로 `depth`는 1입니다. +* `format`: `VK_FORMAT_R8G8B8A8_SRGB`는 8비트 RGBA 채널을 사용하며, sRGB 색 공간에 있음을 의미합니다. 픽셀 데이터와 형식이 일치해야 합니다. +* `tiling`: `OPTIMAL`은 셰이더에서 효율적으로 접근하기 위한 구현 정의 레이아웃을 사용합니다. +* `initial_layout`: `UNDEFINED`로 설정합니다. 첫 전환 시 텍셀 내용이 필요 없기 때문입니다. +* `usage`: `TRANSFER_DST`는 이 이미지가 복사 작업의 대상이 될 수 있음을, `SAMPLED`는 셰이더에서 샘플링할 수 있음을 의미합니다. +* 나머지 필드는 버퍼 생성과 유사합니다. -```c++ -VkCommandBuffer beginSingleTimeCommands() { - VkCommandBufferAllocateInfo allocInfo{}; - allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; - allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; - allocInfo.commandPool = commandPool; - allocInfo.commandBufferCount = 1; +이미지를 생성한 후, `get_image_memory_requirements`로 메모리 요구사항을 얻고, 적절한 메모리 타입을 찾아 `allocate_memory`로 메모리를 할당한 뒤, `bind_image_memory`로 이미지와 메모리를 바인딩합니다. - VkCommandBuffer commandBuffer; - vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); +## 레이아웃 전환 - VkCommandBufferBeginInfo beginInfo{}; - beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; - beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; +이제부터 커맨드 버퍼를 기록하고 실행하는 작업이 반복되므로, 이 로직을 헬퍼 함수로 분리합시다. - vkBeginCommandBuffer(commandBuffer, &beginInfo); +```rust +impl VulkanApp { + fn begin_single_time_commands(&self) -> Result> { + let alloc_info = vk::CommandBufferAllocateInfo::builder() + .level(vk::CommandBufferLevel::PRIMARY) + .command_pool(self.command_pool) + .command_buffer_count(1); - return commandBuffer; -} - -void endSingleTimeCommands(VkCommandBuffer commandBuffer) { - vkEndCommandBuffer(commandBuffer); - - VkSubmitInfo submitInfo{}; - submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &commandBuffer; + let command_buffer = unsafe { self.device.allocate_command_buffers(&alloc_info)?[0] }; - vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); - vkQueueWaitIdle(graphicsQueue); - - vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); -} -``` + let begin_info = vk::CommandBufferBeginInfo::builder() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); -The code for these functions is based on the existing code in `copyBuffer`. You -can now simplify that function to: + unsafe { self.device.begin_command_buffer(command_buffer, &begin_info)? }; -```c++ -void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { - VkCommandBuffer commandBuffer = beginSingleTimeCommands(); + Ok(command_buffer) + } - VkBufferCopy copyRegion{}; - copyRegion.size = size; - vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region); + fn end_single_time_commands(&self, command_buffer: vk::CommandBuffer) -> Result<(), Box> { + unsafe { + self.device.end_command_buffer(command_buffer)?; - endSingleTimeCommands(commandBuffer); -} -``` + let submit_info = vk::SubmitInfo::builder() + .command_buffers(&[command_buffer]); -If we were still using buffers, then we could now write a function to record and -execute `vkCmdCopyBufferToImage` to finish the job, but this command requires -the image to be in the right layout first. Create a new function to handle -layout transitions: + self.device.queue_submit(self.graphics_queue, &[submit_info.build()], vk::Fence::null())?; + self.device.queue_wait_idle(self.graphics_queue)?; -```c++ -void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout) { - VkCommandBuffer commandBuffer = beginSingleTimeCommands(); - - endSingleTimeCommands(commandBuffer); + self.device.free_command_buffers(self.command_pool, &[command_buffer]); + } + Ok(()) + } } ``` -One of the most common ways to perform layout transitions is using an *image -memory barrier*. A pipeline barrier like that is generally used to synchronize -access to resources, like ensuring that a write to a buffer completes before -reading from it, but it can also be used to transition image layouts and -transfer queue family ownership when `VK_SHARING_MODE_EXCLUSIVE` is used. There -is an equivalent *buffer memory barrier* to do this for buffers. - -```c++ -VkImageMemoryBarrier barrier{}; -barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; -barrier.oldLayout = oldLayout; -barrier.newLayout = newLayout; -``` - -The first two fields specify layout transition. It is possible to use -`VK_IMAGE_LAYOUT_UNDEFINED` as `oldLayout` if you don't care about the existing -contents of the image. - -```c++ -barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; -barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; -``` - -If you are using the barrier to transfer queue family ownership, then these two -fields should be the indices of the queue families. They must be set to -`VK_QUEUE_FAMILY_IGNORED` if you don't want to do this (not the default value!). - -```c++ -barrier.image = image; -barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; -barrier.subresourceRange.baseMipLevel = 0; -barrier.subresourceRange.levelCount = 1; -barrier.subresourceRange.baseArrayLayer = 0; -barrier.subresourceRange.layerCount = 1; -``` +이 두 함수는 이전에 `copy_buffer`에서 사용했던 로직과 동일합니다. 일회성 커맨드 버퍼를 할당하고 시작하며, 제출 후 대기하고 해제합니다. 이제 `copy_buffer` 함수를 이 헬퍼들을 사용해 단순화할 수 있습니다. -The `image` and `subresourceRange` specify the image that is affected and the -specific part of the image. Our image is not an array and does not have mipmapping -levels, so only one level and layer are specified. +```rust +// In VulkanApp impl +fn copy_buffer( + &self, + src_buffer: vk::Buffer, + dst_buffer: vk::Buffer, + size: vk::DeviceSize, +) -> Result<(), Box> { + let command_buffer = self.begin_single_time_commands()?; -```c++ -barrier.srcAccessMask = 0; // TODO -barrier.dstAccessMask = 0; // TODO -``` + let copy_region = vk::BufferCopy::builder().size(size); + unsafe { + self.device.cmd_copy_buffer(command_buffer, src_buffer, dst_buffer, &[copy_region.build()]); + } -Barriers are primarily used for synchronization purposes, so you must specify -which types of operations that involve the resource must happen before the -barrier, and which operations that involve the resource must wait on the -barrier. We need to do that despite already using `vkQueueWaitIdle` to manually -synchronize. The right values depend on the old and new layout, so we'll get -back to this once we've figured out which transitions we're going to use. - -```c++ -vkCmdPipelineBarrier( - commandBuffer, - 0 /* TODO */, 0 /* TODO */, - 0, - 0, nullptr, - 0, nullptr, - 1, &barrier -); -``` + self.end_single_time_commands(command_buffer)?; -All types of pipeline barriers are submitted using the same function. The first -parameter after the command buffer specifies in which pipeline stage the -operations occur that should happen before the barrier. The second parameter -specifies the pipeline stage in which operations will wait on the barrier. The -pipeline stages that you are allowed to specify before and after the barrier -depend on how you use the resource before and after the barrier. The allowed -values are listed in [this table](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-access-types-supported) -of the specification. For example, if you're going to read from a uniform after -the barrier, you would specify a usage of `VK_ACCESS_UNIFORM_READ_BIT` and the -earliest shader that will read from the uniform as pipeline stage, for example -`VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT`. It would not make sense to specify -a non-shader pipeline stage for this type of usage and the validation layers -will warn you when you specify a pipeline stage that does not match the type of -usage. - -The third parameter is either `0` or `VK_DEPENDENCY_BY_REGION_BIT`. The latter -turns the barrier into a per-region condition. That means that the -implementation is allowed to already begin reading from the parts of a resource -that were written so far, for example. - -The last three pairs of parameters reference arrays of pipeline barriers of the -three available types: memory barriers, buffer memory barriers, and image memory -barriers like the one we're using here. Note that we're not using the `VkFormat` -parameter yet, but we'll be using that one for special transitions in the depth -buffer chapter. - -## Copying buffer to image - -Before we get back to `createTextureImage`, we're going to write one more helper -function: `copyBufferToImage`: - -```c++ -void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) { - VkCommandBuffer commandBuffer = beginSingleTimeCommands(); - - endSingleTimeCommands(commandBuffer); + Ok(()) } ``` -Just like with buffer copies, you need to specify which part of the buffer is -going to be copied to which part of the image. This happens through -`VkBufferImageCopy` structs: - -```c++ -VkBufferImageCopy region{}; -region.bufferOffset = 0; -region.bufferRowLength = 0; -region.bufferImageHeight = 0; - -region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; -region.imageSubresource.mipLevel = 0; -region.imageSubresource.baseArrayLayer = 0; -region.imageSubresource.layerCount = 1; - -region.imageOffset = {0, 0, 0}; -region.imageExtent = { - width, - height, - 1 -}; -``` - -Most of these fields are self-explanatory. The `bufferOffset` specifies the byte -offset in the buffer at which the pixel values start. The `bufferRowLength` and -`bufferImageHeight` fields specify how the pixels are laid out in memory. For -example, you could have some padding bytes between rows of the image. Specifying -`0` for both indicates that the pixels are simply tightly packed like they are -in our case. The `imageSubresource`, `imageOffset` and `imageExtent` fields -indicate to which part of the image we want to copy the pixels. - -Buffer to image copy operations are enqueued using the `vkCmdCopyBufferToImage` -function: - -```c++ -vkCmdCopyBufferToImage( - commandBuffer, - buffer, - image, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - 1, - ®ion -); -``` - -The fourth parameter indicates which layout the image is currently using. I'm -assuming here that the image has already been transitioned to the layout that is -optimal for copying pixels to. Right now we're only copying one chunk of pixels -to the whole image, but it's possible to specify an array of `VkBufferImageCopy` -to perform many different copies from this buffer to the image in one operation. - -## Preparing the texture image - -We now have all of the tools we need to finish setting up the texture image, so -we're going back to the `createTextureImage` function. The last thing we did -there was creating the texture image. The next step is to copy the staging -buffer to the texture image. This involves two steps: - -* Transition the texture image to `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL` -* Execute the buffer to image copy operation +이제 이미지 레이아웃 전환을 위한 함수를 만듭니다. 레이아웃 전환에는 *이미지 메모리 배리어*를 사용한 파이프라인 배리어가 주로 사용됩니다. + +```rust +// In VulkanApp impl +fn transition_image_layout( + &self, + image: vk::Image, + format: vk::Format, // format is not used yet, but will be for depth buffer + old_layout: vk::ImageLayout, + new_layout: vk::ImageLayout, +) -> Result<(), Box> { + let command_buffer = self.begin_single_time_commands()?; + + let (src_access_mask, dst_access_mask, src_stage, dst_stage) = + match (old_layout, new_layout) { + ( + vk::ImageLayout::UNDEFINED, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + ) => ( + vk::AccessFlags::empty(), + vk::AccessFlags::TRANSFER_WRITE, + vk::PipelineStageFlags::TOP_OF_PIPE, + vk::PipelineStageFlags::TRANSFER, + ), + ( + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + ) => ( + vk::AccessFlags::TRANSFER_WRITE, + vk::AccessFlags::SHADER_READ, + vk::PipelineStageFlags::TRANSFER, + vk::PipelineStageFlags::FRAGMENT_SHADER, + ), + _ => return Err("Unsupported layout transition!".into()), + }; + + let barrier = vk::ImageMemoryBarrier::builder() + .old_layout(old_layout) + .new_layout(new_layout) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(image) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }) + .src_access_mask(src_access_mask) + .dst_access_mask(dst_access_mask); + + unsafe { + self.device.cmd_pipeline_barrier( + command_buffer, + src_stage, + dst_stage, + vk::DependencyFlags::empty(), + &[], + &[], + &[barrier.build()], + ); + } -This is easy to do with the functions we just created: + self.end_single_time_commands(command_buffer)?; -```c++ -transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); -copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast(texHeight)); + Ok(()) +} ``` -The image was created with the `VK_IMAGE_LAYOUT_UNDEFINED` layout, so that one -should be specified as old layout when transitioning `textureImage`. Remember -that we can do this because we don't care about its contents before performing -the copy operation. +배리어는 리소스 접근을 동기화하는 데 사용됩니다. +* `old_layout`, `new_layout`: 전환 전후의 레이아웃을 지정합니다. +* `src_queue_family_index`, `dst_queue_family_index`: 큐 패밀리 소유권 이전이 없으면 `IGNORED`로 설정합니다. +* `image`, `subresource_range`: 어떤 이미지의 어떤 부분에 배리어를 적용할지 지정합니다. +* `src_access_mask`, `dst_access_mask`: 배리어 이전 작업과 배리어 이후 작업을 동기화합니다. +* `source_stage`, `destination_stage`: 위 작업들이 발생하는 파이프라인 단계를 지정합니다. + +우리가 처리할 두 가지 전환은 다음과 같습니다. +1. **Undefined → Transfer Destination**: 데이터를 이미지에 쓰기 전입니다. 이전 작업은 없으므로 `src_access_mask`는 비어있고, `src_stage`는 파이프라인의 가장 처음인 `TOP_OF_PIPE`입니다. 쓰기 작업은 전송(transfer) 작업이므로 `dst_access_mask`는 `TRANSFER_WRITE`, `dst_stage`는 `TRANSFER`입니다. +2. **Transfer Destination → Shader Read Only**: 이미지 쓰기가 완료된 후, 셰이더에서 읽을 준비를 합니다. 이전 작업은 전송 쓰기(`TRANSFER_WRITE` in `TRANSFER` stage)였고, 이후 작업은 프래그먼트 셰이더에서의 읽기(`SHADER_READ` in `FRAGMENT_SHADER` stage)가 될 것입니다. + +## 버퍼를 이미지로 복사하기 + +스테이징 버퍼의 데이터를 이미지로 복사하는 헬퍼 함수도 만듭니다. + +```rust +// In VulkanApp impl +fn copy_buffer_to_image( + &self, + buffer: vk::Buffer, + image: vk::Image, + width: u32, + height: u32, +) -> Result<(), Box> { + let command_buffer = self.begin_single_time_commands()?; + + let region = vk::BufferImageCopy::builder() + .buffer_offset(0) + .buffer_row_length(0) + .buffer_image_height(0) + .image_subresource(vk::ImageSubresourceLayers { + aspect_mask: vk::ImageAspectFlags::COLOR, + mip_level: 0, + base_array_layer: 0, + layer_count: 1, + }) + .image_offset(vk::Offset3D { x: 0, y: 0, z: 0 }) + .image_extent(vk::Extent3D { width, height, depth: 1 }); + + unsafe { + self.device.cmd_copy_buffer_to_image( + command_buffer, + buffer, + image, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + &[region.build()], + ); + } -To be able to start sampling from the texture image in the shader, we need one -last transition to prepare it for shader access: + self.end_single_time_commands(command_buffer)?; -```c++ -transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + Ok(()) +} ``` +`vkCmdCopyBufferToImage`는 버퍼의 어느 영역을 이미지의 어느 영역으로 복사할지 `VkBufferImageCopy` 구조체로 지정받습니다. 여기서 이미지는 복사 대상에 최적화된 레이아웃인 `TRANSFER_DST_OPTIMAL` 상태여야 합니다. -## Transition barrier masks - -If you run your application with validation layers enabled now, then you'll see that -it complains about the access masks and pipeline stages in -`transitionImageLayout` being invalid. We still need to set those based on the -layouts in the transition. - -There are two transitions we need to handle: +## 텍스처 이미지 준비하기 -* Undefined → transfer destination: transfer writes that don't need to wait on -anything -* Transfer destination → shader reading: shader reads should wait on transfer -writes, specifically the shader reads in the fragment shader, because that's -where we're going to use the texture +이제 모든 헬퍼 함수를 사용하여 `create_texture_image` 함수를 완성할 수 있습니다. -These rules are specified using the following access masks and pipeline stages: +```rust +// final version of create_texture_image +fn create_texture_image(&mut self) -> Result<(), Box> { + let image_object = image::open("textures/texture.jpg")?; + let (tex_width, tex_height) = image_object.dimensions(); + let image_data = image_object.to_rgba8().into_raw(); + let image_size = (tex_width * tex_height * 4) as vk::DeviceSize; -```c++ -VkPipelineStageFlags sourceStage; -VkPipelineStageFlags destinationStage; + let (staging_buffer, staging_buffer_memory) = self.create_buffer( + image_size, + vk::BufferUsageFlags::TRANSFER_SRC, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + )?; -if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) { - barrier.srcAccessMask = 0; - barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - - sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; - destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT; -} else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) { - barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; - barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; - - sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT; - destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; -} else { - throw std::invalid_argument("unsupported layout transition!"); -} + // Copy data to staging buffer + unsafe { + let data_ptr = self.device.map_memory(staging_buffer_memory, 0, image_size, vk::MemoryMapFlags::empty())? as *mut u8; + data_ptr.copy_from_nonoverlapping(image_data.as_ptr(), image_data.len()); + self.device.unmap_memory(staging_buffer_memory); + } -vkCmdPipelineBarrier( - commandBuffer, - sourceStage, destinationStage, - 0, - 0, nullptr, - 0, nullptr, - 1, &barrier -); -``` + let (texture_image, texture_image_memory) = self.create_image( + tex_width, + tex_height, + vk::Format::R8G8B8A8_SRGB, + vk::ImageTiling::OPTIMAL, + vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::SAMPLED, + vk::MemoryPropertyFlags::DEVICE_LOCAL, + )?; + self.texture_image = texture_image; + self.texture_image_memory = texture_image_memory; + + // Transition layout and copy buffer + self.transition_image_layout( + self.texture_image, + vk::Format::R8G8B8A8_SRGB, + vk::ImageLayout::UNDEFINED, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + )?; + self.copy_buffer_to_image(staging_buffer, self.texture_image, tex_width, tex_height)?; + self.transition_image_layout( + self.texture_image, + vk::Format::R8G8B8A8_SRGB, + vk::ImageLayout::TRANSFER_DST_OPTIMAL, + vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + )?; + + // Cleanup staging buffer + unsafe { + self.device.destroy_buffer(staging_buffer, None); + self.device.free_memory(staging_buffer_memory, None); + } -As you can see in the aforementioned table, transfer writes must occur in the -pipeline transfer stage. Since the writes don't have to wait on anything, you -may specify an empty access mask and the earliest possible pipeline stage -`VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT` for the pre-barrier operations. It should be -noted that `VK_PIPELINE_STAGE_TRANSFER_BIT` is not a *real* stage within the -graphics and compute pipelines. It is more of a pseudo-stage where transfers -happen. See [the documentation](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#VkPipelineStageFlagBits) -for more information and other examples of pseudo-stages. - -The image will be written in the same pipeline stage and subsequently read by -the fragment shader, which is why we specify shader reading access in the -fragment shader pipeline stage. - -If we need to do more transitions in the future, then we'll extend the function. -The application should now run successfully, although there are of course no -visual changes yet. - -One thing to note is that command buffer submission results in implicit -`VK_ACCESS_HOST_WRITE_BIT` synchronization at the beginning. Since the -`transitionImageLayout` function executes a command buffer with only a single -command, you could use this implicit synchronization and set `srcAccessMask` to -`0` if you ever needed a `VK_ACCESS_HOST_WRITE_BIT` dependency in a layout -transition. It's up to you if you want to be explicit about it or not, but I'm -personally not a fan of relying on these OpenGL-like "hidden" operations. - -There is actually a special type of image layout that supports all operations, -`VK_IMAGE_LAYOUT_GENERAL`. The problem with it, of course, is that it doesn't -necessarily offer the best performance for any operation. It is required for -some special cases, like using an image as both input and output, or for reading -an image after it has left the preinitialized layout. - -All of the helper functions that submit commands so far have been set up to -execute synchronously by waiting for the queue to become idle. For practical -applications it is recommended to combine these operations in a single command -buffer and execute them asynchronously for higher throughput, especially the -transitions and copy in the `createTextureImage` function. Try to experiment -with this by creating a `setupCommandBuffer` that the helper functions record -commands into, and add a `flushSetupCommands` to execute the commands that have -been recorded so far. It's best to do this after the texture mapping works to -check if the texture resources are still set up correctly. - -## Cleanup - -Finish the `createTextureImage` function by cleaning up the staging buffer and -its memory at the end: - -```c++ - transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); - - vkDestroyBuffer(device, stagingBuffer, nullptr); - vkFreeMemory(device, stagingBufferMemory, nullptr); + Ok(()) } ``` -The main texture image is used until the end of the program: - -```c++ -void cleanup() { - cleanupSwapChain(); - - vkDestroyImage(device, textureImage, nullptr); - vkFreeMemory(device, textureImageMemory, nullptr); - - ... +전체 프로세스는 다음과 같습니다: +1. 이미지를 파일에서 로드합니다. +2. 픽셀 데이터를 담을 스테이징 버퍼를 생성하고 데이터를 복사합니다. +3. 최종 텍스처 이미지(`DEVICE_LOCAL`)를 생성합니다. +4. 이미지 레이아웃을 `UNDEFINED`에서 `TRANSFER_DST_OPTIMAL`로 전환합니다. +5. 스테이징 버퍼에서 텍스처 이미지로 데이터를 복사합니다. +6. 셰이더에서 읽을 수 있도록 레이아웃을 `TRANSFER_DST_OPTIMAL`에서 `SHADER_READ_ONLY_OPTIMAL`로 전환합니다. +7. 스테이징 버퍼와 그 메모리를 해제합니다. + +## 정리 + +애플리케이션이 종료될 때 텍스처 이미지와 메모리도 해제해야 합니다. `cleanup` 함수를 수정하세요. + +```rust +impl VulkanApp { + fn cleanup(&mut self) { + unsafe { + // ... + self.device.destroy_image(self.texture_image, None); + self.device.free_memory(self.texture_image_memory, None); + // ... + } + } } ``` -The image now contains the texture, but we still need a way to access it from -the graphics pipeline. We'll work on that in the next chapter. - -[C++ code](/code/24_texture_image.cpp) / -[Vertex shader](/code/22_shader_ubo.vert) / -[Fragment shader](/code/22_shader_ubo.frag) +이제 이미지가 텍스처를 포함하게 되었지만, 아직 그래픽 파이프라인에서 접근할 방법이 없습니다. 다음 장에서 이 부분을 다루겠습니다. \ No newline at end of file diff --git a/ko-rust/06_Texture_mapping/01_Image_view_and_sampler.md b/ko-rust/06_Texture_mapping/01_Image_view_and_sampler.md index 9d98c9e4..38024473 100644 --- a/ko-rust/06_Texture_mapping/01_Image_view_and_sampler.md +++ b/ko-rust/06_Texture_mapping/01_Image_view_and_sampler.md @@ -1,369 +1,365 @@ -In this chapter we're going to create two more resources that are needed for the -graphics pipeline to sample an image. The first resource is one that we've -already seen before while working with the swap chain images, but the second one -is new - it relates to how the shader will read texels from the image. +이번 장에서는 그래픽스 파이프라인이 이미지를 샘플링하는 데 필요한 두 가지 리소스를 더 만들 것입니다. 첫 번째 리소스는 스왑 체인 이미지에서 이미 다루었던 것이지만, 두 번째 리소스는 새로운 것으로 셰이더가 이미지에서 텍셀(texel)을 어떻게 읽을지와 관련이 있습니다. -## Texture image view +## 텍스처 이미지 뷰 -We've seen before, with the swap chain images and the framebuffer, that images -are accessed through image views rather than directly. We will also need to -create such an image view for the texture image. +우리는 이전에 스왑 체인 이미지와 프레임버퍼에서 이미지가 직접 접근되는 대신 이미지 뷰를 통해 접근된다는 것을 보았습니다. 텍스처 이미지에 대해서도 이러한 이미지 뷰를 만들어야 합니다. -Add a class member to hold a `VkImageView` for the texture image and create a -new function `createTextureImageView` where we'll create it: +텍스처 이미지의 `ash::vk::ImageView`를 저장할 구조체 필드를 추가하고, 이를 생성할 `create_texture_image_view` 메서드를 새로 만듭니다. -```c++ -VkImageView textureImageView; - -... - -void initVulkan() { - ... - createTextureImage(); - createTextureImageView(); - createVertexBuffer(); - ... +```rust +struct HelloTriangleApplication { + // ... + texture_image: vk::Image, + texture_image_memory: vk::DeviceMemory, + texture_image_view: vk::ImageView, + // ... } -... - -void createTextureImageView() { - -} -``` +impl HelloTriangleApplication { + pub fn new(window: &Window) -> Self { + // ... + } -The code for this function can be based directly on `createImageViews`. The only -two changes you have to make are the `format` and the `image`: - -```c++ -VkImageViewCreateInfo viewInfo{}; -viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; -viewInfo.image = textureImage; -viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; -viewInfo.format = VK_FORMAT_R8G8B8A8_SRGB; -viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; -viewInfo.subresourceRange.baseMipLevel = 0; -viewInfo.subresourceRange.levelCount = 1; -viewInfo.subresourceRange.baseArrayLayer = 0; -viewInfo.subresourceRange.layerCount = 1; -``` + fn init_vulkan(&mut self, window: &Window) { + // ... + self.create_texture_image(); + self.create_texture_image_view(); + self.create_vertex_buffer(); + // ... + } -I've left out the explicit `viewInfo.components` initialization, because -`VK_COMPONENT_SWIZZLE_IDENTITY` is defined as `0` anyway. Finish creating the -image view by calling `vkCreateImageView`: + // ... -```c++ -if (vkCreateImageView(device, &viewInfo, nullptr, &textureImageView) != VK_SUCCESS) { - throw std::runtime_error("failed to create texture image view!"); + fn create_texture_image_view(&mut self) { + // ... + } } ``` -Because so much of the logic is duplicated from `createImageViews`, you may wish -to abstract it into a new `createImageView` function: - -```c++ -VkImageView createImageView(VkImage image, VkFormat format) { - VkImageViewCreateInfo viewInfo{}; - viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; - viewInfo.image = image; - viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; - viewInfo.format = format; - viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; - viewInfo.subresourceRange.baseMipLevel = 0; - viewInfo.subresourceRange.levelCount = 1; - viewInfo.subresourceRange.baseArrayLayer = 0; - viewInfo.subresourceRange.layerCount = 1; - - VkImageView imageView; - if (vkCreateImageView(device, &viewInfo, nullptr, &imageView) != VK_SUCCESS) { - throw std::runtime_error("failed to create image view!"); +이 메서드의 코드는 `create_image_views` 메서드를 거의 그대로 가져와서 만들 수 있습니다. 변경해야 할 부분은 `format`과 `image` 단 두 가지뿐입니다. + +```rust +let view_info = vk::ImageViewCreateInfo::builder() + .image(self.texture_image) + .view_type(vk::ImageViewType::TYPE_2D) + .format(vk::Format::R8G8B8A8_SRGB) + .subresource_range( + vk::ImageSubresourceRange::builder() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .base_mip_level(0) + .level_count(1) + .base_array_layer(0) + .layer_count(1) + .build(), + ); + +self.texture_image_view = unsafe { + self.device + .create_image_view(&view_info, None) + .expect("Failed to create texture image view!") +}; +``` +C++ 버전에서 `viewInfo.components` 초기화를 생략한 것처럼, Rust의 빌더 패턴에서도 기본값은 `IDENTITY`이므로 명시적으로 설정할 필요가 없습니다. + +`create_image_views`와 많은 로직이 중복되므로, 이를 새로운 `create_image_view` 헬퍼 메서드로 추상화할 수 있습니다. + +```rust +impl HelloTriangleApplication { + // ... + fn create_image_view( + &self, + image: vk::Image, + format: vk::Format, + ) -> vk::ImageView { + let view_info = vk::ImageViewCreateInfo::builder() + .image(image) + .view_type(vk::ImageViewType::TYPE_2D) + .format(format) + .subresource_range( + vk::ImageSubresourceRange::builder() + .aspect_mask(vk::ImageAspectFlags::COLOR) + .base_mip_level(0) + .level_count(1) + .base_array_layer(0) + .layer_count(1) + .build(), + ); + + unsafe { + self.device + .create_image_view(&view_info, None) + .expect("Failed to create image view!") + } } - - return imageView; + // ... } ``` -The `createTextureImageView` function can now be simplified to: +이제 `create_texture_image_view` 메서드는 다음과 같이 단순화할 수 있습니다. -```c++ -void createTextureImageView() { - textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_SRGB); +```rust +fn create_texture_image_view(&mut self) { + self.texture_image_view = + self.create_image_view(self.texture_image, vk::Format::R8G8B8A8_SRGB); } ``` -And `createImageViews` can be simplified to: - -```c++ -void createImageViews() { - swapChainImageViews.resize(swapChainImages.size()); - - for (uint32_t i = 0; i < swapChainImages.size(); i++) { - swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat); - } +그리고 `create_image_views`도 다음과 같이 단순화됩니다. + +```rust +fn create_image_views(&mut self) { + self.swapchain_image_views = self + .swapchain_images + .iter() + .map(|&image| { + self.create_image_view(image, self.swapchain_image_format) + }) + .collect(); } ``` -Make sure to destroy the image view at the end of the program, right before -destroying the image itself: +프로그램이 끝날 때, 이미지 자체를 파괴하기 직전에 이미지 뷰를 파괴하도록 `cleanup` 메서드를 수정해야 합니다. -```c++ -void cleanup() { - cleanupSwapChain(); +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + self.cleanup_swapchain(); - vkDestroyImageView(device, textureImageView, nullptr); + self.device.destroy_image_view(self.texture_image_view, None); - vkDestroyImage(device, textureImage, nullptr); - vkFreeMemory(device, textureImageMemory, nullptr); + self.device.destroy_image(self.texture_image, None); + self.device.free_memory(self.texture_image_memory, None); + // ... + } + } +} ``` -## Samplers +## 샘플러 -It is possible for shaders to read texels directly from images, but that is not -very common when they are used as textures. Textures are usually accessed -through samplers, which will apply filtering and transformations to compute the -final color that is retrieved. +셰이더가 이미지에서 직접 텍셀을 읽는 것도 가능하지만, 이미지가 텍스처로 사용될 때는 흔한 방식이 아닙니다. 텍스처는 보통 샘플러를 통해 접근되며, 샘플러는 최종적으로 검색될 색상을 계산하기 위해 필터링과 변환을 적용합니다. -These filters are helpful to deal with problems like oversampling. Consider a -texture that is mapped to geometry with more fragments than texels. If you -simply took the closest texel for the texture coordinate in each fragment, then -you would get a result like the first image: +이러한 필터들은 오버샘플링(oversampling) 같은 문제를 해결하는 데 유용합니다. 텍셀보다 더 많은 프래그먼트가 있는 지오메트리에 텍스처가 매핑되는 경우를 생각해보세요. 만약 각 프래그먼트의 텍스처 좌표에 가장 가까운 텍셀을 단순히 가져온다면, 아래 첫 번째 이미지와 같은 결과를 얻게 될 것입니다. ![](/images/texture_filtering.png) -If you combined the 4 closest texels through linear interpolation, then you -would get a smoother result like the one on the right. Of course your -application may have art style requirements that fit the left style more (think -Minecraft), but the right is preferred in conventional graphics applications. A -sampler object automatically applies this filtering for you when reading a color -from the texture. +만약 가장 가까운 4개의 텍셀을 선형 보간(linear interpolation)으로 혼합한다면, 오른쪽 이미지처럼 더 부드러운 결과를 얻을 수 있습니다. 물론 애플리케이션의 아트 스타일에 따라 왼쪽 스타일(마인크래프트처럼)이 더 적합할 수도 있지만, 일반적인 그래픽스 애플리케이션에서는 오른쪽 방식이 선호됩니다. 샘플러 객체는 텍스처에서 색상을 읽을 때 이 필터링을 자동으로 적용해줍니다. -Undersampling is the opposite problem, where you have more texels than -fragments. This will lead to artifacts when sampling high frequency patterns -like a checkerboard texture at a sharp angle: +언더샘플링(undersampling)은 그 반대의 문제로, 프래그먼트보다 텍셀이 더 많은 경우입니다. 이는 체커보드 텍스처처럼 고주파 패턴을 예리한 각도에서 샘플링할 때 아티팩트를 유발합니다. ![](/images/anisotropic_filtering.png) -As shown in the left image, the texture turns into a blurry mess in the -distance. The solution to this is [anisotropic filtering](https://en.wikipedia.org/wiki/Anisotropic_filtering), -which can also be applied automatically by a sampler. +왼쪽 이미지에서 보듯이, 텍스처가 멀어질수록 흐릿한 덩어리로 변합니다. 이에 대한 해결책은 [비등방성 필터링(anisotropic filtering)](https://ko.wikipedia.org/wiki/%EB%B9%84%EB%93%B1%EB%B0%A9%EC%84%B1_%ED%95%84%ED%84%B0%EB%A7%81)이며, 이 또한 샘플러에 의해 자동으로 적용될 수 있습니다. -Aside from these filters, a sampler can also take care of transformations. It -determines what happens when you try to read texels outside the image through -its *addressing mode*. The image below displays some of the possibilities: +이러한 필터 외에도, 샘플러는 변환도 처리할 수 있습니다. 샘플러는 *주소 지정 모드(addressing mode)*를 통해 이미지 외부의 텍셀을 읽으려고 할 때 어떤 일이 일어날지를 결정합니다. 아래 이미지는 몇 가지 가능한 옵션을 보여줍니다. ![](/images/texture_addressing.png) -We will now create a function `createTextureSampler` to set up such a sampler -object. We'll be using that sampler to read colors from the texture in the -shader later on. - -```c++ -void initVulkan() { - ... - createTextureImage(); - createTextureImageView(); - createTextureSampler(); - ... -} +이제 이러한 샘플러 객체를 설정하기 위해 `create_texture_sampler` 메서드를 만들 것입니다. 나중에 셰이더에서 이 샘플러를 사용해 텍스처로부터 색상을 읽게 됩니다. -... +```rust +struct HelloTriangleApplication { + // ... + texture_image_view: vk::ImageView, + texture_sampler: vk::Sampler, + // ... +} -void createTextureSampler() { +// ... +fn init_vulkan(&mut self, window: &Window) { + // ... + self.create_texture_image(); + self.create_texture_image_view(); + self.create_texture_sampler(); + self.create_vertex_buffer(); + // ... +} +// ... +fn create_texture_sampler(&mut self) { + // ... } ``` -Samplers are configured through a `VkSamplerCreateInfo` structure, which -specifies all filters and transformations that it should apply. +샘플러는 `ash::vk::SamplerCreateInfo` 구조체를 통해 구성되며, 이 구조체는 샘플러가 적용해야 할 모든 필터와 변환을 명시합니다. Ash의 빌더 패턴을 사용하여 생성합니다. -```c++ -VkSamplerCreateInfo samplerInfo{}; -samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; -samplerInfo.magFilter = VK_FILTER_LINEAR; -samplerInfo.minFilter = VK_FILTER_LINEAR; +```rust +let sampler_info = vk::SamplerCreateInfo::builder() + .mag_filter(vk::Filter::LINEAR) + .min_filter(vk::Filter::LINEAR); ``` -The `magFilter` and `minFilter` fields specify how to interpolate texels that -are magnified or minified. Magnification concerns the oversampling problem -describes above, and minification concerns undersampling. The choices are -`VK_FILTER_NEAREST` and `VK_FILTER_LINEAR`, corresponding to the modes -demonstrated in the images above. +`mag_filter`와 `min_filter` 필드는 텍셀이 확대되거나 축소될 때 어떻게 보간할지를 지정합니다. 확대는 위에서 설명한 오버샘플링 문제와 관련이 있고, 축소는 언더샘플링 문제와 관련이 있습니다. 선택지는 `VK_FILTER_NEAREST`와 `VK_FILTER_LINEAR`이며, 이는 위 이미지에서 보여준 모드에 해당합니다. -```c++ -samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; -samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; -samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; +```rust +let sampler_info = vk::SamplerCreateInfo::builder() + .mag_filter(vk::Filter::LINEAR) + .min_filter(vk::Filter::LINEAR) + .address_mode_u(vk::SamplerAddressMode::REPEAT) + .address_mode_v(vk::SamplerAddressMode::REPEAT) + .address_mode_w(vk::SamplerAddressMode::REPEAT); ``` -The addressing mode can be specified per axis using the `addressMode` fields. -The available values are listed below. Most of these are demonstrated in the -image above. Note that the axes are called U, V and W instead of X, Y and Z. -This is a convention for texture space coordinates. - -* `VK_SAMPLER_ADDRESS_MODE_REPEAT`: Repeat the texture when going beyond the -image dimensions. -* `VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT`: Like repeat, but inverts the -coordinates to mirror the image when going beyond the dimensions. -* `VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE`: Take the color of the edge closest to -the coordinate beyond the image dimensions. -* `VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE`: Like clamp to edge, but -instead uses the edge opposite to the closest edge. -* `VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER`: Return a solid color when sampling -beyond the dimensions of the image. - -It doesn't really matter which addressing mode we use here, because we're not -going to sample outside of the image in this tutorial. However, the repeat mode -is probably the most common mode, because it can be used to tile textures like -floors and walls. - -```c++ -samplerInfo.anisotropyEnable = VK_TRUE; -samplerInfo.maxAnisotropy = ???; -``` +주소 지정 모드는 축별로 지정할 수 있습니다. 사용 가능한 값은 다음과 같습니다. 대부분은 위 이미지에서 시연되었습니다. 축이 X, Y, Z 대신 U, V, W로 불리는 점에 유의하세요. 이는 텍스처 공간 좌표의 관례입니다. -These two fields specify if anisotropic filtering should be used. There is no -reason not to use this unless performance is a concern. The `maxAnisotropy` -field limits the amount of texel samples that can be used to calculate the final -color. A lower value results in better performance, but lower quality results. -To figure out which value we can use, we need to retrieve the properties of the physical device like so: +* `vk::SamplerAddressMode::REPEAT`: 이미지 크기를 벗어날 때 텍스처를 반복합니다. +* `vk::SamplerAddressMode::MIRRORED_REPEAT`: 반복과 같지만, 크기를 벗어날 때 좌표를 반전시켜 이미지를 거울처럼 반사합니다. +* `vk::SamplerAddressMode::CLAMP_TO_EDGE`: 이미지 크기를 벗어나는 좌표에 대해 가장 가까운 가장자리의 색상을 사용합니다. +* `vk::SamplerAddressMode::MIRROR_CLAMP_TO_EDGE`: 가장자리 클램프와 비슷하지만, 가장 가까운 가장자리가 아닌 반대쪽 가장자리를 사용합니다. +* `vk::SamplerAddressMode::CLAMP_TO_BORDER`: 이미지 크기 밖을 샘플링할 때 지정된 단색을 반환합니다. -```c++ -VkPhysicalDeviceProperties properties{}; -vkGetPhysicalDeviceProperties(physicalDevice, &properties); -``` +이 튜토리얼에서는 이미지 외부를 샘플링하지 않을 것이므로 어떤 주소 지정 모드를 사용하든 큰 차이는 없습니다. 하지만 바닥이나 벽처럼 텍스처를 타일링하는 데 사용될 수 있기 때문에 반복 모드가 아마 가장 일반적일 것입니다. -If you look at the documentation for the `VkPhysicalDeviceProperties` structure, you'll see that it contains a `VkPhysicalDeviceLimits` member named `limits`. This struct in turn has a member called `maxSamplerAnisotropy` and this is the maximum value we can specify for `maxAnisotropy`. If we want to go for maximum quality, we can simply use that value directly: - -```c++ -samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy; +```rust +// ... + .anisotropy_enable(true) + .max_anisotropy(???) +// ... ``` -You can either query the properties at the beginning of your program and pass them around to the functions that need them, or query them in the `createTextureSampler` function itself. +이 두 필드는 비등방성 필터링을 사용할지 여부를 지정합니다. 성능이 우려되는 경우가 아니라면 사용하지 않을 이유가 없습니다. `max_anisotropy` 필드는 최종 색상을 계산하는 데 사용될 수 있는 텍셀 샘플의 양을 제한합니다. 값이 낮을수록 성능은 좋아지지만 결과물의 품질은 떨어집니다. 우리가 사용할 수 있는 값을 알아내려면, 다음과 같이 물리 장치의 속성을 가져와야 합니다. -```c++ -samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; +```rust +let properties = unsafe { self.instance.get_physical_device_properties(self.physical_device) }; ``` -The `borderColor` field specifies which color is returned when sampling beyond -the image with clamp to border addressing mode. It is possible to return black, -white or transparent in either float or int formats. You cannot specify an -arbitrary color. +`ash::vk::PhysicalDeviceProperties` 구조체를 보면 `limits`라는 `ash::vk::PhysicalDeviceLimits` 타입의 필드가 있습니다. 이 구조체는 다시 `max_sampler_anisotropy`라는 필드를 가지고 있으며, 이것이 `max_anisotropy`에 지정할 수 있는 최대값입니다. 최고의 품질을 원한다면 이 값을 직접 사용하면 됩니다. -```c++ -samplerInfo.unnormalizedCoordinates = VK_FALSE; +```rust +// ... + .max_anisotropy(properties.limits.max_sampler_anisotropy) +// ... ``` -The `unnormalizedCoordinates` field specifies which coordinate system you want -to use to address texels in an image. If this field is `VK_TRUE`, then you can -simply use coordinates within the `[0, texWidth)` and `[0, texHeight)` range. If -it is `VK_FALSE`, then the texels are addressed using the `[0, 1)` range on all -axes. Real-world applications almost always use normalized coordinates, because -then it's possible to use textures of varying resolutions with the exact same -coordinates. - -```c++ -samplerInfo.compareEnable = VK_FALSE; -samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS; -``` +`create_texture_sampler` 메서드 내에서 속성을 조회할 수 있습니다. -If a comparison function is enabled, then texels will first be compared to a -value, and the result of that comparison is used in filtering operations. This -is mainly used for [percentage-closer filtering](https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch11.html) -on shadow maps. We'll look at this in a future chapter. - -```c++ -samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; -samplerInfo.mipLodBias = 0.0f; -samplerInfo.minLod = 0.0f; -samplerInfo.maxLod = 0.0f; +```rust +// ... + .border_color(vk::BorderColor::INT_OPAQUE_BLACK) + .unnormalized_coordinates(false) +// ... ``` -All of these fields apply to mipmapping. We will look at mipmapping in a [later -chapter](/Generating_Mipmaps), but basically it's another type of filter that can be applied. - -The functioning of the sampler is now fully defined. Add a class member to -hold the handle of the sampler object and create the sampler with -`vkCreateSampler`: - -```c++ -VkImageView textureImageView; -VkSampler textureSampler; - -... +`border_color` 필드는 `clamp to border` 주소 지정 모드로 이미지 외부를 샘플링할 때 반환될 색상을 지정합니다. `unnormalized_coordinates` 필드는 텍셀 주소에 정규화된 좌표(`[0, 1)`)를 사용할지 여부를 지정합니다. 실제 애플리케이션에서는 거의 항상 정규화된 좌표를 사용합니다. -void createTextureSampler() { - ... - - if (vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler) != VK_SUCCESS) { - throw std::runtime_error("failed to create texture sampler!"); - } +```rust +// ... + .compare_enable(false) + .compare_op(vk::CompareOp::ALWAYS) +// ... +``` +비교 함수는 주로 섀도 맵에 사용되며, 여기서는 비활성화합니다. + +```rust +// ... + .mipmap_mode(vk::SamplerMipmapMode::LINEAR) + .mip_lod_bias(0.0) + .min_lod(0.0) + .max_lod(0.0); +``` +이 필드들은 모두 밉매핑에 적용됩니다. 밉매핑은 다음 장에서 다룰 것입니다. + +이제 샘플러의 모든 설정이 완료되었습니다. `vkCreateSampler`를 호출하여 샘플러를 생성합니다. + +```rust +fn create_texture_sampler(&mut self) { + let properties = unsafe { self.instance.get_physical_device_properties(self.physical_device) }; + + let sampler_info = vk::SamplerCreateInfo::builder() + .mag_filter(vk::Filter::LINEAR) + .min_filter(vk::Filter::LINEAR) + .address_mode_u(vk::SamplerAddressMode::REPEAT) + .address_mode_v(vk::SamplerAddressMode::REPEAT) + .address_mode_w(vk::SamplerAddressMode::REPEAT) + .anisotropy_enable(true) + .max_anisotropy(properties.limits.max_sampler_anisotropy) + .border_color(vk::BorderColor::INT_OPAQUE_BLACK) + .unnormalized_coordinates(false) + .compare_enable(false) + .compare_op(vk::CompareOp::ALWAYS) + .mipmap_mode(vk::SamplerMipmapMode::LINEAR) + .mip_lod_bias(0.0) + .min_lod(0.0) + .max_lod(0.0); + + self.texture_sampler = unsafe { + self.device + .create_sampler(&sampler_info, None) + .expect("Failed to create texture sampler!") + }; } ``` -Note the sampler does not reference a `VkImage` anywhere. The sampler is a -distinct object that provides an interface to extract colors from a texture. It -can be applied to any image you want, whether it is 1D, 2D or 3D. This is -different from many older APIs, which combined texture images and filtering into -a single state. +샘플러는 어디에도 `vk::Image`를 참조하지 않는다는 점에 유의하세요. 샘플러는 텍스처에서 색상을 추출하는 인터페이스를 제공하는 별개의 객체입니다. -Destroy the sampler at the end of the program when we'll no longer be accessing -the image: +프로그램이 종료될 때 샘플러를 파괴하도록 `drop` 구현을 수정합니다. -```c++ -void cleanup() { - cleanupSwapChain(); +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + self.cleanup_swapchain(); - vkDestroySampler(device, textureSampler, nullptr); - vkDestroyImageView(device, textureImageView, nullptr); + self.device.destroy_sampler(self.texture_sampler, None); + self.device.destroy_image_view(self.texture_image_view, None); + self.device.destroy_image(self.texture_image, None); + self.device.free_memory(self.texture_image_memory, None); - ... + // ... + } + } } ``` -## Anisotropy device feature +## 비등방성 장치 기능 -If you run your program right now, you'll see a validation layer message like -this: +지금 프로그램을 실행하면 다음과 같은 검증 레이어 메시지를 볼 수 있습니다. ![](/images/validation_layer_anisotropy.png) -That's because anisotropic filtering is actually an optional device feature. We -need to update the `createLogicalDevice` function to request it: +이는 비등방성 필터링이 사실 선택적(optional) 장치 기능이기 때문입니다. 이를 요청하도록 `create_logical_device` 메서드를 업데이트해야 합니다. -```c++ -VkPhysicalDeviceFeatures deviceFeatures{}; -deviceFeatures.samplerAnisotropy = VK_TRUE; -``` +```rust +// in create_logical_device +let mut features = vk::PhysicalDeviceFeatures::builder(); +features.sampler_anisotropy = vk::TRUE; -And even though it is very unlikely that a modern graphics card will not support -it, we should update `isDeviceSuitable` to check if it is available: +// ... +let create_info = vk::DeviceCreateInfo::builder() + .queue_create_infos(&queue_create_infos) + .enabled_extension_names(&device_extensions_raw) + .enabled_features(&features); +``` -```c++ -bool isDeviceSuitable(VkPhysicalDevice device) { - ... +그리고 최신 그래픽 카드가 이를 지원하지 않을 가능성은 매우 낮지만, `is_device_suitable` 함수를 업데이트하여 사용 가능한지 확인해야 합니다. - VkPhysicalDeviceFeatures supportedFeatures; - vkGetPhysicalDeviceFeatures(device, &supportedFeatures); +```rust +// in is_device_suitable +let supported_features = unsafe { instance.get_physical_device_features(device) }; - return indices.isComplete() && extensionsSupported && swapChainAdequate && supportedFeatures.samplerAnisotropy; -} +// ... +indices.is_complete() + && extensions_supported + && swapchain_adequate + && supported_features.sampler_anisotropy == vk::TRUE ``` -The `vkGetPhysicalDeviceFeatures` repurposes the `VkPhysicalDeviceFeatures` -struct to indicate which features are supported rather than requested by setting -the boolean values. +`get_physical_device_features`는 `ash::vk::PhysicalDeviceFeatures` 구조체를 사용하여 지원되는 기능을 나타냅니다. -Instead of enforcing the availability of anisotropic filtering, it's also -possible to simply not use it by conditionally setting: +비등방성 필터링의 사용 가능성을 강제하는 대신, 조건부로 사용하지 않도록 설정할 수도 있습니다. -```c++ -samplerInfo.anisotropyEnable = VK_FALSE; -samplerInfo.maxAnisotropy = 1.0f; +```rust +// ... + .anisotropy_enable(false) + .max_anisotropy(1.0) +// ... ``` -In the next chapter we will expose the image and sampler objects to the shaders -to draw the texture onto the square. +다음 장에서는 이미지와 샘플러 객체를 셰이더에 노출하여 사각형에 텍스처를 그릴 것입니다. + +(참고: 코드 링크는 원본 C++ 튜토리얼을 가리킵니다.) -[C++ code](/code/25_sampler.cpp) / -[Vertex shader](/code/22_shader_ubo.vert) / -[Fragment shader](/code/22_shader_ubo.frag) +[C++ 코드](/code/25_sampler.cpp) / +[정점 셰이더](/code/22_shader_ubo.vert) / +[프래그먼트 셰이더](/code/22_shader_ubo.frag) \ No newline at end of file diff --git a/ko-rust/06_Texture_mapping/02_Combined_image_sampler.md b/ko-rust/06_Texture_mapping/02_Combined_image_sampler.md index 0f1e5496..e16c6e29 100644 --- a/ko-rust/06_Texture_mapping/02_Combined_image_sampler.md +++ b/ko-rust/06_Texture_mapping/02_Combined_image_sampler.md @@ -1,202 +1,186 @@ -## Introduction - -We looked at descriptors for the first time in the uniform buffers part of the -tutorial. In this chapter we will look at a new type of descriptor: *combined -image sampler*. This descriptor makes it possible for shaders to access an image -resource through a sampler object like the one we created in the previous -chapter. - -We'll start by modifying the descriptor set layout, descriptor pool and descriptor -set to include such a combined image sampler descriptor. After that, we're going -to add texture coordinates to `Vertex` and modify the fragment shader to read -colors from the texture instead of just interpolating the vertex colors. - -## Updating the descriptors - -Browse to the `createDescriptorSetLayout` function and add a -`VkDescriptorSetLayoutBinding` for a combined image sampler descriptor. We'll -simply put it in the binding after the uniform buffer: - -```c++ -VkDescriptorSetLayoutBinding samplerLayoutBinding{}; -samplerLayoutBinding.binding = 1; -samplerLayoutBinding.descriptorCount = 1; -samplerLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; -samplerLayoutBinding.pImmutableSamplers = nullptr; -samplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; - -std::array bindings = {uboLayoutBinding, samplerLayoutBinding}; -VkDescriptorSetLayoutCreateInfo layoutInfo{}; -layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; -layoutInfo.bindingCount = static_cast(bindings.size()); -layoutInfo.pBindings = bindings.data(); -``` +## 소개 -Make sure to set the `stageFlags` to indicate that we intend to use the combined -image sampler descriptor in the fragment shader. That's where the color of the -fragment is going to be determined. It is possible to use texture sampling in -the vertex shader, for example to dynamically deform a grid of vertices by a -[heightmap](https://en.wikipedia.org/wiki/Heightmap). - -We must also create a larger descriptor pool to make room for the allocation -of the combined image sampler by adding another `VkPoolSize` of type -`VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER` to the -`VkDescriptorPoolCreateInfo`. Go to the `createDescriptorPool` function and -modify it to include a `VkDescriptorPoolSize` for this descriptor: - -```c++ -std::array poolSizes{}; -poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; -poolSizes[0].descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); -poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; -poolSizes[1].descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); - -VkDescriptorPoolCreateInfo poolInfo{}; -poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; -poolInfo.poolSizeCount = static_cast(poolSizes.size()); -poolInfo.pPoolSizes = poolSizes.data(); -poolInfo.maxSets = static_cast(MAX_FRAMES_IN_FLIGHT); -``` +우리는 유니폼 버퍼(uniform buffers) 파트에서 처음으로 디스크립터(descriptor)를 살펴보았습니다. 이번 챕터에서는 새로운 유형의 디스크립터인 **결합 이미지 샘플러(combined image sampler)**에 대해 알아보겠습니다. 이 디스크립터를 사용하면 셰이더가 이전 챕터에서 생성한 것과 같은 샘플러 객체를 통해 이미지 리소스에 접근할 수 있습니다. -Inadequate descriptor pools are a good example of a problem that the validation -layers will not catch: As of Vulkan 1.1, `vkAllocateDescriptorSets` may fail -with the error code `VK_ERROR_POOL_OUT_OF_MEMORY` if the pool is not -sufficiently large, but the driver may also try to solve the problem internally. -This means that sometimes (depending on hardware, pool size and allocation size) -the driver will let us get away with an allocation that exceeds the limits of -our descriptor pool. Other times, `vkAllocateDescriptorSets` will fail and -return `VK_ERROR_POOL_OUT_OF_MEMORY`. This can be particularly frustrating if -the allocation succeeds on some machines, but fails on others. - -Since Vulkan shifts the responsiblity for the allocation to the driver, it is no -longer a strict requirement to only allocate as many descriptors of a certain -type (`VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER`, etc.) as specified by the -corresponding `descriptorCount` members for the creation of the descriptor pool. -However, it remains best practise to do so, and in the future, -`VK_LAYER_KHRONOS_validation` will warn about this type of problem if you enable -[Best Practice Validation](https://vulkan.lunarg.com/doc/view/1.4.304.0/linux/best_practices.html). - -The final step is to bind the actual image and sampler resources to the -descriptors in the descriptor set. Go to the `createDescriptorSets` function. - -```c++ -for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { - VkDescriptorBufferInfo bufferInfo{}; - bufferInfo.buffer = uniformBuffers[i]; - bufferInfo.offset = 0; - bufferInfo.range = sizeof(UniformBufferObject); - - VkDescriptorImageInfo imageInfo{}; - imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; - imageInfo.imageView = textureImageView; - imageInfo.sampler = textureSampler; - - ... -} +우선 디스크립터 셋 레이아웃, 디스크립터 풀, 디스크립터 셋을 수정하여 결합 이미지 샘플러 디스크립터를 포함하는 것부터 시작하겠습니다. 그 후, `Vertex`에 텍스처 좌표를 추가하고 프래그먼트 셰이더를 수정하여 단순히 정점 색상을 보간하는 대신 텍스처에서 색상을 읽도록 할 것입니다. + +*이 문서는 기존 Rust 및 `ash`로 작성된 Vulkan 애플리케이션 구조를 따른다고 가정합니다.* + +## 디스크립터 업데이트하기 + +`create_descriptor_set_layout` 함수로 가서 결합 이미지 샘플러 디스크립터를 위한 `vk::DescriptorSetLayoutBinding`을 추가합니다. 단순히 유니폼 버퍼 다음 바인딩에 추가하겠습니다. `ash`의 빌더 패턴을 사용하면 코드가 더 명확해집니다. + +```rust +let ubo_layout_binding = vk::DescriptorSetLayoutBinding::builder() + .binding(0) + .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) + .descriptor_count(1) + .stage_flags(vk::ShaderStageFlags::VERTEX); + +let sampler_layout_binding = vk::DescriptorSetLayoutBinding::builder() + .binding(1) + .descriptor_count(1) + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .p_immutable_samplers(&[]) + .stage_flags(vk::ShaderStageFlags::FRAGMENT); + +let bindings = [ubo_layout_binding.build(), sampler_layout_binding.build()]; +let layout_info = vk::DescriptorSetLayoutCreateInfo::builder() + .bindings(&bindings); + +self.descriptor_set_layout = unsafe { + device.create_descriptor_set_layout(&layout_info, None) +}?; ``` -The resources for a combined image sampler structure must be specified in a -`VkDescriptorImageInfo` struct, just like the buffer resource for a uniform -buffer descriptor is specified in a `VkDescriptorBufferInfo` struct. This is -where the objects from the previous chapter come together. - -```c++ -std::array descriptorWrites{}; - -descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; -descriptorWrites[0].dstSet = descriptorSets[i]; -descriptorWrites[0].dstBinding = 0; -descriptorWrites[0].dstArrayElement = 0; -descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; -descriptorWrites[0].descriptorCount = 1; -descriptorWrites[0].pBufferInfo = &bufferInfo; - -descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; -descriptorWrites[1].dstSet = descriptorSets[i]; -descriptorWrites[1].dstBinding = 1; -descriptorWrites[1].dstArrayElement = 0; -descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; -descriptorWrites[1].descriptorCount = 1; -descriptorWrites[1].pImageInfo = &imageInfo; - -vkUpdateDescriptorSets(device, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); +`stage_flags`를 `vk::ShaderStageFlags::FRAGMENT`로 설정하여 프래그먼트 셰이더에서 결합 이미지 샘플러 디스크립터를 사용하려는 의도를 나타내야 합니다. 프래그먼트의 색상이 결정되는 곳이 바로 여기입니다. 버텍스 셰이더에서 텍스처 샘플링을 사용하는 것도 가능합니다. 예를 들어, [하이트맵(heightmap)](https://en.wikipedia.org/wiki/Heightmap)을 사용하여 정점 그리드를 동적으로 변형시킬 수 있습니다. + +또한 `vk::DescriptorType::COMBINED_IMAGE_SAMPLER` 타입의 풀 크기를 추가하여 결합 이미지 샘플러 할당을 위한 공간을 만들기 위해 더 큰 디스크립터 풀을 생성해야 합니다. `create_descriptor_pool` 함수로 가서 이 디스크립터를 위한 `vk::DescriptorPoolSize`를 포함하도록 수정합니다. + +```rust +let pool_sizes = [ + vk::DescriptorPoolSize { + ty: vk::DescriptorType::UNIFORM_BUFFER, + descriptor_count: MAX_FRAMES_IN_FLIGHT as u32, + }, + vk::DescriptorPoolSize { + ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER, + descriptor_count: MAX_FRAMES_IN_FLIGHT as u32, + }, +]; + +let pool_info = vk::DescriptorPoolCreateInfo::builder() + .pool_sizes(&pool_sizes) + .max_sets(MAX_FRAMES_IN_FLIGHT as u32); + +self.descriptor_pool = unsafe { + device.create_descriptor_pool(&pool_info, None) +}?; ``` -The descriptors must be updated with this image info, just like the buffer. This -time we're using the `pImageInfo` array instead of `pBufferInfo`. The descriptors -are now ready to be used by the shaders! +부적절한 디스크립터 풀은 검증 레이어가 잡아내지 못하는 문제의 좋은 예입니다. Vulkan 1.1부터, 풀이 충분히 크지 않으면 `vkAllocateDescriptorSets`가 `VK_ERROR_POOL_OUT_OF_MEMORY` 오류 코드로 실패할 수 있지만, 드라이버가 내부적으로 이 문제를 해결하려고 시도할 수도 있습니다. 이는 때때로 (하드웨어, 풀 크기, 할당 크기에 따라) 드라이버가 디스크립터 풀의 한도를 초과하는 할당을 허용할 수 있음을 의미합니다. 다른 경우에는 `vkAllocateDescriptorSets`가 실패하고 `VK_ERROR_POOL_OUT_OF_MEMORY`를 반환합니다. 이는 일부 머신에서는 할당이 성공하고 다른 머신에서는 실패할 경우 특히 좌절스러울 수 있습니다. -## Texture coordinates +Vulkan은 할당에 대한 책임을 드라이버에게 넘기므로, 디스크립터 풀 생성 시 해당 `descriptor_count` 멤버로 지정된 만큼만 특정 유형의 디스크립터를 할당하는 것이 더 이상 엄격한 요구 사항은 아닙니다. 하지만, 여전히 그렇게 하는 것이 모범 사례로 남아 있습니다. -There is one important ingredient for texture mapping that is still missing, and -that's the actual texture coordinates for each vertex. The texture coordinates determine how the -image is actually mapped to the geometry. +마지막 단계는 실제 이미지와 샘플러 리소스를 디스크립터 셋의 디스크립터에 바인딩하는 것입니다. `create_descriptor_sets` 함수로 가세요. -```c++ -struct Vertex { - glm::vec2 pos; - glm::vec3 color; - glm::vec2 texCoord; +```rust +for i in 0..MAX_FRAMES_IN_FLIGHT { + let buffer_info = [vk::DescriptorBufferInfo { + buffer: self.uniform_buffers[i], + offset: 0, + range: std::mem::size_of::() as u64, + }]; - static VkVertexInputBindingDescription getBindingDescription() { - VkVertexInputBindingDescription bindingDescription{}; - bindingDescription.binding = 0; - bindingDescription.stride = sizeof(Vertex); - bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + let image_info = [vk::DescriptorImageInfo { + image_layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + image_view: self.texture_image_view, + sampler: self.texture_sampler, + }]; - return bindingDescription; - } + // ... +} +``` + +결합 이미지 샘플러 구조를 위한 리소스는 유니폼 버퍼 디스크립터의 버퍼 리소스가 `vk::DescriptorBufferInfo` 구조체에 지정되는 것과 마찬가지로, `vk::DescriptorImageInfo` 구조체에 지정되어야 합니다. 여기서 이전 챕터의 객체들이 함께 사용됩니다. + +```rust +let descriptor_writes = [ + vk::WriteDescriptorSet::builder() + .dst_set(self.descriptor_sets[i]) + .dst_binding(0) + .dst_array_element(0) + .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER) + .buffer_info(&buffer_info) + .build(), + vk::WriteDescriptorSet::builder() + .dst_set(self.descriptor_sets[i]) + .dst_binding(1) + .dst_array_element(0) + .descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER) + .image_info(&image_info) + .build(), +]; + +unsafe { + device.update_descriptor_sets(&descriptor_writes, &[]); +} +``` + +디스크립터는 이 이미지 정보로 업데이트되어야 합니다. 이번에는 `buffer_info` 슬라이스 대신 `image_info` 슬라이스를 사용합니다. 이제 디스크립터는 셰이더에서 사용할 준비가 되었습니다! + +## 텍스처 좌표 - static std::array getAttributeDescriptions() { - std::array attributeDescriptions{}; +텍스처 매핑에 있어 아직 빠진 중요한 요소가 하나 있는데, 바로 각 정점에 대한 실제 텍스처 좌표입니다. 텍스처 좌표는 이미지가 지오메트리에 실제로 어떻게 매핑될지 결정합니다. - attributeDescriptions[0].binding = 0; - attributeDescriptions[0].location = 0; - attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; - attributeDescriptions[0].offset = offsetof(Vertex, pos); +```rust +// glam이나 다른 수학 라이브러리를 사용한다고 가정합니다. +use glam::{Vec2, Vec3}; +// C++의 offsetof와 같은 기능을 위해 memoffset 크레이트가 필요합니다. +// Cargo.toml에 memoffset = "0.9" 를 추가하세요. +use memoffset::offset_of; - attributeDescriptions[1].binding = 0; - attributeDescriptions[1].location = 1; - attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; - attributeDescriptions[1].offset = offsetof(Vertex, color); +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct Vertex { + pos: Vec2, + color: Vec3, + tex_coord: Vec2, +} - attributeDescriptions[2].binding = 0; - attributeDescriptions[2].location = 2; - attributeDescriptions[2].format = VK_FORMAT_R32G32_SFLOAT; - attributeDescriptions[2].offset = offsetof(Vertex, texCoord); +impl Vertex { + fn get_binding_description() -> vk::VertexInputBindingDescription { + vk::VertexInputBindingDescription { + binding: 0, + stride: std::mem::size_of::() as u32, + input_rate: vk::VertexInputRate::VERTEX, + } + } - return attributeDescriptions; + fn get_attribute_descriptions() -> [vk::VertexInputAttributeDescription; 3] { + [ + vk::VertexInputAttributeDescription { + binding: 0, + location: 0, + format: vk::Format::R32G32_SFLOAT, + offset: offset_of!(Self, pos) as u32, + }, + vk::VertexInputAttributeDescription { + binding: 0, + location: 1, + format: vk::Format::R32G32B32_SFLOAT, + offset: offset_of!(Self, color) as u32, + }, + vk::VertexInputAttributeDescription { + binding: 0, + location: 2, + format: vk::Format::R32G32_SFLOAT, + offset: offset_of!(Self, tex_coord) as u32, + }, + ] } -}; +} ``` -Modify the `Vertex` struct to include a `vec2` for texture coordinates. Make -sure to also add a `VkVertexInputAttributeDescription` so that we can use access -texture coordinates as input in the vertex shader. That is necessary to be able -to pass them to the fragment shader for interpolation across the surface of the -square. - -```c++ -const std::vector vertices = { - {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}}, - {{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}}, - {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}}, - {{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}} -}; +`Vertex` 구조체를 수정하여 텍스처 좌표를 위한 `Vec2` 타입의 `tex_coord` 필드를 포함시킵니다. 또한 버텍스 셰이더에서 텍스처 좌표를 입력으로 접근할 수 있도록 `vk::VertexInputAttributeDescription`을 추가해야 합니다. 이는 사각형 표면 전체에 걸쳐 보간을 위해 프래그먼트 셰이더로 전달하는 데 필요합니다. Rust에서는 멤버의 오프셋을 얻기 위해 `memoffset` 크레이트의 `offset_of!` 매크로를 사용하는 것이 일반적입니다. + +```rust +const VERTICES: [Vertex; 4] = [ + Vertex { pos: Vec2::new(-0.5, -0.5), color: Vec3::new(1.0, 0.0, 0.0), tex_coord: Vec2::new(1.0, 0.0) }, + Vertex { pos: Vec2::new(0.5, -0.5), color: Vec3::new(0.0, 1.0, 0.0), tex_coord: Vec2::new(0.0, 0.0) }, + Vertex { pos: Vec2::new(0.5, 0.5), color: Vec3::new(0.0, 0.0, 1.0), tex_coord: Vec2::new(0.0, 1.0) }, + Vertex { pos: Vec2::new(-0.5, 0.5), color: Vec3::new(1.0, 1.0, 1.0), tex_coord: Vec2::new(1.0, 1.0) }, +]; ``` -In this tutorial, I will simply fill the square with the texture by using -coordinates from `0, 0` in the top-left corner to `1, 1` in the bottom-right -corner. Feel free to experiment with different coordinates. Try using -coordinates below `0` or above `1` to see the addressing modes in action! +이 튜토리얼에서는 왼쪽 위 모서리의 `(0, 0)`에서 오른쪽 아래 모서리의 `(1, 1)`까지의 좌표를 사용하여 텍스처로 사각형을 채울 것입니다. 자유롭게 다른 좌표로 실험해보세요. `0` 미만 또는 `1` 초과의 좌표를 사용하여 주소 지정 모드가 실제로 어떻게 작동하는지 확인해보세요! -## Shaders +## 셰이더 -The final step is modifying the shaders to sample colors from the texture. We -first need to modify the vertex shader to pass through the texture coordinates -to the fragment shader: +마지막 단계는 셰이더를 수정하여 텍스처에서 색상을 샘플링하는 것입니다. 먼저 버텍스 셰이더를 수정하여 텍스처 좌표를 프래그먼트 셰이더로 전달해야 합니다. ```glsl +// vertex shader layout(location = 0) in vec2 inPosition; layout(location = 1) in vec3 inColor; layout(location = 2) in vec2 inTexCoord; @@ -211,11 +195,10 @@ void main() { } ``` -Just like the per vertex colors, the `fragTexCoord` values will be smoothly -interpolated across the area of the square by the rasterizer. We can visualize -this by having the fragment shader output the texture coordinates as colors: +정점별 색상과 마찬가지로 `fragTexCoord` 값은 래스터라이저에 의해 사각형 영역 전체에 걸쳐 부드럽게 보간됩니다. 프래그먼트 셰이더가 텍스처 좌표를 색상으로 출력하게 하여 이를 시각화할 수 있습니다. ```glsl +// fragment shader #version 450 layout(location = 0) in vec3 fragColor; @@ -228,45 +211,36 @@ void main() { } ``` -You should see something like the image below. Don't forget to recompile the -shaders! +셰이더를 다시 컴파일하는 것을 잊지 마세요! 아래와 같은 이미지를 보게 될 것입니다. ![](/images/texcoord_visualization.png) -The green channel represents the horizontal coordinates and the red channel the -vertical coordinates. The black and yellow corners confirm that the texture -coordinates are correctly interpolated from `0, 0` to `1, 1` across the square. -Visualizing data using colors is the shader programming equivalent of `printf` -debugging, for lack of a better option! +녹색 채널은 수평 좌표를, 적색 채널은 수직 좌표를 나타냅니다. 검은색과 노란색 모서리는 텍스처 좌표가 사각형에 걸쳐 `0, 0`에서 `1, 1`까지 올바르게 보간되었음을 확인시켜 줍니다. 색상을 사용한 데이터 시각화는 셰이더 프로그래밍에서 더 나은 대안이 없을 때 사용하는 `println!` 디버깅과 같습니다! -A combined image sampler descriptor is represented in GLSL by a sampler uniform. -Add a reference to it in the fragment shader: +결합 이미지 샘플러 디스크립터는 GLSL에서 샘플러 유니폼으로 표현됩니다. 프래그먼트 셰이더에 이에 대한 참조를 추가하세요. ```glsl +// fragment shader layout(binding = 1) uniform sampler2D texSampler; ``` -There are equivalent `sampler1D` and `sampler3D` types for other types of -images. Make sure to use the correct binding here. +다른 유형의 이미지를 위한 `sampler1D` 및 `sampler3D`와 같은 타입도 있습니다. 여기서 올바른 바인딩(`binding = 1`)을 사용해야 합니다. ```glsl +// fragment shader main() void main() { outColor = texture(texSampler, fragTexCoord); } ``` -Textures are sampled using the built-in `texture` function. It takes a `sampler` -and coordinate as arguments. The sampler automatically takes care of the -filtering and transformations in the background. You should now see the texture -on the square when you run the application: +텍스처는 내장 함수 `texture`를 사용하여 샘플링됩니다. 이 함수는 `sampler`와 좌표를 인수로 받습니다. 샘플러는 백그라운드에서 필터링과 변환을 자동으로 처리합니다. 이제 애플리케이션을 실행하면 사각형 위에 텍스처가 표시될 것입니다. ![](/images/texture_on_square.png) -Try experimenting with the addressing modes by scaling the texture coordinates -to values higher than `1`. For example, the following fragment shader produces -the result in the image below when using `VK_SAMPLER_ADDRESS_MODE_REPEAT`: +텍스처 좌표를 `1`보다 큰 값으로 조정하여 주소 지정 모드를 실험해보세요. 예를 들어, 다음 프래그먼트 셰이더는 `VK_SAMPLER_ADDRESS_MODE_REPEAT`를 사용할 때 아래 이미지와 같은 결과를 생성합니다. ```glsl +// fragment shader main() void main() { outColor = texture(texSampler, fragTexCoord * 2.0); } @@ -274,23 +248,17 @@ void main() { ![](/images/texture_on_square_repeated.png) -You can also manipulate the texture colors using the vertex colors: +정점 색상을 사용하여 텍스처 색상을 조작할 수도 있습니다. ```glsl +// fragment shader main() void main() { outColor = vec4(fragColor * texture(texSampler, fragTexCoord).rgb, 1.0); } ``` -I've separated the RGB and alpha channels here to not scale the alpha channel. +알파 채널이 변하지 않도록 RGB와 알파 채널을 분리했습니다. ![](/images/texture_on_square_colorized.png) -You now know how to access images in shaders! This is a very powerful technique -when combined with images that are also written to in framebuffers. You can use -these images as inputs to implement cool effects like post-processing and camera -displays within the 3D world. - -[C++ code](/code/26_texture_mapping.cpp) / -[Vertex shader](/code/26_shader_textures.vert) / -[Fragment shader](/code/26_shader_textures.frag) +이제 셰이더에서 이미지에 접근하는 방법을 알게 되었습니다! 이 기술은 프레임버퍼에 쓰여지기도 하는 이미지와 결합될 때 매우 강력합니다. 이러한 이미지를 입력으로 사용하여 후처리(post-processing)나 3D 세계 내 카메라 디스플레이와 같은 멋진 효과를 구현할 수 있습니다. \ No newline at end of file diff --git a/ko/04_Vertex_buffers/00_Vertex_input_description.md b/ko/04_Vertex_buffers/00_Vertex_input_description.md index e7da3e4f..69cdefcd 100644 --- a/ko/04_Vertex_buffers/00_Vertex_input_description.md +++ b/ko/04_Vertex_buffers/00_Vertex_input_description.md @@ -1,16 +1,10 @@ -## Introduction +## 소개 -In the next few chapters, we're going to replace the hardcoded vertex data in -the vertex shader with a vertex buffer in memory. We'll start with the easiest -approach of creating a CPU visible buffer and using `memcpy` to copy the vertex -data into it directly, and after that we'll see how to use a staging buffer to -copy the vertex data to high performance memory. +다음 몇 개의 챕터에서는 버텍스 셰이더에 하드코딩된 정점 데이터를 메모리의 버텍스 버퍼로 교체할 것입니다. 가장 쉬운 접근법으로 시작하여, CPU에서 볼 수 있는(visible) 버퍼를 만들고 `memcpy`를 사용해 정점 데이터를 직접 복사하는 방법을 알아볼 것입니다. 그 후에는 스테이징 버퍼(staging buffer)를 사용해 정점 데이터를 고성능 메모리로 복사하는 방법도 살펴볼 것입니다. -## Vertex shader +## 버텍스 셰이더 -First change the vertex shader to no longer include the vertex data in the -shader code itself. The vertex shader takes input from a vertex buffer using the -`in` keyword. +먼저 버텍스 셰이더를 변경하여, 셰이더 코드 자체에 더 이상 정점 데이터를 포함하지 않도록 합니다. 버텍스 셰이더는 `in` 키워드를 사용하여 버텍스 버퍼로부터 입력을 받습니다. ```glsl #version 450 @@ -26,36 +20,26 @@ void main() { } ``` -The `inPosition` and `inColor` variables are *vertex attributes*. They're -properties that are specified per-vertex in the vertex buffer, just like we -manually specified a position and color per vertex using the two arrays. Make -sure to recompile the vertex shader! +`inPosition`과 `inColor` 변수는 **정점 속성(vertex attributes)**입니다. 이들은 우리가 이전에 두 배열을 사용해 수동으로 위치와 색상을 지정했던 것처럼, 버텍스 버퍼에서 정점 단위로 지정되는 속성입니다. 버텍스 셰이더를 다시 컴파일하는 것을 잊지 마세요! -Just like `fragColor`, the `layout(location = x)` annotations assign indices to -the inputs that we can later use to reference them. It is important to know that -some types, like `dvec3` 64 bit vectors, use multiple *slots*. That means that -the index after it must be at least 2 higher: +`fragColor`와 마찬가지로, `layout(location = x)` 어노테이션은 나중에 참조할 수 있도록 입력에 인덱스를 할당합니다. 64비트 벡터인 `dvec3` 같은 일부 타입은 여러 개의 **슬롯(slot)**을 사용한다는 점을 아는 것이 중요합니다. 즉, 그 다음의 인덱스는 최소 2 이상 커야 합니다. ```glsl layout(location = 0) in dvec3 inPosition; layout(location = 2) in vec3 inColor; ``` -You can find more info about the layout qualifier in the [OpenGL wiki](https://www.khronos.org/opengl/wiki/Layout_Qualifier_(GLSL)). +레이아웃 한정자(layout qualifier)에 대한 더 많은 정보는 [OpenGL 위키](https://www.khronos.org/opengl/wiki/Layout_Qualifier_(GLSL))에서 찾을 수 있습니다. -## Vertex data +## 정점 데이터 -We're moving the vertex data from the shader code to an array in the code of our -program. Start by including the GLM library, which provides us with linear -algebra related types like vectors and matrices. We're going to use these types -to specify the position and color vectors. +이제 정점 데이터를 셰이더 코드에서 우리 프로그램 코드의 배열로 옮길 것입니다. 먼저 벡터나 행렬 같은 선형대수 관련 타입을 제공하는 GLM 라이브러리를 포함합니다. 이 타입들을 사용하여 위치와 색상 벡터를 지정할 것입니다. ```c++ #include ``` -Create a new structure called `Vertex` with the two attributes that we're going -to use in the vertex shader inside it: +`Vertex`라는 새로운 구조체를 만들고, 그 안에 버텍스 셰이더에서 사용할 두 속성을 넣습니다. ```c++ struct Vertex { @@ -64,8 +48,7 @@ struct Vertex { }; ``` -GLM conveniently provides us with C++ types that exactly match the vector types -used in the shader language. +GLM은 셰이더 언어에서 사용되는 벡터 타입과 정확히 일치하는 C++ 타입을 편리하게 제공합니다. ```c++ const std::vector vertices = { @@ -75,18 +58,13 @@ const std::vector vertices = { }; ``` -Now use the `Vertex` structure to specify an array of vertex data. We're using -exactly the same position and color values as before, but now they're combined -into one array of vertices. This is known as *interleaving* vertex attributes. +이제 `Vertex` 구조체를 사용해 정점 데이터의 배열을 지정합니다. 이전과 정확히 같은 위치와 색상 값을 사용하지만, 이제는 하나의 정점 배열로 결합되었습니다. 이를 **인터리빙(interleaving)** 정점 속성이라고 합니다. -## Binding descriptions +## 바인딩 서술 (Binding descriptions) -The next step is to tell Vulkan how to pass this data format to the vertex -shader once it's been uploaded into GPU memory. There are two types of -structures needed to convey this information. +다음 단계는 이 데이터 포맷이 GPU 메모리에 업로드된 후, 버텍스 셰이더로 어떻게 전달될지를 Vulkan에게 알려주는 것입니다. 이 정보를 전달하기 위해서는 두 가지 종류의 구조체가 필요합니다. -The first structure is `VkVertexInputBindingDescription` and we'll add a member -function to the `Vertex` struct to populate it with the right data. +첫 번째 구조체는 `VkVertexInputBindingDescription`이며, `Vertex` 구조체에 멤버 함수를 추가하여 올바른 데이터로 채우도록 할 것입니다. ```c++ struct Vertex { @@ -101,9 +79,7 @@ struct Vertex { }; ``` -A vertex binding describes at which rate to load data from memory throughout the -vertices. It specifies the number of bytes between data entries and whether to -move to the next data entry after each vertex or after each instance. +정점 바인딩(vertex binding)은 정점들 전체에서 메모리로부터 데이터를 어떤 속도(rate)로 로드할지 서술합니다. 이는 데이터 항목 사이의 바이트 수와 각 정점 또는 각 인스턴스 이후에 다음 데이터 항목으로 이동할지 여부를 지정합니다. ```c++ VkVertexInputBindingDescription bindingDescription{}; @@ -112,23 +88,16 @@ bindingDescription.stride = sizeof(Vertex); bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; ``` -All of our per-vertex data is packed together in one array, so we're only going -to have one binding. The `binding` parameter specifies the index of the binding -in the array of bindings. The `stride` parameter specifies the number of bytes -from one entry to the next, and the `inputRate` parameter can have one of the -following values: +우리의 모든 정점별 데이터는 하나의 배열에 함께 묶여 있으므로, 우리는 하나의 바인딩만 가질 것입니다. `binding` 파라미터는 바인딩 배열에서의 인덱스를 지정합니다. `stride` 파라미터는 한 항목에서 다음 항목까지의 바이트 수를 지정하며, `inputRate` 파라미터는 다음 값 중 하나를 가질 수 있습니다. -* `VK_VERTEX_INPUT_RATE_VERTEX`: Move to the next data entry after each vertex -* `VK_VERTEX_INPUT_RATE_INSTANCE`: Move to the next data entry after each -instance +* `VK_VERTEX_INPUT_RATE_VERTEX`: 각 정점마다 다음 데이터 항목으로 이동 +* `VK_VERTEX_INPUT_RATE_INSTANCE`: 각 인스턴스마다 다음 데이터 항목으로 이동 -We're not going to use instanced rendering, so we'll stick to per-vertex data. +우리는 인스턴스 렌더링을 사용하지 않을 것이므로, 정점별(per-vertex) 데이터를 고수할 것입니다. -## Attribute descriptions +## 속성 서술 (Attribute descriptions) -The second structure that describes how to handle vertex input is -`VkVertexInputAttributeDescription`. We're going to add another helper function -to `Vertex` to fill in these structs. +정점 입력을 처리하는 방법을 서술하는 두 번째 구조체는 `VkVertexInputAttributeDescription`입니다. 이 구조체들을 채우기 위해 `Vertex`에 또 다른 헬퍼 함수를 추가할 것입니다. ```c++ #include @@ -142,11 +111,7 @@ static std::array getAttributeDescriptions } ``` -As the function prototype indicates, there are going to be two of these -structures. An attribute description struct describes how to extract a vertex -attribute from a chunk of vertex data originating from a binding description. We -have two attributes, position and color, so we need two attribute description -structs. +함수 프로토타입에서 알 수 있듯이, 이 구조체는 두 개가 될 것입니다. 속성 서술(attribute description) 구조체는 바인딩 서술에서 비롯된 정점 데이터 덩어리(chunk)로부터 어떻게 정점 속성을 추출할지를 서술합니다. 우리는 위치와 색상이라는 두 가지 속성을 가지고 있으므로, 두 개의 속성 서술 구조체가 필요합니다. ```c++ attributeDescriptions[0].binding = 0; @@ -155,39 +120,22 @@ attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; attributeDescriptions[0].offset = offsetof(Vertex, pos); ``` -The `binding` parameter tells Vulkan from which binding the per-vertex data -comes. The `location` parameter references the `location` directive of the -input in the vertex shader. The input in the vertex shader with location `0` is -the position, which has two 32-bit float components. - -The `format` parameter describes the type of data for the attribute. A bit -confusingly, the formats are specified using the same enumeration as color -formats. The following shader types and formats are commonly used together: - -* `float`: `VK_FORMAT_R32_SFLOAT` -* `vec2`: `VK_FORMAT_R32G32_SFLOAT` -* `vec3`: `VK_FORMAT_R32G32B32_SFLOAT` -* `vec4`: `VK_FORMAT_R32G32B32A32_SFLOAT` - -As you can see, you should use the format where the amount of color channels -matches the number of components in the shader data type. It is allowed to use -more channels than the number of components in the shader, but they will be -silently discarded. If the number of channels is lower than the number of -components, then the BGA components will use default values of `(0, 0, 1)`. The -color type (`SFLOAT`, `UINT`, `SINT`) and bit width should also match the type -of the shader input. See the following examples: - -* `ivec2`: `VK_FORMAT_R32G32_SINT`, a 2-component vector of 32-bit signed -integers -* `uvec4`: `VK_FORMAT_R32G32B32A32_UINT`, a 4-component vector of 32-bit -unsigned integers -* `double`: `VK_FORMAT_R64_SFLOAT`, a double-precision (64-bit) float - -The `format` parameter implicitly defines the byte size of attribute data and -the `offset` parameter specifies the number of bytes since the start of the -per-vertex data to read from. The binding is loading one `Vertex` at a time and -the position attribute (`pos`) is at an offset of `0` bytes from the beginning -of this struct. This is automatically calculated using the `offsetof` macro. +`binding` 파라미터는 정점별 데이터가 어느 바인딩에서 오는지 Vulkan에게 알려줍니다. `location` 파라미터는 버텍스 셰이더의 입력에 있는 `location` 지시어를 참조합니다. `location`이 `0`인 버텍스 셰이더의 입력은 위치(position)이며, 이는 2개의 32비트 부동소수점 컴포넌트를 가집니다. + +`format` 파라미터는 속성의 데이터 타입을 서술합니다. 조금 혼란스러울 수 있지만, 포맷은 색상 포맷과 동일한 열거형(enumeration)으로 지정됩니다. 다음 셰이더 타입과 포맷은 일반적으로 함께 사용됩니다: + +* `float`: `VK_FORMAT_R32_SFLOAT` +* `vec2`: `VK_FORMAT_R32G32_SFLOAT` +* `vec3`: `VK_FORMAT_R32G32B32_SFLOAT` +* `vec4`: `VK_FORMAT_R32G32B32A32_SFLOAT` + +보시다시피, 색상 채널의 수가 셰이더 데이터 타입의 컴포넌트 수와 일치하는 포맷을 사용해야 합니다. 셰이더의 컴포넌트 수보다 더 많은 채널을 사용하는 것은 허용되지만, 초과된 채널은 조용히 무시됩니다. 채널 수가 컴포넌트 수보다 적으면, BGA 컴포넌트는 기본값인 `(0, 0, 1)`을 사용하게 됩니다. 색상 타입(`SFLOAT`, `UINT`, `SINT`)과 비트 폭 또한 셰이더 입력의 타입과 일치해야 합니다. 다음 예시를 보세요: + +* `ivec2`: `VK_FORMAT_R32G32_SINT`, 2-컴포넌트 32비트 부호 있는 정수 벡터 +* `uvec4`: `VK_FORMAT_R32G32B32A32_UINT`, 4-컴포넌트 32비트 부호 없는 정수 벡터 +* `double`: `VK_FORMAT_R64_SFLOAT`, 배정밀도(64비트) 부동소수점 + +`format` 파라미터는 속성 데이터의 바이트 크기를 암시적으로 정의하며, `offset` 파라미터는 정점별 데이터의 시작 부분으로부터 몇 바이트를 읽어야 하는지 지정합니다. 바인딩은 한 번에 하나의 `Vertex`를 로드하며, 위치 속성(`pos`)은 이 구조체의 시작으로부터 `0` 바이트 오프셋에 있습니다. 이는 `offsetof` 매크로를 사용해 자동으로 계산됩니다. ```c++ attributeDescriptions[1].binding = 0; @@ -196,13 +144,11 @@ attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; attributeDescriptions[1].offset = offsetof(Vertex, color); ``` -The color attribute is described in much the same way. +색상 속성도 거의 같은 방식으로 서술됩니다. -## Pipeline vertex input +## 파이프라인 정점 입력 -We now need to set up the graphics pipeline to accept vertex data in this format -by referencing the structures in `createGraphicsPipeline`. Find the -`vertexInputInfo` struct and modify it to reference the two descriptions: +이제 `createGraphicsPipeline`에서 구조체들을 참조하여, 이 포맷의 정점 데이터를 받도록 그래픽 파이프라인을 설정해야 합니다. `vertexInputInfo` 구조체를 찾아 두 서술을 참조하도록 수정합니다: ```c++ auto bindingDescription = Vertex::getBindingDescription(); @@ -214,12 +160,8 @@ vertexInputInfo.pVertexBindingDescriptions = &bindingDescription; vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data(); ``` -The pipeline is now ready to accept vertex data in the format of the `vertices` -container and pass it on to our vertex shader. If you run the program now with -validation layers enabled, you'll see that it complains that there is no vertex -buffer bound to the binding. The next step is to create a vertex buffer and move -the vertex data to it so the GPU is able to access it. +이제 파이프라인은 `vertices` 컨테이너 포맷의 정점 데이터를 받아들여 우리 버텍스 셰이더로 전달할 준비가 되었습니다. 만약 지금 검증 레이어를 활성화한 상태로 프로그램을 실행하면, 바인딩에 연결된 버텍스 버퍼가 없다고 불평하는 것을 볼 수 있을 것입니다. 다음 단계는 버텍스 버퍼를 생성하고 정점 데이터를 그곳으로 옮겨 GPU가 접근할 수 있도록 하는 것입니다. -[C++ code](/code/18_vertex_input.cpp) / -[Vertex shader](/code/18_shader_vertexbuffer.vert) / -[Fragment shader](/code/18_shader_vertexbuffer.frag) +[C++ 코드](/code/18_vertex_input.cpp) / +[버텍스 셰이더](/code/18_shader_vertexbuffer.vert) / +[프래그먼트 셰이더](/code/18_shader_vertexbuffer.frag) \ No newline at end of file diff --git a/ko/04_Vertex_buffers/01_Vertex_buffer_creation.md b/ko/04_Vertex_buffers/01_Vertex_buffer_creation.md index 77122c50..6afd2c9b 100644 --- a/ko/04_Vertex_buffers/01_Vertex_buffer_creation.md +++ b/ko/04_Vertex_buffers/01_Vertex_buffer_creation.md @@ -1,17 +1,10 @@ -## Introduction +## 소개 -Buffers in Vulkan are regions of memory used for storing arbitrary data that can -be read by the graphics card. They can be used to store vertex data, which we'll -do in this chapter, but they can also be used for many other purposes that we'll -explore in future chapters. Unlike the Vulkan objects we've been dealing with so -far, buffers do not automatically allocate memory for themselves. The work from -the previous chapters has shown that the Vulkan API puts the programmer in -control of almost everything and memory management is one of those things. +Vulkan에서 버퍼(Buffer)는 그래픽 카드가 읽을 수 있는 임의의 데이터를 저장하는 데 사용되는 메모리 영역입니다. 이번 장에서 다룰 정점 데이터(vertex data)를 저장하는 데 사용할 수도 있지만, 앞으로의 장에서 살펴볼 다른 많은 목적으로도 사용될 수 있습니다. 지금까지 다뤄온 다른 Vulkan 객체들과는 달리, 버퍼는 스스로 메모리를 할당하지 않습니다. 이전 장들에서 보았듯이 Vulkan API는 프로그래머가 거의 모든 것을 직접 제어하도록 하며, 메모리 관리도 그중 하나입니다. -## Buffer creation +## 버퍼 생성 -Create a new function `createVertexBuffer` and call it from `initVulkan` right -before `createCommandBuffers`. +`createVertexBuffer`라는 새 함수를 만들고, `initVulkan` 함수에서 `createCommandBuffers` 바로 전에 호출하도록 합시다. ```c++ void initVulkan() { @@ -38,7 +31,7 @@ void createVertexBuffer() { } ``` -Creating a buffer requires us to fill a `VkBufferCreateInfo` structure. +버퍼를 생성하려면 `VkBufferCreateInfo` 구조체를 채워야 합니다. ```c++ VkBufferCreateInfo bufferInfo{}; @@ -46,32 +39,23 @@ bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufferInfo.size = sizeof(vertices[0]) * vertices.size(); ``` -The first field of the struct is `size`, which specifies the size of the buffer -in bytes. Calculating the byte size of the vertex data is straightforward with -`sizeof`. +구조체의 첫 번째 필드는 `size`로, 버퍼의 크기를 바이트 단위로 지정합니다. 정점 데이터의 바이트 크기는 `sizeof`를 사용해 간단하게 계산할 수 있습니다. ```c++ bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; ``` -The second field is `usage`, which indicates for which purposes the data in the -buffer is going to be used. It is possible to specify multiple purposes using a -bitwise or. Our use case will be a vertex buffer, we'll look at other types of -usage in future chapters. +두 번째 필드는 `usage`로, 버퍼에 있는 데이터가 어떤 목적으로 사용될지를 나타냅니다. 비트 OR 연산을 사용하여 여러 목적을 동시에 지정할 수도 있습니다. 우리의 사용 사례는 정점 버퍼이므로, 이 플래그를 사용합니다. 다른 사용 유형은 향후 장에서 살펴보겠습니다. ```c++ bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; ``` -Just like the images in the swap chain, buffers can also be owned by a specific -queue family or be shared between multiple at the same time. The buffer will -only be used from the graphics queue, so we can stick to exclusive access. +스왑 체인의 이미지처럼, 버퍼도 특정 큐 패밀리가 소유하거나 여러 큐 패밀리 간에 공유될 수 있습니다. 이 버퍼는 그래픽스 큐에서만 사용할 것이므로, 배타적(exclusive) 접근 방식이면 충분합니다. -The `flags` parameter is used to configure sparse buffer memory, which is not -relevant right now. We'll leave it at the default value of `0`. +`flags` 매개변수는 희소 버퍼 메모리(sparse buffer memory)를 설정하는 데 사용되며, 지금은 중요하지 않습니다. 기본값인 `0`으로 두겠습니다. -We can now create the buffer with `vkCreateBuffer`. Define a class member to -hold the buffer handle and call it `vertexBuffer`. +이제 `vkCreateBuffer`를 사용해 버퍼를 생성할 수 있습니다. 버퍼 핸들을 저장할 클래스 멤버 `vertexBuffer`를 정의하고 함수를 호출합니다. ```c++ VkBuffer vertexBuffer; @@ -91,9 +75,7 @@ void createVertexBuffer() { } ``` -The buffer should be available for use in rendering commands until the end of -the program and it does not depend on the swap chain, so we'll clean it up in -the original `cleanup` function: +버퍼는 프로그램이 끝날 때까지 렌더링 명령어에서 사용할 수 있어야 하며, 스왑 체인에 종속되지 않으므로 원래의 `cleanup` 함수에서 정리합니다. ```c++ void cleanup() { @@ -105,32 +87,22 @@ void cleanup() { } ``` -## Memory requirements +## 메모리 요구사항 -The buffer has been created, but it doesn't actually have any memory assigned to -it yet. The first step of allocating memory for the buffer is to query its -memory requirements using the aptly named `vkGetBufferMemoryRequirements` -function. +버퍼는 생성되었지만 아직 메모리가 할당되지 않았습니다. 버퍼에 메모리를 할당하는 첫 단계는 이름 그대로인 `vkGetBufferMemoryRequirements` 함수를 사용하여 메모리 요구사항을 쿼리하는 것입니다. ```c++ VkMemoryRequirements memRequirements; vkGetBufferMemoryRequirements(device, vertexBuffer, &memRequirements); ``` -The `VkMemoryRequirements` struct has three fields: +`VkMemoryRequirements` 구조체에는 세 가지 필드가 있습니다. -* `size`: The size of the required amount of memory in bytes, may differ from -`bufferInfo.size`. -* `alignment`: The offset in bytes where the buffer begins in the allocated -region of memory, depends on `bufferInfo.usage` and `bufferInfo.flags`. -* `memoryTypeBits`: Bit field of the memory types that are suitable for the -buffer. +* `size`: 필요한 메모리의 크기(바이트 단위)이며, `bufferInfo.size`와 다를 수 있습니다. +* `alignment`: 할당된 메모리 영역 내에서 버퍼가 시작되는 오프셋(offset)을 바이트 단위로 나타냅니다. `bufferInfo.usage`와 `bufferInfo.flags`에 따라 달라집니다. +* `memoryTypeBits`: 버퍼에 적합한 메모리 타입들의 비트 필드입니다. -Graphics cards can offer different types of memory to allocate from. Each type -of memory varies in terms of allowed operations and performance characteristics. -We need to combine the requirements of the buffer and our own application -requirements to find the right type of memory to use. Let's create a new -function `findMemoryType` for this purpose. +그래픽 카드는 할당할 수 있는 여러 다른 종류의 메모리를 제공합니다. 각 메모리 타입은 허용되는 연산과 성능 특성 면에서 다릅니다. 우리는 버퍼의 요구사항과 우리 애플리케이션의 요구사항을 결합하여 사용할 올바른 메모리 타입을 찾아야 합니다. 이를 위해 `findMemoryType`이라는 새 함수를 만들어 봅시다. ```c++ uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) { @@ -138,22 +110,16 @@ uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) { } ``` -First we need to query info about the available types of memory using -`vkGetPhysicalDeviceMemoryProperties`. +먼저 `vkGetPhysicalDeviceMemoryProperties`를 사용하여 사용 가능한 메모리 타입에 대한 정보를 쿼리해야 합니다. ```c++ VkPhysicalDeviceMemoryProperties memProperties; vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties); ``` -The `VkPhysicalDeviceMemoryProperties` structure has two arrays `memoryTypes` -and `memoryHeaps`. Memory heaps are distinct memory resources like dedicated -VRAM and swap space in RAM for when VRAM runs out. The different types of memory -exist within these heaps. Right now we'll only concern ourselves with the type -of memory and not the heap it comes from, but you can imagine that this can -affect performance. +`VkPhysicalDeviceMemoryProperties` 구조체에는 `memoryTypes`와 `memoryHeaps` 두 개의 배열이 있습니다. 메모리 힙(Memory heap)은 전용 VRAM이나 VRAM이 부족할 때 사용되는 RAM의 스왑 공간과 같은 개별적인 메모리 자원입니다. 다양한 종류의 메모리 타입이 이 힙들 안에 존재합니다. 지금은 메모리 타입 자체에만 신경 쓰고 힙은 신경 쓰지 않겠지만, 이 선택이 성능에 영향을 미칠 수 있다는 점은 상상할 수 있을 것입니다. -Let's first find a memory type that is suitable for the buffer itself: +먼저 버퍼 자체에 적합한 메모리 타입을 찾아봅시다. ```c++ for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { @@ -165,21 +131,11 @@ for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { throw std::runtime_error("failed to find suitable memory type!"); ``` -The `typeFilter` parameter will be used to specify the bit field of memory types -that are suitable. That means that we can find the index of a suitable memory -type by simply iterating over them and checking if the corresponding bit is set -to `1`. +`typeFilter` 매개변수는 적합한 메모리 타입들의 비트 필드를 지정하는 데 사용됩니다. 즉, 단순히 모든 메모리 타입을 순회하며 해당하는 비트가 `1`로 설정되어 있는지 확인하면 적합한 메모리 타입의 인덱스를 찾을 수 있습니다. -However, we're not just interested in a memory type that is suitable for the -vertex buffer. We also need to be able to write our vertex data to that memory. -The `memoryTypes` array consists of `VkMemoryType` structs that specify the heap -and properties of each type of memory. The properties define special features -of the memory, like being able to map it so we can write to it from the CPU. -This property is indicated with `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT`, but we -also need to use the `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` property. We'll see -why when we map the memory. +하지만 우리는 정점 버퍼에 적합한 메모리 타입에만 관심 있는 것이 아닙니다. 또한 그 메모리에 정점 데이터를 쓸 수 있어야 합니다. `memoryTypes` 배열은 각 메모리 타입의 힙과 속성을 지정하는 `VkMemoryType` 구조체로 구성됩니다. 이 속성들은 메모리의 특별한 기능들을 정의하는데, 예를 들어 CPU에서 메모리에 쓰기 위해 맵핑(map)할 수 있는 기능이 있습니다. 이 속성은 `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT`으로 표시되며, `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` 속성도 필요합니다. 메모리를 맵핑할 때 그 이유를 알게 될 것입니다. -We can now modify the loop to also check for the support of this property: +이제 이 속성들의 지원 여부도 확인하도록 루프를 수정할 수 있습니다. ```c++ for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { @@ -189,16 +145,11 @@ for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { } ``` -We may have more than one desirable property, so we should check if the result -of the bitwise AND is not just non-zero, but equal to the desired properties bit -field. If there is a memory type suitable for the buffer that also has all of -the properties we need, then we return its index, otherwise we throw an -exception. +우리가 원하는 속성이 여러 개일 수 있으므로, 비트 AND 연산의 결과가 0이 아닌지 확인하는 것뿐만 아니라, 원하는 속성 비트 필드와 정확히 일치하는지 확인해야 합니다. 버퍼에 적합하면서 우리가 필요로 하는 모든 속성을 가진 메모리 타입이 있다면 그 인덱스를 반환하고, 그렇지 않으면 예외를 던집니다. -## Memory allocation +## 메모리 할당 -We now have a way to determine the right memory type, so we can actually -allocate the memory by filling in the `VkMemoryAllocateInfo` structure. +이제 올바른 메모리 타입을 결정할 방법을 알았으니, `VkMemoryAllocateInfo` 구조체를 채워 실제로 메모리를 할당할 수 있습니다. ```c++ VkMemoryAllocateInfo allocInfo{}; @@ -207,10 +158,7 @@ allocInfo.allocationSize = memRequirements.size; allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); ``` -Memory allocation is now as simple as specifying the size and type, both of -which are derived from the memory requirements of the vertex buffer and the -desired property. Create a class member to store the handle to the memory and -allocate it with `vkAllocateMemory`. +이제 메모리 할당은 크기와 타입을 지정하는 것만큼 간단해졌습니다. 두 값 모두 정점 버퍼의 메모리 요구사항과 원하는 속성으로부터 파생됩니다. 메모리 핸들을 저장할 클래스 멤버를 만들고 `vkAllocateMemory`로 할당합니다. ```c++ VkBuffer vertexBuffer; @@ -223,22 +171,15 @@ if (vkAllocateMemory(device, &allocInfo, nullptr, &vertexBufferMemory) != VK_SUC } ``` -If memory allocation was successful, then we can now associate this memory with -the buffer using `vkBindBufferMemory`: +메모리 할당에 성공했다면, 이제 `vkBindBufferMemory`를 사용하여 이 메모리를 버퍼와 연결할 수 있습니다. ```c++ vkBindBufferMemory(device, vertexBuffer, vertexBufferMemory, 0); ``` -The first three parameters are self-explanatory and the fourth parameter is the -offset within the region of memory. Since this memory is allocated specifically -for this the vertex buffer, the offset is simply `0`. If the offset is non-zero, -then it is required to be divisible by `memRequirements.alignment`. +처음 세 매개변수는 이름에서 알 수 있듯이 명확하며, 네 번째 매개변수는 메모리 영역 내의 오프셋입니다. 이 메모리는 이 정점 버퍼를 위해 특별히 할당되었으므로, 오프셋은 간단히 `0`입니다. 만약 오프셋이 0이 아니라면, `memRequirements.alignment`로 나누어떨어져야 합니다. -Of course, just like dynamic memory allocation in C++, the memory should be -freed at some point. Memory that is bound to a buffer object may be freed once -the buffer is no longer used, so let's free it after the buffer has been -destroyed: +물론, C++의 동적 메모리 할당처럼, 이 메모리도 언젠가는 해제되어야 합니다. 버퍼 객체에 바인딩된 메모리는 버퍼가 더 이상 사용되지 않을 때 해제할 수 있으므로, 버퍼가 파괴된 후에 해제하도록 합시다. ```c++ void cleanup() { @@ -248,24 +189,16 @@ void cleanup() { vkFreeMemory(device, vertexBufferMemory, nullptr); ``` -## Filling the vertex buffer +## 정점 버퍼 채우기 -It is now time to copy the vertex data to the buffer. This is done by [mapping -the buffer memory](https://en.wikipedia.org/wiki/Memory-mapped_I/O) into CPU -accessible memory with `vkMapMemory`. +이제 정점 데이터를 버퍼에 복사할 차례입니다. 이는 `vkMapMemory`를 사용하여 버퍼 메모리를 CPU에서 접근 가능한 메모리로 맵핑(mapping)하여 수행됩니다. ```c++ void* data; vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data); ``` -This function allows us to access a region of the specified memory resource -defined by an offset and size. The offset and size here are `0` and -`bufferInfo.size`, respectively. It is also possible to specify the special -value `VK_WHOLE_SIZE` to map all of the memory. The second to last parameter can -be used to specify flags, but there aren't any available yet in the current API. -It must be set to the value `0`. The last parameter specifies the output for the -pointer to the mapped memory. +이 함수를 사용하면 오프셋과 크기로 정의된 지정된 메모리 자원의 영역에 접근할 수 있습니다. 여기서 오프셋과 크기는 각각 `0`과 `bufferInfo.size`입니다. 모든 메모리를 맵핑하기 위해 특별한 값 `VK_WHOLE_SIZE`를 지정할 수도 있습니다. 뒤에서 두 번째 매개변수는 플래그를 지정하는 데 사용될 수 있지만, 현재 API에는 아직 사용 가능한 플래그가 없습니다. 반드시 `0`으로 설정해야 합니다. 마지막 매개변수는 맵핑된 메모리에 대한 포인터의 출력을 지정합니다. ```c++ void* data; @@ -274,28 +207,18 @@ vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data); vkUnmapMemory(device, vertexBufferMemory); ``` -You can now simply `memcpy` the vertex data to the mapped memory and unmap it -again using `vkUnmapMemory`. Unfortunately the driver may not immediately copy -the data into the buffer memory, for example because of caching. It is also -possible that writes to the buffer are not visible in the mapped memory yet. -There are two ways to deal with that problem: +이제 `memcpy`를 사용하여 정점 데이터를 맵핑된 메모리에 복사하고, `vkUnmapMemory`를 사용하여 다시 언맵핑하면 됩니다. 안타깝게도 드라이버가 데이터를 버퍼 메모리로 즉시 복사하지 않을 수 있습니다 (예: 캐싱 때문). 또한 버퍼에 대한 쓰기가 맵핑된 메모리에서 아직 보이지 않을 수도 있습니다. 이 문제를 해결하는 두 가지 방법이 있습니다. -* Use a memory heap that is host coherent, indicated with -`VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` -* Call `vkFlushMappedMemoryRanges` after writing to the mapped memory, and -call `vkInvalidateMappedMemoryRanges` before reading from the mapped memory +* `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT`로 표시된 호스트 일관성(host coherent) 메모리 힙을 사용합니다. +* 맵핑된 메모리에 쓴 후 `vkFlushMappedMemoryRanges`를 호출하고, 맵핑된 메모리에서 읽기 전에 `vkInvalidateMappedMemoryRanges`를 호출합니다. -We went for the first approach, which ensures that the mapped memory always -matches the contents of the allocated memory. Do keep in mind that this may lead -to slightly worse performance than explicit flushing, but we'll see why that -doesn't matter in the next chapter. +우리는 첫 번째 접근 방식, 즉 맵핑된 메모리가 항상 할당된 메모리의 내용과 일치하도록 보장하는 방식을 선택했습니다. 이 방식이 명시적인 플러싱(flushing)보다 성능이 약간 저하될 수 있지만, 다음 장에서 왜 이것이 중요하지 않은지 알게 될 것입니다. -Flushing memory ranges or using a coherent memory heap means that the driver will be aware of our writes to the buffer, but it doesn't mean that they are actually visible on the GPU yet. The transfer of data to the GPU is an operation that happens in the background and the specification simply [tells us](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-submission-host-writes) that it is guaranteed to be complete as of the next call to `vkQueueSubmit`. +메모리 범위를 플러싱하거나 일관성 있는 메모리 힙을 사용하는 것은 드라이버가 우리의 버퍼 쓰기를 인지한다는 것을 의미하지만, 이것이 GPU에서 실제로 보인다는 것을 의미하지는 않습니다. GPU로의 데이터 전송은 백그라운드에서 발생하는 작업이며, 사양에서는 단순히 [다음 `vkQueueSubmit` 호출 시점에 완료되는 것이 보장된다](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-submission-host-writes)고 명시합니다. -## Binding the vertex buffer +## 정점 버퍼 바인딩 -All that remains now is binding the vertex buffer during rendering operations. -We're going to extend the `recordCommandBuffer` function to do that. +이제 남은 일은 렌더링 작업 중에 정점 버퍼를 바인딩하는 것뿐입니다. 이를 위해 `recordCommandBuffer` 함수를 확장하겠습니다. ```c++ vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); @@ -307,20 +230,13 @@ vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets); vkCmdDraw(commandBuffer, static_cast(vertices.size()), 1, 0, 0); ``` -The `vkCmdBindVertexBuffers` function is used to bind vertex buffers to -bindings, like the one we set up in the previous chapter. The first two -parameters, besides the command buffer, specify the offset and number of -bindings we're going to specify vertex buffers for. The last two parameters -specify the array of vertex buffers to bind and the byte offsets to start -reading vertex data from. You should also change the call to `vkCmdDraw` to pass -the number of vertices in the buffer as opposed to the hardcoded number `3`. +`vkCmdBindVertexBuffers` 함수는 이전 장에서 설정한 것과 같은 바인딩에 정점 버퍼를 바인딩하는 데 사용됩니다. 명령어 버퍼 다음의 첫 두 매개변수는 우리가 정점 버퍼를 지정할 바인딩의 시작 오프셋과 개수를 지정합니다. 마지막 두 매개변수는 바인딩할 정점 버퍼의 배열과 정점 데이터를 읽기 시작할 바이트 오프셋을 지정합니다. 또한 `vkCmdDraw` 호출을 수정하여 하드코딩된 숫자 `3` 대신 버퍼에 있는 정점의 수를 전달하도록 해야 합니다. -Now run the program and you should see the familiar triangle again: +이제 프로그램을 실행하면 익숙한 삼각형이 다시 나타날 것입니다. ![](/images/triangle.png) -Try changing the color of the top vertex to white by modifying the `vertices` -array: +`vertices` 배열을 수정하여 맨 위 정점의 색상을 흰색으로 변경해 보세요. ```c++ const std::vector vertices = { @@ -330,13 +246,12 @@ const std::vector vertices = { }; ``` -Run the program again and you should see the following: +프로그램을 다시 실행하면 다음과 같이 보일 것입니다. ![](/images/triangle_white.png) -In the next chapter we'll look at a different way to copy vertex data to a -vertex buffer that results in better performance, but takes some more work. +다음 장에서는 더 나은 성능을 제공하지만 약간의 추가 작업이 필요한, 정점 데이터를 정점 버퍼로 복사하는 다른 방법을 살펴보겠습니다. -[C++ code](/code/19_vertex_buffer.cpp) / -[Vertex shader](/code/18_shader_vertexbuffer.vert) / -[Fragment shader](/code/18_shader_vertexbuffer.frag) +[C++ 코드](/code/19_vertex_buffer.cpp) / +[정점 셰이더](/code/18_shader_vertexbuffer.vert) / +[프래그먼트 셰이더](/code/18_shader_vertexbuffer.frag) \ No newline at end of file diff --git a/ko/04_Vertex_buffers/02_Staging_buffer.md b/ko/04_Vertex_buffers/02_Staging_buffer.md index 289e74d4..3c02bf27 100644 --- a/ko/04_Vertex_buffers/02_Staging_buffer.md +++ b/ko/04_Vertex_buffers/02_Staging_buffer.md @@ -1,47 +1,24 @@ -## Introduction - -The vertex buffer we have right now works correctly, but the memory type that -allows us to access it from the CPU may not be the most optimal memory type for -the graphics card itself to read from. The most optimal memory has the -`VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT` flag and is usually not accessible by the -CPU on dedicated graphics cards. In this chapter we're going to create two -vertex buffers. One *staging buffer* in CPU accessible memory to upload the data -from the vertex array to, and the final vertex buffer in device local memory. -We'll then use a buffer copy command to move the data from the staging buffer to -the actual vertex buffer. - -## Transfer queue - -The buffer copy command requires a queue family that supports transfer -operations, which is indicated using `VK_QUEUE_TRANSFER_BIT`. The good news is -that any queue family with `VK_QUEUE_GRAPHICS_BIT` or `VK_QUEUE_COMPUTE_BIT` -capabilities already implicitly support `VK_QUEUE_TRANSFER_BIT` operations. The -implementation is not required to explicitly list it in `queueFlags` in those -cases. - -If you like a challenge, then you can still try to use a different queue family -specifically for transfer operations. It will require you to make the following -modifications to your program: - -* Modify `QueueFamilyIndices` and `findQueueFamilies` to explicitly look for a -queue family with the `VK_QUEUE_TRANSFER_BIT` bit, but not the -`VK_QUEUE_GRAPHICS_BIT`. -* Modify `createLogicalDevice` to request a handle to the transfer queue -* Create a second command pool for command buffers that are submitted on the -transfer queue family -* Change the `sharingMode` of resources to be `VK_SHARING_MODE_CONCURRENT` and -specify both the graphics and transfer queue families -* Submit any transfer commands like `vkCmdCopyBuffer` (which we'll be using in -this chapter) to the transfer queue instead of the graphics queue - -It's a bit of work, but it'll teach you a lot about how resources are shared -between queue families. - -## Abstracting buffer creation - -Because we're going to create multiple buffers in this chapter, it's a good idea -to move buffer creation to a helper function. Create a new function -`createBuffer` and move the code in `createVertexBuffer` (except mapping) to it. +## 소개 + +지금 우리가 사용하는 정점 버퍼는 올바르게 작동하지만, CPU에서 접근할 수 있도록 하는 메모리 타입이 그래픽 카드 자체에서 읽기에 가장 최적의 메모리 타입은 아닐 수 있습니다. 가장 최적화된 메모리는 `VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT` 플래그를 가지며, 보통 외장 그래픽 카드에서는 CPU가 접근할 수 없습니다. 이번 장에서는 두 개의 정점 버퍼를 만들 것입니다. 하나는 정점 배열의 데이터를 업로드하기 위한 CPU 접근 가능 메모리의 *스테이징 버퍼(staging buffer)*이고, 다른 하나는 디바이스 로컬 메모리에 있는 최종 정점 버퍼입니다. 그런 다음 버퍼 복사 명령을 사용해 스테이징 버퍼의 데이터를 실제 정점 버퍼로 이동시킬 것입니다. + +## 전송 큐 (Transfer queue) + +버퍼 복사 명령은 `VK_QUEUE_TRANSFER_BIT`로 표시되는 전송(transfer) 연산을 지원하는 큐 패밀리(queue family)를 필요로 합니다. 좋은 소식은 `VK_QUEUE_GRAPHICS_BIT`나 `VK_QUEUE_COMPUTE_BIT` 기능을 가진 모든 큐 패밀리는 이미 암시적으로 `VK_QUEUE_TRANSFER_BIT` 연산을 지원한다는 것입니다. 이런 경우 구현체는 `queueFlags`에 이 비트를 명시적으로 표시하지 않아도 됩니다. + +만약 도전해보고 싶다면, 전송 연산만을 위한 별도의 큐 패밀리를 사용해볼 수도 있습니다. 이를 위해서는 프로그램에 다음과 같은 수정이 필요합니다. + +* `QueueFamilyIndices`와 `findQueueFamilies`를 수정하여 `VK_QUEUE_GRAPHICS_BIT`는 없지만 `VK_QUEUE_TRANSFER_BIT` 비트를 가진 큐 패밀리를 명시적으로 찾도록 합니다. +* `createLogicalDevice`를 수정하여 전송 큐에 대한 핸들을 요청합니다. +* 전송 큐 패밀리에서 제출될 커맨드 버퍼를 위한 두 번째 커맨드 풀(command pool)을 생성합니다. +* 리소스의 `sharingMode`를 `VK_SHARING_MODE_CONCURRENT`로 변경하고 그래픽 큐와 전송 큐 패밀리를 모두 지정합니다. +* `vkCmdCopyBuffer`와 같은 모든 전송 명령을 그래픽 큐가 아닌 전송 큐에 제출합니다. + +약간의 작업이 필요하지만, 이를 통해 큐 패밀리 간에 리소스를 어떻게 공유하는지에 대해 많은 것을 배울 수 있을 것입니다. + +## 버퍼 생성 추상화 + +이번 장에서는 여러 버퍼를 생성할 것이므로, 버퍼 생성을 헬퍼(helper) 함수로 옮기는 것이 좋습니다. `createBuffer`라는 새 함수를 만들고, `createVertexBuffer`에 있던 코드(매핑 제외)를 이 함수로 옮기세요. ```c++ void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, VkDeviceMemory& bufferMemory) { @@ -71,12 +48,9 @@ void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyF } ``` -Make sure to add parameters for the buffer size, memory properties and usage so -that we can use this function to create many different types of buffers. The -last two parameters are output variables to write the handles to. +다양한 종류의 버퍼를 생성하는 데 이 함수를 사용할 수 있도록 버퍼 크기, 메모리 속성, 사용 목적을 매개변수로 추가해야 합니다. 마지막 두 매개변수는 핸들을 기록하기 위한 출력 변수입니다. -You can now remove the buffer creation and memory allocation code from -`createVertexBuffer` and just call `createBuffer` instead: +이제 `createVertexBuffer`에서 버퍼 생성 및 메모리 할당 코드를 제거하고, 대신 `createBuffer`를 호출할 수 있습니다. ```c++ void createVertexBuffer() { @@ -90,12 +64,11 @@ void createVertexBuffer() { } ``` -Run your program to make sure that the vertex buffer still works properly. +프로그램을 실행하여 정점 버퍼가 여전히 제대로 작동하는지 확인하세요. -## Using a staging buffer +## 스테이징 버퍼 사용하기 -We're now going to change `createVertexBuffer` to only use a host visible buffer -as temporary buffer and use a device local one as actual vertex buffer. +이제 `createVertexBuffer` 함수를 수정하여, 호스트 가시성(host visible) 버퍼는 임시 버퍼로만 사용하고, 디바이스 로컬(device local) 버퍼를 실제 정점 버퍼로 사용하도록 변경하겠습니다. ```c++ void createVertexBuffer() { @@ -114,24 +87,14 @@ void createVertexBuffer() { } ``` -We're now using a new `stagingBuffer` with `stagingBufferMemory` for mapping and -copying the vertex data. In this chapter we're going to use two new buffer usage -flags: +이제 정점 데이터를 매핑하고 복사하기 위해 `stagingBuffer`와 `stagingBufferMemory`를 사용합니다. 이번 장에서는 두 개의 새로운 버퍼 사용 플래그를 사용합니다: -* `VK_BUFFER_USAGE_TRANSFER_SRC_BIT`: Buffer can be used as source in a memory -transfer operation. -* `VK_BUFFER_USAGE_TRANSFER_DST_BIT`: Buffer can be used as destination in a -memory transfer operation. +* `VK_BUFFER_USAGE_TRANSFER_SRC_BIT`: 버퍼가 메모리 전송 연산의 원본(source)으로 사용될 수 있습니다. +* `VK_BUFFER_USAGE_TRANSFER_DST_BIT`: 버퍼가 메모리 전송 연산의 대상(destination)으로 사용될 수 있습니다. -The `vertexBuffer` is now allocated from a memory type that is device local, -which generally means that we're not able to use `vkMapMemory`. However, we can -copy data from the `stagingBuffer` to the `vertexBuffer`. We have to indicate -that we intend to do that by specifying the transfer source flag for the -`stagingBuffer` and the transfer destination flag for the `vertexBuffer`, along -with the vertex buffer usage flag. +이제 `vertexBuffer`는 디바이스 로컬 메모리 타입으로 할당됩니다. 이는 일반적으로 우리가 `vkMapMemory`를 사용할 수 없다는 것을 의미합니다. 하지만 `stagingBuffer`에서 `vertexBuffer`로 데이터를 복사할 수는 있습니다. 이를 위해 `stagingBuffer`에는 전송 원본 플래그를, `vertexBuffer`에는 정점 버퍼 사용 플래그와 함께 전송 대상 플래그를 지정해야 합니다. -We're now going to write a function to copy the contents from one buffer to -another, called `copyBuffer`. +이제 한 버퍼의 내용을 다른 버퍼로 복사하는 `copyBuffer` 함수를 작성하겠습니다. ```c++ void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { @@ -139,12 +102,7 @@ void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { } ``` -Memory transfer operations are executed using command buffers, just like drawing -commands. Therefore we must first allocate a temporary command buffer. You may -wish to create a separate command pool for these kinds of short-lived buffers, -because the implementation may be able to apply memory allocation optimizations. -You should use the `VK_COMMAND_POOL_CREATE_TRANSIENT_BIT` flag during command -pool generation in that case. +메모리 전송 연산은 그리기 명령과 마찬가지로 커맨드 버퍼를 사용하여 실행됩니다. 따라서 먼저 임시 커맨드 버퍼를 할당해야 합니다. 이런 종류의 단기(short-lived) 버퍼를 위해 별도의 커맨드 풀을 만드는 것을 고려할 수 있습니다. 왜냐하면 구현체가 메모리 할당 최적화를 적용할 수 있기 때문입니다. 그 경우 커맨드 풀 생성 시 `VK_COMMAND_POOL_CREATE_TRANSIENT_BIT` 플래그를 사용해야 합니다. ```c++ void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { @@ -159,7 +117,7 @@ void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { } ``` -And immediately start recording the command buffer: +그리고 즉시 커맨드 버퍼 기록을 시작합니다. ```c++ VkCommandBufferBeginInfo beginInfo{}; @@ -169,9 +127,7 @@ beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vkBeginCommandBuffer(commandBuffer, &beginInfo); ``` -We're only going to use the command buffer once and wait with returning from the function until the copy -operation has finished executing. It's good practice to tell the driver about -our intent using `VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT`. +우리는 이 커맨드 버퍼를 한 번만 사용할 것이며, 복사 작업이 실행 완료될 때까지 함수에서 반환하지 않고 기다릴 것입니다. 드라이버에게 우리의 의도를 `VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT` 플래그를 통해 알려주는 것이 좋은 습관입니다. ```c++ VkBufferCopy copyRegion{}; @@ -181,18 +137,13 @@ copyRegion.size = size; vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region); ``` -Contents of buffers are transferred using the `vkCmdCopyBuffer` command. It -takes the source and destination buffers as arguments, and an array of regions -to copy. The regions are defined in `VkBufferCopy` structs and consist of a -source buffer offset, destination buffer offset and size. It is not possible to -specify `VK_WHOLE_SIZE` here, unlike the `vkMapMemory` command. +버퍼의 내용은 `vkCmdCopyBuffer` 명령을 통해 전송됩니다. 이 함수는 원본과 대상 버퍼를 인자로 받고, 복사할 영역의 배열을 받습니다. 이 영역들은 `VkBufferCopy` 구조체로 정의되며, 원본 버퍼 오프셋, 대상 버퍼 오프셋, 그리고 크기로 구성됩니다. `vkMapMemory` 명령과 달리 여기서는 `VK_WHOLE_SIZE`를 지정할 수 없습니다. ```c++ vkEndCommandBuffer(commandBuffer); ``` -This command buffer only contains the copy command, so we can stop recording -right after that. Now execute the command buffer to complete the transfer: +이 커맨드 버퍼는 복사 명령만 포함하므로, 바로 기록을 중단할 수 있습니다. 이제 커맨드 버퍼를 실행하여 전송을 완료합니다. ```c++ VkSubmitInfo submitInfo{}; @@ -204,22 +155,15 @@ vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); vkQueueWaitIdle(graphicsQueue); ``` -Unlike the draw commands, there are no events we need to wait on this time. We -just want to execute the transfer on the buffers immediately. There are again -two possible ways to wait on this transfer to complete. We could use a fence and -wait with `vkWaitForFences`, or simply wait for the transfer queue to become -idle with `vkQueueWaitIdle`. A fence would allow you to schedule multiple -transfers simultaneously and wait for all of them complete, instead of executing -one at a time. That may give the driver more opportunities to optimize. +그리기 명령과 달리 이번에는 기다려야 할 이벤트가 없습니다. 단지 버퍼에 대한 전송을 즉시 실행하기만 하면 됩니다. 이 전송이 완료되기를 기다리는 방법에는 다시 두 가지가 있습니다. 펜스(fence)를 사용하고 `vkWaitForFences`로 기다리거나, 단순히 `vkQueueWaitIdle`로 전송 큐가 유휴(idle) 상태가 될 때까지 기다릴 수 있습니다. 펜스를 사용하면 여러 전송을 동시에 스케줄링하고 모든 작업이 완료될 때까지 기다릴 수 있어, 드라이버가 최적화할 더 많은 기회를 가질 수 있습니다. ```c++ vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); ``` -Don't forget to clean up the command buffer used for the transfer operation. +전송 작업에 사용된 커맨드 버퍼를 정리하는 것을 잊지 마세요. -We can now call `copyBuffer` from the `createVertexBuffer` function to move the -vertex data to the device local buffer: +이제 `createVertexBuffer` 함수에서 `copyBuffer`를 호출하여 정점 데이터를 디바이스 로컬 버퍼로 옮길 수 있습니다. ```c++ createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory); @@ -227,8 +171,7 @@ createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERT copyBuffer(stagingBuffer, vertexBuffer, bufferSize); ``` -After copying the data from the staging buffer to the device buffer, we should -clean it up: +스테이징 버퍼의 데이터를 디바이스 버퍼로 복사한 후에는 스테이징 버퍼를 정리해야 합니다. ```c++ ... @@ -240,28 +183,14 @@ clean it up: } ``` -Run your program to verify that you're seeing the familiar triangle again. The -improvement may not be visible right now, but its vertex data is now being -loaded from high performance memory. This will matter when we're going to start -rendering more complex geometry. - -## Conclusion - -It should be noted that in a real world application, you're not supposed to -actually call `vkAllocateMemory` for every individual buffer. The maximum number -of simultaneous memory allocations is limited by the `maxMemoryAllocationCount` -physical device limit, which may be as low as `4096` even on high end hardware -like an NVIDIA GTX 1080. The right way to allocate memory for a large number of -objects at the same time is to create a custom allocator that splits up a single -allocation among many different objects by using the `offset` parameters that -we've seen in many functions. - -You can either implement such an allocator yourself, or use the -[VulkanMemoryAllocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator) -library provided by the GPUOpen initiative. However, for this tutorial it's okay -to use a separate allocation for every resource, because we won't come close to -hitting any of these limits for now. - -[C++ code](/code/20_staging_buffer.cpp) / -[Vertex shader](/code/18_shader_vertexbuffer.vert) / -[Fragment shader](/code/18_shader_vertexbuffer.frag) +프로그램을 실행하여 익숙한 삼각형이 다시 보이는지 확인하세요. 성능 향상이 지금 당장은 눈에 보이지 않을 수 있지만, 이제 정점 데이터는 고성능 메모리에서 로드되고 있습니다. 이는 앞으로 더 복잡한 지오메트리를 렌더링하기 시작할 때 중요해질 것입니다. + +## 결론 + +실제 애플리케이션에서는 모든 개별 버퍼에 대해 `vkAllocateMemory`를 호출해서는 안 된다는 점에 유의해야 합니다. 동시 메모리 할당의 최대 수는 `maxMemoryAllocationCount` 물리 디바이스 제한에 의해 제한되며, NVIDIA GTX 1080과 같은 고사양 하드웨어에서도 `4096` 정도로 낮을 수 있습니다. 다수의 객체에 대해 동시에 메모리를 할당하는 올바른 방법은, 우리가 많은 함수에서 보았던 `offset` 매개변수를 사용하여 단일 할당을 여러 객체에 나누어 사용하는 커스텀 할당자(custom allocator)를 만드는 것입니다. + +이러한 할당자를 직접 구현하거나, GPUOpen 이니셔티브에서 제공하는 [VulkanMemoryAllocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator) 라이브러리를 사용할 수 있습니다. 하지만 이 튜토리얼에서는 지금 당장 이러한 제한에 도달할 일이 없으므로 모든 리소스에 대해 별도의 할당을 사용하는 것이 괜찮습니다. + +[C++ 코드](/code/20_staging_buffer.cpp) / +[정점 셰이더](/code/18_shader_vertexbuffer.vert) / +[프래그먼트 셰이더](/code/18_shader_vertexbuffer.frag) \ No newline at end of file diff --git a/ko/04_Vertex_buffers/03_Index_buffer.md b/ko/04_Vertex_buffers/03_Index_buffer.md index 088263db..87f2f144 100644 --- a/ko/04_Vertex_buffers/03_Index_buffer.md +++ b/ko/04_Vertex_buffers/03_Index_buffer.md @@ -1,29 +1,16 @@ -## Introduction +## 소개 -The 3D meshes you'll be rendering in a real world application will often share -vertices between multiple triangles. This already happens even with something -simple like drawing a rectangle: +실제 애플리케이션에서 렌더링할 3D 메시는 여러 삼각형 간에 정점을 공유하는 경우가 많습니다. 이는 사각형을 그리는 것처럼 간단한 작업에서도 이미 발생합니다. ![](/images/vertex_vs_index.svg) -Drawing a rectangle takes two triangles, which means that we need a vertex -buffer with 6 vertices. The problem is that the data of two vertices needs to be -duplicated resulting in 50% redundancy. It only gets worse with more complex -meshes, where vertices are reused in an average number of 3 triangles. The -solution to this problem is to use an *index buffer*. +사각형을 그리려면 두 개의 삼각형이 필요하며, 이는 6개의 정점으로 구성된 정점 버퍼가 필요하다는 것을 의미합니다. 문제는 두 정점의 데이터가 중복되어 50%의 중복이 발생한다는 것입니다. 더 복잡한 메시에서는 정점이 평균 3개의 삼각형에서 재사용되므로 이 문제는 더욱 심각해집니다. 이 문제에 대한 해결책은 *인덱스 버퍼(index buffer)*를 사용하는 것입니다. -An index buffer is essentially an array of pointers into the vertex buffer. It -allows you to reorder the vertex data, and reuse existing data for multiple -vertices. The illustration above demonstrates what the index buffer would look -like for the rectangle if we have a vertex buffer containing each of the four -unique vertices. The first three indices define the upper-right triangle and the -last three indices define the vertices for the bottom-left triangle. +인덱스 버퍼는 본질적으로 정점 버퍼에 대한 포인터 배열입니다. 인덱스 버퍼를 사용하면 정점 데이터의 순서를 바꾸고, 여러 정점에 대해 기존 데이터를 재사용할 수 있습니다. 위 그림은 4개의 고유한 정점을 포함하는 정점 버퍼가 있을 때, 사각형을 위한 인덱스 버퍼가 어떻게 보일지를 보여줍니다. 처음 세 개의 인덱스는 오른쪽 위 삼각형을 정의하고, 마지막 세 개의 인덱스는 왼쪽 아래 삼각형의 정점을 정의합니다. -## Index buffer creation +## 인덱스 버퍼 생성 -In this chapter we're going to modify the vertex data and add index data to -draw a rectangle like the one in the illustration. Modify the vertex data to -represent the four corners: +이번 장에서는 정점 데이터를 수정하고 인덱스 데이터를 추가하여 그림과 같은 사각형을 그려보겠습니다. 네 개의 모서리를 나타내도록 정점 데이터를 수정합니다. ```c++ const std::vector vertices = { @@ -34,10 +21,7 @@ const std::vector vertices = { }; ``` -The top-left corner is red, top-right is green, bottom-right is blue and the -bottom-left is white. We'll add a new array `indices` to represent the contents -of the index buffer. It should match the indices in the illustration to draw the -upper-right triangle and bottom-left triangle. +왼쪽 아래 모서리는 빨간색, 오른쪽 아래는 녹색, 오른쪽 위는 파란색, 왼쪽 위는 흰색입니다. `indices`라는 새 배열을 추가하여 인덱스 버퍼의 내용을 나타냅니다. 이 배열은 그림의 인덱스와 일치시켜 오른쪽 위 삼각형과 왼쪽 아래 삼각형을 그려야 합니다. ```c++ const std::vector indices = { @@ -45,13 +29,9 @@ const std::vector indices = { }; ``` -It is possible to use either `uint16_t` or `uint32_t` for your index buffer -depending on the number of entries in `vertices`. We can stick to `uint16_t` for -now because we're using less than 65535 unique vertices. +`vertices`의 항목 수에 따라 인덱스 버퍼에 `uint16_t` 또는 `uint32_t`를 사용할 수 있습니다. 65535개 미만의 고유 정점을 사용하므로 지금은 `uint16_t`를 사용하겠습니다. -Just like the vertex data, the indices need to be uploaded into a `VkBuffer` for -the GPU to be able to access them. Define two new class members to hold the -resources for the index buffer: +정점 데이터와 마찬가지로, 인덱스도 GPU가 접근할 수 있도록 `VkBuffer`에 업로드해야 합니다. 인덱스 버퍼의 리소스를 저장하기 위해 두 개의 새로운 클래스 멤버를 정의합니다. ```c++ VkBuffer vertexBuffer; @@ -60,8 +40,7 @@ VkBuffer indexBuffer; VkDeviceMemory indexBufferMemory; ``` -The `createIndexBuffer` function that we'll add now is almost identical to -`createVertexBuffer`: +이제 추가할 `createIndexBuffer` 함수는 `createVertexBuffer`와 거의 동일합니다. ```c++ void initVulkan() { @@ -92,16 +71,9 @@ void createIndexBuffer() { } ``` -There are only two notable differences. The `bufferSize` is now equal to the -number of indices times the size of the index type, either `uint16_t` or -`uint32_t`. The usage of the `indexBuffer` should be -`VK_BUFFER_USAGE_INDEX_BUFFER_BIT` instead of -`VK_BUFFER_USAGE_VERTEX_BUFFER_BIT`, which makes sense. Other than that, the -process is exactly the same. We create a staging buffer to copy the contents of -`indices` to and then copy it to the final device local index buffer. +주목할 만한 차이점은 두 가지뿐입니다. `bufferSize`는 이제 인덱스 수에 인덱스 타입(`uint16_t` 또는 `uint32_t`)의 크기를 곱한 값과 같습니다. `indexBuffer`의 usage는 `VK_BUFFER_USAGE_VERTEX_BUFFER_BIT` 대신 `VK_BUFFER_USAGE_INDEX_BUFFER_BIT`이어야 하는데, 이는 타당한 설정입니다. 그 외의 과정은 정확히 동일합니다. `indices`의 내용을 복사하기 위한 스테이징 버퍼를 만들고, 그 내용을 최종 장치 로컬 인덱스 버퍼로 복사합니다. -The index buffer should be cleaned up at the end of the program, just like the -vertex buffer: +인덱스 버퍼는 정점 버퍼와 마찬가지로 프로그램이 끝날 때 정리해야 합니다. ```c++ void cleanup() { @@ -117,14 +89,9 @@ void cleanup() { } ``` -## Using an index buffer +## 인덱스 버퍼 사용하기 -Using an index buffer for drawing involves two changes to -`recordCommandBuffer`. We first need to bind the index buffer, just like we did -for the vertex buffer. The difference is that you can only have a single index -buffer. It's unfortunately not possible to use different indices for each vertex -attribute, so we do still have to completely duplicate vertex data even if just -one attribute varies. +그리기에 인덱스 버퍼를 사용하는 것은 `recordCommandBuffer`에 두 가지 변경 사항을 수반합니다. 먼저 정점 버퍼에 했던 것처럼 인덱스 버퍼를 바인딩해야 합니다. 차이점은 인덱스 버퍼는 하나만 가질 수 있다는 것입니다. 아쉽게도 각 정점 속성에 대해 서로 다른 인덱스를 사용하는 것은 불가능하므로, 속성 하나만 다르더라도 정점 데이터를 완전히 복제해야 합니다. ```c++ vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets); @@ -132,48 +99,24 @@ vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets); vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT16); ``` -An index buffer is bound with `vkCmdBindIndexBuffer` which has the index buffer, -a byte offset into it, and the type of index data as parameters. As mentioned -before, the possible types are `VK_INDEX_TYPE_UINT16` and -`VK_INDEX_TYPE_UINT32`. +`vkCmdBindIndexBuffer`를 사용하여 인덱스 버퍼를 바인딩하며, 이 함수는 인덱스 버퍼, 버퍼 내의 바이트 오프셋, 그리고 인덱스 데이터의 타입을 매개변수로 받습니다. 앞서 언급했듯이, 가능한 타입은 `VK_INDEX_TYPE_UINT16`과 `VK_INDEX_TYPE_UINT32`입니다. -Just binding an index buffer doesn't change anything yet, we also need to change -the drawing command to tell Vulkan to use the index buffer. Remove the -`vkCmdDraw` line and replace it with `vkCmdDrawIndexed`: +인덱스 버퍼를 바인딩하는 것만으로는 아직 아무것도 바뀌지 않으며, Vulkan에 인덱스 버퍼를 사용하도록 지시하기 위해 그리기 명령도 변경해야 합니다. `vkCmdDraw` 라인을 제거하고 `vkCmdDrawIndexed`로 교체합니다. ```c++ vkCmdDrawIndexed(commandBuffer, static_cast(indices.size()), 1, 0, 0, 0); ``` -A call to this function is very similar to `vkCmdDraw`. The first two parameters -specify the number of indices and the number of instances. We're not using -instancing, so just specify `1` instance. The number of indices represents the -number of vertices that will be passed to the vertex shader. The next parameter -specifies an offset into the index buffer, using a value of `1` would cause the -graphics card to start reading at the second index. The second to last parameter -specifies an offset to add to the indices in the index buffer. The final -parameter specifies an offset for instancing, which we're not using. +이 함수 호출은 `vkCmdDraw`와 매우 유사합니다. 처음 두 매개변수는 인덱스의 수와 인스턴스의 수를 지정합니다. 인스턴싱은 사용하지 않으므로 인스턴스는 `1`로 지정합니다. 인덱스의 수는 정점 셰이더로 전달될 정점의 수를 나타냅니다. 다음 매개변수는 인덱스 버퍼로의 오프셋을 지정하며, 값으로 `1`을 사용하면 그래픽 카드가 두 번째 인덱스부터 읽기 시작합니다. 끝에서 두 번째 매개변수는 인덱스 버퍼의 인덱스에 더할 오프셋을 지정합니다. 마지막 매개변수는 인스턴싱을 위한 오프셋을 지정하는데, 우리는 사용하지 않습니다. -Now run your program and you should see the following: +이제 프로그램을 실행하면 다음과 같은 결과가 나타나야 합니다. ![](/images/indexed_rectangle.png) -You now know how to save memory by reusing vertices with index buffers. This -will become especially important in a future chapter where we're going to load -complex 3D models. - -The previous chapter already mentioned that you should allocate multiple -resources like buffers from a single memory allocation, but in fact you should -go a step further. [Driver developers recommend](https://developer.nvidia.com/vulkan-memory-management) -that you also store multiple buffers, like the vertex and index buffer, into a -single `VkBuffer` and use offsets in commands like `vkCmdBindVertexBuffers`. The -advantage is that your data is more cache friendly in that case, because it's -closer together. It is even possible to reuse the same chunk of memory for -multiple resources if they are not used during the same render operations, -provided that their data is refreshed, of course. This is known as *aliasing* -and some Vulkan functions have explicit flags to specify that you want to do -this. - -[C++ code](/code/21_index_buffer.cpp) / -[Vertex shader](/code/18_shader_vertexbuffer.vert) / -[Fragment shader](/code/18_shader_vertexbuffer.frag) +이제 인덱스 버퍼를 사용하여 정점을 재사용함으로써 메모리를 절약하는 방법을 알게 되었습니다. 이는 나중에 복잡한 3D 모델을 로드할 장에서 특히 중요해질 것입니다. + +이전 장에서 이미 여러 리소스(예: 버퍼)를 단일 메모리 할당에서 할당해야 한다고 언급했지만, 사실은 한 단계 더 나아가야 합니다. [드라이버 개발자들은](https://developer.nvidia.com/vulkan-memory-management) 정점 버퍼와 인덱스 버퍼 같은 여러 버퍼를 단일 `VkBuffer`에 저장하고 `vkCmdBindVertexBuffers`와 같은 명령어에서 오프셋을 사용할 것을 권장합니다. 이렇게 하면 데이터가 더 가깝게 모여있기 때문에 캐시 친화적(cache friendly)이라는 장점이 있습니다. 동일한 렌더링 작업 중에 사용되지 않는 여러 리소스에 대해 동일한 메모리 청크를 재사용하는 것도 가능합니다. 물론 데이터는 새로고침되어야 합니다. 이를 *에일리어싱(aliasing)*이라고 하며, 일부 Vulkan 함수에는 이를 원한다고 명시적으로 지정하는 플래그가 있습니다. + +[C++ 코드](/code/21_index_buffer.cpp) / +[정점 셰이더](/code/18_shader_vertexbuffer.vert) / +[프래그먼트 셰이더](/code/18_shader_vertexbuffer.frag) \ No newline at end of file diff --git a/ko/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md b/ko/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md index 2bdcc2dc..6d1e6349 100644 --- a/ko/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md +++ b/ko/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md @@ -1,34 +1,16 @@ -## Introduction - -We're now able to pass arbitrary attributes to the vertex shader for each -vertex, but what about global variables? We're going to move on to 3D graphics -from this chapter on and that requires a model-view-projection matrix. We could -include it as vertex data, but that's a waste of memory and it would require us -to update the vertex buffer whenever the transformation changes. The -transformation could easily change every single frame. - -The right way to tackle this in Vulkan is to use *resource descriptors*. A -descriptor is a way for shaders to freely access resources like buffers and -images. We're going to set up a buffer that contains the transformation matrices -and have the vertex shader access them through a descriptor. Usage of -descriptors consists of three parts: - -* Specify a descriptor set layout during pipeline creation -* Allocate a descriptor set from a descriptor pool -* Bind the descriptor set during rendering - -The *descriptor set layout* specifies the types of resources that are going to be -accessed by the pipeline, just like a render pass specifies the types of -attachments that will be accessed. A *descriptor set* specifies the actual -buffer or image resources that will be bound to the descriptors, just like a -framebuffer specifies the actual image views to bind to render pass attachments. -The descriptor set is then bound for the drawing commands just like the vertex -buffers and framebuffer. - -There are many types of descriptors, but in this chapter we'll work with uniform -buffer objects (UBO). We'll look at other types of descriptors in future -chapters, but the basic process is the same. Let's say we have the data we want -the vertex shader to have in a C struct like this: +## 서론 + +이제 우리는 각 정점마다 임의의 어트리뷰트를 버텍스 셰이더로 전달할 수 있게 되었습니다. 하지만 전역 변수는 어떨까요? 이번 장부터는 3D 그래픽스로 넘어가게 되는데, 여기에는 모델-뷰-프로젝션(MVP) 행렬이 필요합니다. 이 행렬을 정점 데이터에 포함시킬 수도 있겠지만, 이는 메모리 낭비이며 변환이 변경될 때마다 정점 버퍼를 업데이트해야 합니다. 변환은 매 프레임마다 쉽게 바뀔 수 있습니다. + +Vulkan에서 이 문제를 해결하는 올바른 방법은 *리소스 디스크립터(resource descriptor)*를 사용하는 것입니다. 디스크립터는 셰이더가 버퍼나 이미지 같은 리소스에 자유롭게 접근할 수 있게 해주는 방법입니다. 우리는 변환 행렬들을 담고 있는 버퍼를 설정하고, 버텍스 셰이더가 디스크립터를 통해 이들에 접근하도록 할 것입니다. 디스크립터 사용은 세 부분으로 구성됩니다: + +* 파이프라인 생성 시 디스크립터 셋 레이아웃 명시 +* 디스크립터 풀에서 디스크립터 셋 할당 +* 렌더링 시 디스크립터 셋 바인딩 + +*디스크립터 셋 레이아웃*은 렌더 패스가 접근할 어태치먼트의 타입을 명시하는 것과 유사하게, 파이프라인이 접근할 리소스의 타입을 명시합니다. *디스크립터 셋*은 프레임버퍼가 렌더 패스 어태치먼트에 바인딩할 실제 이미지 뷰를 지정하는 것과 유사하게, 디스크립터에 바인딩될 실제 버퍼나 이미지 리소스를 지정합니다. 그 후 디스크립터 셋은 정점 버퍼나 프레임버퍼처럼 드로우 커맨드를 위해 바인딩됩니다. + +많은 종류의 디스크립터가 있지만, 이번 장에서는 uniform buffer object (UBO)를 다룰 것입니다. 다른 종류의 디스크립터는 다음 장들에서 살펴보겠지만, 기본적인 과정은 동일합니다. 버텍스 셰이더가 사용하길 원하는 데이터를 다음과 같은 C++ 구조체에 담는다고 가정해 봅시다: ```c++ struct UniformBufferObject { @@ -38,8 +20,7 @@ struct UniformBufferObject { }; ``` -Then we can copy the data to a `VkBuffer` and access it through a uniform buffer -object descriptor from the vertex shader like this: +그러면 우리는 이 데이터를 `VkBuffer`로 복사하고, 버텍스 셰이더에서 uniform buffer object 디스크립터를 통해 다음과 같이 접근할 수 있습니다: ```glsl layout(binding = 0) uniform UniformBufferObject { @@ -54,15 +35,11 @@ void main() { } ``` -We're going to update the model, view and projection matrices every frame to -make the rectangle from the previous chapter spin around in 3D. +우리는 이전 장의 사각형을 3D 공간에서 회전시키기 위해 매 프레임마다 모델, 뷰, 프로젝션 행렬을 업데이트할 것입니다. -## Vertex shader +## 버텍스 셰이더 -Modify the vertex shader to include the uniform buffer object like it was -specified above. I will assume that you are familiar with MVP transformations. -If you're not, see [the resource](https://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/) -mentioned in the first chapter. +위에서 명시된 것처럼 uniform buffer object를 포함하도록 버텍스 셰이더를 수정하세요. MVP 변환에 대해서는 이미 익숙하다고 가정하겠습니다. 만약 익숙하지 않다면, 첫 장에서 언급된 [참고 자료](https://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/)를 확인하세요. ```glsl #version 450 @@ -84,20 +61,11 @@ void main() { } ``` -Note that the order of the `uniform`, `in` and `out` declarations doesn't -matter. The `binding` directive is similar to the `location` directive for -attributes. We're going to reference this binding in the descriptor set layout. The -line with `gl_Position` is changed to use the transformations to compute the -final position in clip coordinates. Unlike the 2D triangles, the last component -of the clip coordinates may not be `1`, which will result in a division when -converted to the final normalized device coordinates on the screen. This is used -in perspective projection as the *perspective division* and is essential for -making closer objects look larger than objects that are further away. +`uniform`, `in`, `out` 선언 순서는 중요하지 않습니다. `binding` 지시어는 어트리뷰트의 `location` 지시어와 유사합니다. 우리는 디스크립터 셋 레이아웃에서 이 바인딩을 참조할 것입니다. `gl_Position`을 계산하는 줄은 변환 행렬들을 사용하여 최종 클립 좌표(clip coordinates)를 계산하도록 변경되었습니다. 2D 삼각형과 달리, 클립 좌표의 마지막 성분은 `1`이 아닐 수 있으며, 이는 화면의 최종 정규화된 장치 좌표(normalized device coordinates)로 변환될 때 나눗셈을 유발합니다. 이것은 원근 투영에서 *원근 분할(perspective division)*로 사용되며, 가까운 물체가 멀리 있는 물체보다 더 크게 보이게 하는 데 필수적입니다. -## Descriptor set layout +## 디스크립터 셋 레이아웃 -The next step is to define the UBO on the C++ side and to tell Vulkan about this -descriptor in the vertex shader. +다음 단계는 C++ 측에서 UBO를 정의하고, 버텍스 셰이더의 이 디스크립터에 대해 Vulkan에 알려주는 것입니다. ```c++ struct UniformBufferObject { @@ -107,15 +75,9 @@ struct UniformBufferObject { }; ``` -We can exactly match the definition in the shader using data types in GLM. The -data in the matrices is binary compatible with the way the shader expects it, so -we can later just `memcpy` a `UniformBufferObject` to a `VkBuffer`. +GLM의 데이터 타입을 사용하면 셰이더의 정의와 정확히 일치시킬 수 있습니다. 행렬의 데이터는 셰이더가 기대하는 방식과 바이너리 호환되므로, 나중에 `UniformBufferObject`를 `VkBuffer`에 `memcpy`하기만 하면 됩니다. -We need to provide details about every descriptor binding used in the shaders -for pipeline creation, just like we had to do for every vertex attribute and its -`location` index. We'll set up a new function to define all of this information -called `createDescriptorSetLayout`. It should be called right before pipeline -creation, because we're going to need it there. +모든 정점 어트리뷰트와 그 `location` 인덱스에 대해 했던 것처럼, 파이프라인 생성을 위해 셰이더에서 사용되는 모든 디스크립터 바인딩에 대한 세부 정보를 제공해야 합니다. 이 모든 정보를 정의하기 위해 `createDescriptorSetLayout`이라는 새 함수를 설정할 것입니다. 이 함수는 파이프라인 생성 직전에 호출되어야 합니다. 왜냐하면 거기서 필요하기 때문입니다. ```c++ void initVulkan() { @@ -132,8 +94,7 @@ void createDescriptorSetLayout() { } ``` -Every binding needs to be described through a `VkDescriptorSetLayoutBinding` -struct. +모든 바인딩은 `VkDescriptorSetLayoutBinding` 구조체를 통해 기술되어야 합니다. ```c++ void createDescriptorSetLayout() { @@ -144,41 +105,28 @@ void createDescriptorSetLayout() { } ``` -The first two fields specify the `binding` used in the shader and the type of -descriptor, which is a uniform buffer object. It is possible for the shader -variable to represent an array of uniform buffer objects, and `descriptorCount` -specifies the number of values in the array. This could be used to specify a -transformation for each of the bones in a skeleton for skeletal animation, for -example. Our MVP transformation is in a single uniform buffer object, so we're -using a `descriptorCount` of `1`. +첫 두 필드는 셰이더에서 사용되는 `binding`과 디스크립터의 타입, 즉 uniform buffer object를 명시합니다. 셰이더 변수가 uniform buffer object의 배열을 나타내는 것도 가능하며, `descriptorCount`는 배열에 있는 값의 수를 지정합니다. 예를 들어, 이는 스켈레탈 애니메이션에서 각 뼈에 대한 변환을 지정하는 데 사용될 수 있습니다. 우리의 MVP 변환은 단일 uniform buffer object에 있으므로, `descriptorCount`는 `1`을 사용합니다. ```c++ uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; ``` -We also need to specify in which shader stages the descriptor is going to be -referenced. The `stageFlags` field can be a combination of `VkShaderStageFlagBits` values -or the value `VK_SHADER_STAGE_ALL_GRAPHICS`. In our case, we're only referencing -the descriptor from the vertex shader. +우리는 또한 디스크립터가 어느 셰이더 단계에서 참조될 것인지 명시해야 합니다. `stageFlags` 필드는 `VkShaderStageFlagBits` 값들의 조합이거나 `VK_SHADER_STAGE_ALL_GRAPHICS` 값일 수 있습니다. 우리의 경우, 버텍스 셰이더에서만 디스크립터를 참조합니다. ```c++ uboLayoutBinding.pImmutableSamplers = nullptr; // Optional ``` -The `pImmutableSamplers` field is only relevant for image sampling related -descriptors, which we'll look at later. You can leave this to its default value. +`pImmutableSamplers` 필드는 이미지 샘플링 관련 디스크립터에만 관련이 있으며, 이는 나중에 살펴볼 것입니다. 이 값은 기본값으로 남겨둘 수 있습니다. -All of the descriptor bindings are combined into a single -`VkDescriptorSetLayout` object. Define a new class member above -`pipelineLayout`: +모든 디스크립터 바인딩은 단일 `VkDescriptorSetLayout` 객체로 결합됩니다. `pipelineLayout` 위에 새로운 클래스 멤버를 정의하세요: ```c++ VkDescriptorSetLayout descriptorSetLayout; VkPipelineLayout pipelineLayout; ``` -We can then create it using `vkCreateDescriptorSetLayout`. This function accepts -a simple `VkDescriptorSetLayoutCreateInfo` with the array of bindings: +그런 다음 `vkCreateDescriptorSetLayout`을 사용하여 이를 생성할 수 있습니다. 이 함수는 바인딩 배열을 포함하는 간단한 `VkDescriptorSetLayoutCreateInfo`를 받습니다: ```c++ VkDescriptorSetLayoutCreateInfo layoutInfo{}; @@ -191,10 +139,7 @@ if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayo } ``` -We need to specify the descriptor set layout during pipeline creation to tell -Vulkan which descriptors the shaders will be using. Descriptor set layouts are -specified in the pipeline layout object. Modify the `VkPipelineLayoutCreateInfo` -to reference the layout object: +셰이더가 어떤 디스크립터를 사용할지 Vulkan에 알리기 위해 파이프라인 생성 중에 디스크립터 셋 레이아웃을 지정해야 합니다. 디스크립터 셋 레이아웃은 파이프라인 레이아웃 객체에 지정됩니다. 레이아웃 객체를 참조하도록 `VkPipelineLayoutCreateInfo`를 수정하세요: ```c++ VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; @@ -203,13 +148,9 @@ pipelineLayoutInfo.setLayoutCount = 1; pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout; ``` -You may be wondering why it's possible to specify multiple descriptor set -layouts here, because a single one already includes all of the bindings. We'll -get back to that in the next chapter, where we'll look into descriptor pools and -descriptor sets. +왜 하나의 레이아웃에 모든 바인딩이 포함되어 있는데도 여러 디스크립터 셋 레이아웃을 지정할 수 있는지 궁금할 수 있습니다. 이에 대해서는 다음 장에서 디스크립터 풀과 디스크립터 셋을 다룰 때 다시 돌아올 것입니다. -The descriptor set layout should stick around while we may create new graphics -pipelines i.e. until the program ends: +디스크립터 셋 레이아웃은 우리가 새로운 그래픽스 파이프라인을 생성할 수 있는 동안, 즉 프로그램이 끝날 때까지 유지되어야 합니다: ```c++ void cleanup() { @@ -221,21 +162,13 @@ void cleanup() { } ``` -## Uniform buffer +## Uniform 버퍼 -In the next chapter we'll specify the buffer that contains the UBO data for the -shader, but we need to create this buffer first. We're going to copy new data to -the uniform buffer every frame, so it doesn't really make any sense to have a -staging buffer. It would just add extra overhead in this case and likely degrade -performance instead of improving it. +다음 장에서는 셰이더를 위한 UBO 데이터를 담고 있는 버퍼를 명시할 것이지만, 먼저 이 버퍼를 생성해야 합니다. 우리는 매 프레임마다 uniform 버퍼에 새로운 데이터를 복사할 것이므로, 스테이징 버퍼를 사용하는 것은 별 의미가 없습니다. 이 경우 추가적인 오버헤드만 발생시키고 성능을 향상시키기보다는 저하시킬 가능성이 높습니다. -We should have multiple buffers, because multiple frames may be in flight at the same -time and we don't want to update the buffer in preparation of the next frame while a -previous one is still reading from it! Thus, we need to have as many uniform buffers -as we have frames in flight, and write to a uniform buffer that is not currently -being read by the GPU. +우리는 여러 개의 버퍼를 가져야 합니다. 왜냐하면 여러 프레임이 동시에 처리 중(in flight)일 수 있고, 이전 프레임이 여전히 버퍼에서 읽고 있는 동안 다음 프레임을 준비하기 위해 버퍼를 업데이트하고 싶지 않기 때문입니다! 따라서, 우리는 동시에 처리 중인 프레임 수만큼 uniform 버퍼를 가져야 하며, 현재 GPU가 읽고 있지 않은 uniform 버퍼에 써야 합니다. -To that end, add new class members for `uniformBuffers`, and `uniformBuffersMemory`: +이를 위해 `uniformBuffers`와 `uniformBuffersMemory`, `uniformBuffersMapped`를 위한 새로운 클래스 멤버를 추가하세요: ```c++ VkBuffer indexBuffer; @@ -246,8 +179,7 @@ std::vector uniformBuffersMemory; std::vector uniformBuffersMapped; ``` -Similarly, create a new function `createUniformBuffers` that is called after -`createIndexBuffer` and allocates the buffers: +유사하게, `createIndexBuffer` 다음에 호출되어 버퍼들을 할당하는 `createUniformBuffers`라는 새 함수를 생성하세요: ```c++ void initVulkan() { @@ -275,9 +207,9 @@ void createUniformBuffers() { } ``` -We map the buffer right after creation using `vkMapMemory` to get a pointer to which we can write the data later on. The buffer stays mapped to this pointer for the application's whole lifetime. This technique is called **"persistent mapping"** and works on all Vulkan implementations. Not having to map the buffer every time we need to update it increases performances, as mapping is not free. +`vkMapMemory`를 사용하여 버퍼를 생성한 직후 매핑하여, 나중에 데이터를 쓸 수 있는 포인터를 얻습니다. 버퍼는 애플리케이션의 전체 수명 동안 이 포인터에 매핑된 상태로 유지됩니다. 이 기법은 **"영구적 매핑(persistent mapping)"**이라고 불리며 모든 Vulkan 구현에서 작동합니다. 업데이트가 필요할 때마다 버퍼를 매핑할 필요가 없으므로 성능이 향상됩니다. 매핑은 공짜가 아니기 때문입니다. -The uniform data will be used for all draw calls, so the buffer containing it should only be destroyed when we stop rendering. +uniform 데이터는 모든 드로우 콜에 사용되므로, 이를 담고 있는 버퍼는 렌더링을 멈출 때만 파괴되어야 합니다. ```c++ void cleanup() { @@ -295,9 +227,9 @@ void cleanup() { } ``` -## Updating uniform data +## Uniform 데이터 업데이트 -Create a new function `updateUniformBuffer` and add a call to it from the `drawFrame` function before submitting the next frame: +`updateUniformBuffer`라는 새 함수를 만들고, 다음 프레임을 제출하기 전에 `drawFrame` 함수에서 이 함수를 호출하도록 추가하세요: ```c++ void drawFrame() { @@ -320,9 +252,7 @@ void updateUniformBuffer(uint32_t currentImage) { } ``` -This function will generate a new transformation every frame to make the -geometry spin around. We need to include two new headers to implement this -functionality: +이 함수는 지오메트리가 회전하도록 매 프레임 새로운 변환을 생성할 것입니다. 이 기능을 구현하기 위해 두 개의 새로운 헤더를 포함해야 합니다: ```c++ #define GLM_FORCE_RADIANS @@ -332,15 +262,9 @@ functionality: #include ``` -The `glm/gtc/matrix_transform.hpp` header exposes functions that can be used to -generate model transformations like `glm::rotate`, view transformations like -`glm::lookAt` and projection transformations like `glm::perspective`. The -`GLM_FORCE_RADIANS` definition is necessary to make sure that functions like -`glm::rotate` use radians as arguments, to avoid any possible confusion. +`glm/gtc/matrix_transform.hpp` 헤더는 `glm::rotate`와 같은 모델 변환, `glm::lookAt`과 같은 뷰 변환, `glm::perspective`와 같은 프로젝션 변환을 생성하는 데 사용할 수 있는 함수들을 제공합니다. `GLM_FORCE_RADIANS` 정의는 `glm::rotate`와 같은 함수들이 라디안을 인자로 사용하도록 하여 혼동의 여지를 없애는 데 필요합니다. -The `chrono` standard library header exposes functions to do precise -timekeeping. We'll use this to make sure that the geometry rotates 90 degrees -per second regardless of frame rate. +`chrono` 표준 라이브러리 헤더는 정밀한 시간 측정을 위한 함수들을 제공합니다. 우리는 이를 사용하여 프레임 속도에 관계없이 지오메트리가 초당 90도 회전하도록 할 것입니다. ```c++ void updateUniformBuffer(uint32_t currentImage) { @@ -351,66 +275,45 @@ void updateUniformBuffer(uint32_t currentImage) { } ``` -The `updateUniformBuffer` function will start out with some logic to calculate -the time in seconds since rendering has started with floating point accuracy. +`updateUniformBuffer` 함수는 렌더링 시작 후 경과 시간을 부동소수점 정밀도의 초 단위로 계산하는 로직으로 시작합니다. -We will now define the model, view and projection transformations in the -uniform buffer object. The model rotation will be a simple rotation around the -Z-axis using the `time` variable: +이제 uniform buffer object에 모델, 뷰, 프로젝션 변환을 정의하겠습니다. 모델 회전은 `time` 변수를 사용하여 Z축을 중심으로 한 단순한 회전이 될 것입니다: ```c++ UniformBufferObject ubo{}; ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f)); ``` -The `glm::rotate` function takes an existing transformation, rotation angle and -rotation axis as parameters. The `glm::mat4(1.0f)` constructor returns an -identity matrix. Using a rotation angle of `time * glm::radians(90.0f)` -accomplishes the purpose of rotation 90 degrees per second. +`glm::rotate` 함수는 기존 변환, 회전 각도, 회전 축을 매개변수로 받습니다. `glm::mat4(1.0f)` 생성자는 단위 행렬(identity matrix)을 반환합니다. `time * glm::radians(90.0f)`의 회전 각도를 사용하면 초당 90도 회전하는 목적을 달성합니다. ```c++ ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)); ``` -For the view transformation I've decided to look at the geometry from above at a -45 degree angle. The `glm::lookAt` function takes the eye position, center -position and up axis as parameters. +뷰 변환의 경우, 45도 각도에서 위에서 지오메트리를 바라보도록 결정했습니다. `glm::lookAt` 함수는 눈의 위치, 바라보는 중심점, 위쪽 축을 매개변수로 받습니다. ```c++ ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f); ``` -I've chosen to use a perspective projection with a 45 degree vertical -field-of-view. The other parameters are the aspect ratio, near and far -view planes. It is important to use the current swap chain extent to calculate -the aspect ratio to take into account the new width and height of the window -after a resize. +45도의 수직 시야각을 가진 원근 투영을 사용하기로 결정했습니다. 다른 매개변수는 종횡비(aspect ratio), 근평면(near plane), 원평면(far plane)입니다. 창 크기 조정 후의 새로운 너비와 높이를 고려하여 종횡비를 계산하기 위해 현재 스왑 체인의 크기(`swapChainExtent`)를 사용하는 것이 중요합니다. ```c++ ubo.proj[1][1] *= -1; ``` -GLM was originally designed for OpenGL, where the Y coordinate of the clip -coordinates is inverted. The easiest way to compensate for that is to flip the -sign on the scaling factor of the Y axis in the projection matrix. If you don't -do this, then the image will be rendered upside down. +GLM은 원래 Y 좌표가 반전된 OpenGL을 위해 설계되었습니다. 이를 보정하는 가장 쉬운 방법은 투영 행렬에서 Y축의 스케일링 팩터 부호를 뒤집는 것입니다. 이렇게 하지 않으면 이미지가 거꾸로 렌더링됩니다. -All of the transformations are defined now, so we can copy the data in the -uniform buffer object to the current uniform buffer. This happens in exactly the same -way as we did for vertex buffers, except without a staging buffer. As noted earlier, we only map the uniform buffer once, so we can directly write to it without having to map again: +이제 모든 변환이 정의되었으므로, uniform buffer object의 데이터를 현재 uniform 버퍼로 복사할 수 있습니다. 이것은 스테이징 버퍼 없이 정점 버퍼에 대해 했던 것과 정확히 같은 방식으로 일어납니다. 앞서 언급했듯이, uniform 버퍼는 한 번만 매핑하므로, 다시 매핑할 필요 없이 직접 쓸 수 있습니다: ```c++ memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); ``` -Using a UBO this way is not the most efficient way to pass frequently changing -values to the shader. A more efficient way to pass a small buffer of data to -shaders are *push constants*. We may look at these in a future chapter. +이런 식으로 UBO를 사용하는 것은 자주 변경되는 값을 셰이더에 전달하는 가장 효율적인 방법은 아닙니다. 작은 데이터 버퍼를 셰이더에 전달하는 더 효율적인 방법은 *푸시 상수(push constants)*입니다. 이에 대해서는 향후 챕터에서 살펴볼 수 있습니다. -In the next chapter we'll look at descriptor sets, which will actually bind the -`VkBuffer`s to the uniform buffer descriptors so that the shader can access this -transformation data. +다음 장에서는 디스크립터 셋에 대해 살펴보고, 실제로 `VkBuffer`들을 uniform 버퍼 디스크립터에 바인딩하여 셰이더가 이 변환 데이터에 접근할 수 있도록 할 것입니다. -[C++ code](/code/22_descriptor_set_layout.cpp) / -[Vertex shader](/code/22_shader_ubo.vert) / -[Fragment shader](/code/22_shader_ubo.frag) +[C++ 코드](/code/22_descriptor_set_layout.cpp) / +[버텍스 셰이더](/code/22_shader_ubo.vert) / +[프래그먼트 셰이더](/code/22_shader_ubo.frag) \ No newline at end of file diff --git a/ko/05_Uniform_buffers/01_Descriptor_pool_and_sets.md b/ko/05_Uniform_buffers/01_Descriptor_pool_and_sets.md index b204db24..c0039c07 100644 --- a/ko/05_Uniform_buffers/01_Descriptor_pool_and_sets.md +++ b/ko/05_Uniform_buffers/01_Descriptor_pool_and_sets.md @@ -1,16 +1,10 @@ -## Introduction +## 소개 -The descriptor set layout from the previous chapter describes the type of -descriptors that can be bound. In this chapter we're going to create -a descriptor set for each `VkBuffer` resource to bind it to the -uniform buffer descriptor. +이전 장에서 다룬 디스크립터 셋 레이아웃은 바인딩할 수 있는 디스크립터의 유형을 설명합니다. 이번 장에서는 각 `VkBuffer` 리소스마다 디스크립터 셋을 만들어서 유니폼 버퍼 디스크립터에 바인딩할 것입니다. -## Descriptor pool +## 디스크립터 풀 (Descriptor pool) -Descriptor sets can't be created directly, they must be allocated from a pool -like command buffers. The equivalent for descriptor sets is unsurprisingly -called a *descriptor pool*. We'll write a new function `createDescriptorPool` -to set it up. +디스크립터 셋은 직접 생성할 수 없으며, 커맨드 버퍼처럼 풀(pool)에서 할당해야 합니다. 디스크립터 셋을 위한 이러한 풀은 놀랍지 않게도 *디스크립터 풀(descriptor pool)*이라고 불립니다. 이를 설정하기 위해 새로운 함수 `createDescriptorPool`을 작성하겠습니다. ```c++ void initVulkan() { @@ -27,8 +21,7 @@ void createDescriptorPool() { } ``` -We first need to describe which descriptor types our descriptor sets are going -to contain and how many of them, using `VkDescriptorPoolSize` structures. +우선 `VkDescriptorPoolSize` 구조체를 사용해 우리 디스크립터 셋이 어떤 유형의 디스크립터를 얼마나 포함할지 기술해야 합니다. ```c++ VkDescriptorPoolSize poolSize{}; @@ -36,8 +29,7 @@ poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; poolSize.descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); ``` -We will allocate one of these descriptors for every frame. This -pool size structure is referenced by the main `VkDescriptorPoolCreateInfo`: +우리는 프레임마다 하나씩 이 디스크립터를 할당할 것입니다. 이 풀 크기 구조체는 메인 `VkDescriptorPoolCreateInfo`에서 참조됩니다. ```c++ VkDescriptorPoolCreateInfo poolInfo{}; @@ -46,19 +38,13 @@ poolInfo.poolSizeCount = 1; poolInfo.pPoolSizes = &poolSize; ``` -Aside from the maximum number of individual descriptors that are available, we -also need to specify the maximum number of descriptor sets that may be -allocated: +개별 디스크립터의 최대 개수 외에도, 할당될 수 있는 디스크립터 셋의 최대 개수도 지정해야 합니다. ```c++ poolInfo.maxSets = static_cast(MAX_FRAMES_IN_FLIGHT); ``` -The structure has an optional flag similar to command pools that determines if -individual descriptor sets can be freed or not: -`VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT`. We're not going to touch -the descriptor set after creating it, so we don't need this flag. You can leave -`flags` to its default value of `0`. +이 구조체는 커맨드 풀과 유사한 선택적 플래그 `VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT`를 가집니다. 이 플래그는 개별 디스크립터 셋을 해제할 수 있는지 여부를 결정합니다. 우리는 디스크립터 셋을 생성한 후에는 수정하지 않을 것이므로 이 플래그는 필요 없습니다. `flags`는 기본값인 `0`으로 둘 수 있습니다. ```c++ VkDescriptorPool descriptorPool; @@ -70,13 +56,11 @@ if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SU } ``` -Add a new class member to store the handle of the descriptor pool and call -`vkCreateDescriptorPool` to create it. +디스크립터 풀 핸들을 저장할 새 클래스 멤버를 추가하고 `vkCreateDescriptorPool`을 호출하여 생성합니다. -## Descriptor set +## 디스크립터 셋 (Descriptor set) -We can now allocate the descriptor sets themselves. Add a `createDescriptorSets` -function for that purpose: +이제 디스크립터 셋 자체를 할당할 수 있습니다. 이를 위해 `createDescriptorSets` 함수를 추가합시다. ```c++ void initVulkan() { @@ -93,9 +77,7 @@ void createDescriptorSets() { } ``` -A descriptor set allocation is described with a `VkDescriptorSetAllocateInfo` -struct. You need to specify the descriptor pool to allocate from, the number of -descriptor sets to allocate, and the descriptor set layout to base them on: +`VkDescriptorSetAllocateInfo` 구조체로 디스크립터 셋 할당을 기술합니다. 할당할 디스크립터 풀, 할당할 디스크립터 셋의 개수, 그리고 기반으로 할 디스크립터 셋 레이아웃을 지정해야 합니다. ```c++ std::vector layouts(MAX_FRAMES_IN_FLIGHT, descriptorSetLayout); @@ -106,11 +88,9 @@ allocInfo.descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT); allocInfo.pSetLayouts = layouts.data(); ``` -In our case we will create one descriptor set for each frame in flight, all with the same layout. -Unfortunately we do need all the copies of the layout because the next function expects an array matching the number of sets. +우리의 경우, 각 프레임마다 하나의 디스크립터 셋을 생성하며, 모두 동일한 레이아웃을 가집니다. 안타깝게도 다음 함수가 셋의 개수와 일치하는 배열을 기대하기 때문에, 레이아웃의 모든 복사본이 필요합니다. -Add a class member to hold the descriptor set handles and allocate them with -`vkAllocateDescriptorSets`: +디스크립터 셋 핸들을 담을 클래스 멤버를 추가하고 `vkAllocateDescriptorSets`로 할당합니다. ```c++ VkDescriptorPool descriptorPool; @@ -124,10 +104,7 @@ if (vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()) != VK_SU } ``` -You don't need to explicitly clean up descriptor sets, because they will be -automatically freed when the descriptor pool is destroyed. The call to -`vkAllocateDescriptorSets` will allocate descriptor sets, each with one uniform -buffer descriptor. +디스크립터 풀이 파괴될 때 자동으로 해제되므로, 디스크립터 셋을 명시적으로 정리할 필요는 없습니다. `vkAllocateDescriptorSets` 호출은 각각 하나의 유니폼 버퍼 디스크립터를 가진 디스크립터 셋들을 할당할 것입니다. ```c++ void cleanup() { @@ -139,8 +116,7 @@ void cleanup() { } ``` -The descriptor sets have been allocated now, but the descriptors within still need -to be configured. We'll now add a loop to populate every descriptor: +이제 디스크립터 셋은 할당되었지만, 그 안의 디스크립터들은 아직 설정이 필요합니다. 이제 모든 디스크립터를 채우기 위한 루프를 추가합니다. ```c++ for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { @@ -148,10 +124,7 @@ for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { } ``` -Descriptors that refer to buffers, like our uniform buffer -descriptor, are configured with a `VkDescriptorBufferInfo` struct. This -structure specifies the buffer and the region within it that contains the data -for the descriptor. +우리의 유니폼 버퍼 디스크립터처럼 버퍼를 참조하는 디스크립터는 `VkDescriptorBufferInfo` 구조체로 설정합니다. 이 구조체는 버퍼와 디스크립터 데이터를 포함하는 버퍼 내의 영역을 지정합니다. ```c++ for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { @@ -162,8 +135,7 @@ for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { } ``` -If you're overwriting the whole buffer, like we are in this case, then it is also possible to use the `VK_WHOLE_SIZE` value for the range. The configuration of descriptors is updated using the `vkUpdateDescriptorSets` -function, which takes an array of `VkWriteDescriptorSet` structs as parameter. +우리처럼 버퍼 전체를 덮어쓰는 경우, `range`에 `VK_WHOLE_SIZE` 값을 사용하는 것도 가능합니다. 디스크립터 설정은 `VkWriteDescriptorSet` 구조체의 배열을 파라미터로 받는 `vkUpdateDescriptorSets` 함수를 사용하여 업데이트됩니다. ```c++ VkWriteDescriptorSet descriptorWrite{}; @@ -173,20 +145,14 @@ descriptorWrite.dstBinding = 0; descriptorWrite.dstArrayElement = 0; ``` -The first two fields specify the descriptor set to update and the binding. We -gave our uniform buffer binding index `0`. Remember that descriptors can be -arrays, so we also need to specify the first index in the array that we want to -update. We're not using an array, so the index is simply `0`. +첫 두 필드는 업데이트할 디스크립터 셋과 바인딩을 지정합니다. 우리는 유니폼 버퍼 바인딩 인덱스를 `0`으로 지정했습니다. 디스크립터는 배열이 될 수 있으므로, 업데이트를 시작할 배열의 첫 번째 인덱스도 지정해야 합니다. 우리는 배열을 사용하지 않으므로 인덱스는 단순히 `0`입니다. ```c++ descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; descriptorWrite.descriptorCount = 1; ``` -We need to specify the type of descriptor again. It's possible to update -multiple descriptors at once in an array, starting at index `dstArrayElement`. -The `descriptorCount` field specifies how many array elements you want to -update. +디스크립터 유형을 다시 지정해야 합니다. `dstArrayElement` 인덱스에서 시작하여 배열 내의 여러 디스크립터를 한 번에 업데이트하는 것이 가능합니다. `descriptorCount` 필드는 업데이트하려는 배열 요소의 수를 지정합니다. ```c++ descriptorWrite.pBufferInfo = &bufferInfo; @@ -194,66 +160,41 @@ descriptorWrite.pImageInfo = nullptr; // Optional descriptorWrite.pTexelBufferView = nullptr; // Optional ``` -The last field references an array with `descriptorCount` structs that actually -configure the descriptors. It depends on the type of descriptor which one of the -three you actually need to use. The `pBufferInfo` field is used for descriptors -that refer to buffer data, `pImageInfo` is used for descriptors that refer to -image data, and `pTexelBufferView` is used for descriptors that refer to buffer -views. Our descriptor is based on buffers, so we're using `pBufferInfo`. +마지막 필드는 실제로 디스크립터를 설정하는 `descriptorCount` 개의 구조체 배열을 참조합니다. 세 필드 중 어느 것을 사용해야 하는지는 디스크립터의 유형에 따라 다릅니다. `pBufferInfo` 필드는 버퍼 데이터를 참조하는 디스크립터에, `pImageInfo`는 이미지 데이터를 참조하는 디스크립터에, `pTexelBufferView`는 버퍼 뷰를 참조하는 디스크립터에 사용됩니다. 우리 디스크립터는 버퍼 기반이므로 `pBufferInfo`를 사용합니다. ```c++ vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr); ``` -The updates are applied using `vkUpdateDescriptorSets`. It accepts two kinds of -arrays as parameters: an array of `VkWriteDescriptorSet` and an array of -`VkCopyDescriptorSet`. The latter can be used to copy descriptors to each other, -as its name implies. +업데이트는 `vkUpdateDescriptorSets`를 사용하여 적용됩니다. 이 함수는 두 종류의 배열을 파라미터로 받습니다: `VkWriteDescriptorSet` 배열과 `VkCopyDescriptorSet` 배열입니다. 후자는 이름에서 알 수 있듯이 디스크립터를 서로 복사하는 데 사용할 수 있습니다. -## Using descriptor sets +## 디스크립터 셋 사용하기 -We now need to update the `recordCommandBuffer` function to actually bind the -right descriptor set for each frame to the descriptors in the shader with `vkCmdBindDescriptorSets`. This needs to be done before the `vkCmdDrawIndexed` call: +이제 `recordCommandBuffer` 함수를 업데이트하여 `vkCmdDrawIndexed` 호출 전에 `vkCmdBindDescriptorSets`로 셰이더의 디스크립터에 각 프레임에 맞는 디스크립터 셋을 실제로 바인딩해야 합니다. ```c++ vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSets[currentFrame], 0, nullptr); vkCmdDrawIndexed(commandBuffer, static_cast(indices.size()), 1, 0, 0, 0); ``` -Unlike vertex and index buffers, descriptor sets are not unique to graphics -pipelines. Therefore we need to specify if we want to bind descriptor sets to -the graphics or compute pipeline. The next parameter is the layout that the -descriptors are based on. The next three parameters specify the index of the -first descriptor set, the number of sets to bind, and the array of sets to bind. -We'll get back to this in a moment. The last two parameters specify an array of -offsets that are used for dynamic descriptors. We'll look at these in a future -chapter. - -If you run your program now, then you'll notice that unfortunately nothing is -visible. The problem is that because of the Y-flip we did in the projection -matrix, the vertices are now being drawn in counter-clockwise order instead of -clockwise order. This causes backface culling to kick in and prevents -any geometry from being drawn. Go to the `createGraphicsPipeline` function and -modify the `frontFace` in `VkPipelineRasterizationStateCreateInfo` to correct -this: +정점 및 인덱스 버퍼와 달리, 디스크립터 셋은 그래픽스 파이프라인에만 국한되지 않습니다. 따라서 디스크립터 셋을 그래픽스 파이프라인에 바인딩할지, 컴퓨트 파이프라인에 바인딩할지 지정해야 합니다. 다음 파라미터는 디스크립터가 기반으로 하는 레이아웃입니다. 그 다음 세 파라미터는 첫 번째 디스크립터 셋의 인덱스, 바인딩할 셋의 개수, 그리고 바인딩할 셋의 배열을 지정합니다. 이 부분은 잠시 후에 다시 다루겠습니다. 마지막 두 파라미터는 동적 디스크립터에 사용되는 오프셋 배열을 지정하며, 이는 다음 장에서 살펴보겠습니다. + +지금 프로그램을 실행해보면 안타깝게도 아무것도 보이지 않는 것을 알 수 있습니다. 문제는 투영 행렬에서 Y축을 뒤집었기 때문에, 정점들이 시계 방향 대신 반시계 방향으로 그려진다는 것입니다. 이로 인해 후면 컬링(backface culling)이 작동하여 지오메트리가 그려지지 않게 됩니다. `createGraphicsPipeline` 함수로 가서 `VkPipelineRasterizationStateCreateInfo`의 `frontFace`를 수정하여 이를 바로잡습니다. ```c++ rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; ``` -Run your program again and you should now see the following: +프로그램을 다시 실행하면 다음과 같은 화면을 볼 수 있습니다. ![](/images/spinning_quad.png) -The rectangle has changed into a square because the projection matrix now -corrects for aspect ratio. The `updateUniformBuffer` takes care of screen -resizing, so we don't need to recreate the descriptor set in -`recreateSwapChain`. +투영 행렬이 이제 화면 비율을 보정하기 때문에 직사각형이 정사각형으로 변경되었습니다. `updateUniformBuffer`가 화면 크기 조정을 처리하므로 `recreateSwapChain`에서 디스크립터 셋을 다시 만들 필요는 없습니다. -## Alignment requirements +## 정렬 요구사항 (Alignment requirements) -One thing we've glossed over so far is how exactly the data in the C++ structure should match with the uniform definition in the shader. It seems obvious enough to simply use the same types in both: +지금까지 간과한 한 가지는 C++ 구조체의 데이터가 셰이더의 유니폼 정의와 정확히 어떻게 일치해야 하는가입니다. 단순히 양쪽에서 같은 타입을 사용하는 것으로 충분해 보입니다. ```c++ struct UniformBufferObject { @@ -269,7 +210,7 @@ layout(binding = 0) uniform UniformBufferObject { } ubo; ``` -However, that's not all there is to it. For example, try modifying the struct and shader to look like this: +하지만 이게 전부가 아닙니다. 예를 들어, 구조체와 셰이더를 다음과 같이 수정해보세요. ```c++ struct UniformBufferObject { @@ -287,21 +228,21 @@ layout(binding = 0) uniform UniformBufferObject { } ubo; ``` -Recompile your shader and your program and run it and you'll find that the colorful square you worked so far has disappeared! That's because we haven't taken into account the *alignment requirements*. +셰이더와 프로그램을 다시 컴파일하고 실행하면, 지금까지 작업한 다채로운 사각형이 사라진 것을 발견할 것입니다! 이는 우리가 *정렬 요구사항(alignment requirements)*을 고려하지 않았기 때문입니다. -Vulkan expects the data in your structure to be aligned in memory in a specific way, for example: +Vulkan은 구조체의 데이터가 메모리에서 특정 방식으로 정렬되기를 기대합니다. 예를 들면 다음과 같습니다: -* Scalars have to be aligned by N (= 4 bytes given 32 bit floats). -* A `vec2` must be aligned by 2N (= 8 bytes) -* A `vec3` or `vec4` must be aligned by 4N (= 16 bytes) -* A nested structure must be aligned by the base alignment of its members rounded up to a multiple of 16. -* A `mat4` matrix must have the same alignment as a `vec4`. +* 스칼라는 N(32비트 부동소수점의 경우 4바이트)으로 정렬되어야 합니다. +* `vec2`는 2N(8바이트)으로 정렬되어야 합니다. +* `vec3` 또는 `vec4`는 4N(16바이트)으로 정렬되어야 합니다. +* 중첩 구조체는 멤버의 기본 정렬을 16의 배수로 올림한 값으로 정렬되어야 합니다. +* `mat4` 행렬은 `vec4`와 동일한 정렬을 가져야 합니다. -You can find the full list of alignment requirements in [the specification](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap15.html#interfaces-resources-layout). +전체 정렬 요구사항 목록은 [사양서](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap15.html#interfaces-resources-layout)에서 찾을 수 있습니다. -Our original shader with just three `mat4` fields already met the alignment requirements. As each `mat4` is 4 x 4 x 4 = 64 bytes in size, `model` has an offset of `0`, `view` has an offset of 64 and `proj` has an offset of 128. All of these are multiples of 16 and that's why it worked fine. +세 개의 `mat4` 필드만 있던 원래 셰이더는 이미 정렬 요구사항을 충족했습니다. 각 `mat4`는 4 x 4 x 4 = 64바이트 크기이므로 `model`의 오프셋은 0, `view`의 오프셋은 64, `proj`의 오프셋은 128입니다. 이들은 모두 16의 배수이므로 문제가 없었습니다. -The new structure starts with a `vec2` which is only 8 bytes in size and therefore throws off all of the offsets. Now `model` has an offset of `8`, `view` an offset of `72` and `proj` an offset of `136`, none of which are multiples of 16. To fix this problem we can use the [`alignas`](https://en.cppreference.com/w/cpp/language/alignas) specifier introduced in C++11: +새로운 구조체는 크기가 8바이트뿐인 `vec2`로 시작하여 모든 오프셋을 망가뜨립니다. 이제 `model`의 오프셋은 `8`, `view`는 `72`, `proj`는 `136`이 되어, 어느 것도 16의 배수가 아닙니다. 이 문제를 해결하기 위해 C++11에 도입된 [`alignas`](https://en.cppreference.com/w/cpp/language/alignas) 지정자를 사용할 수 있습니다. ```c++ struct UniformBufferObject { @@ -312,9 +253,9 @@ struct UniformBufferObject { }; ``` -If you now compile and run your program again you should see that the shader correctly receives its matrix values once again. +이제 프로그램을 다시 컴파일하고 실행하면 셰이더가 행렬 값을 다시 올바르게 받는 것을 볼 수 있습니다. -Luckily there is a way to not have to think about these alignment requirements *most* of the time. We can define `GLM_FORCE_DEFAULT_ALIGNED_GENTYPES` right before including GLM: +다행히도 *대부분의* 경우 이러한 정렬 요구사항에 대해 생각하지 않아도 되는 방법이 있습니다. GLM을 포함하기 직전에 `GLM_FORCE_DEFAULT_ALIGNED_GENTYPES`를 정의할 수 있습니다. ```c++ #define GLM_FORCE_RADIANS @@ -322,9 +263,9 @@ Luckily there is a way to not have to think about these alignment requirements * #include ``` -This will force GLM to use a version of `vec2` and `mat4` that has the alignment requirements already specified for us. If you add this definition then you can remove the `alignas` specifier and your program should still work. +이렇게 하면 GLM이 `vec2`와 `mat4`에 대해 이미 정렬 요구사항이 지정된 버전을 사용하도록 강제합니다. 이 정의를 추가하면 `alignas` 지정자를 제거해도 프로그램이 여전히 작동해야 합니다. -Unfortunately this method can break down if you start using nested structures. Consider the following definition in the C++ code: +안타깝게도 이 방법은 중첩 구조체를 사용하기 시작하면 문제가 될 수 있습니다. C++ 코드에서 다음과 같은 정의를 생각해보세요. ```c++ struct Foo { @@ -337,7 +278,7 @@ struct UniformBufferObject { }; ``` -And the following shader definition: +그리고 다음 셰이더 정의를 생각해보세요. ```c++ struct Foo { @@ -350,7 +291,7 @@ layout(binding = 0) uniform UniformBufferObject { } ubo; ``` -In this case `f2` will have an offset of `8` whereas it should have an offset of `16` since it is a nested structure. In this case you must specify the alignment yourself: +이 경우 `f2`는 중첩 구조체이므로 오프셋이 `16`이어야 하지만, 실제로는 `8`의 오프셋을 갖게 됩니다. 이런 경우에는 정렬을 직접 지정해야 합니다. ```c++ struct UniformBufferObject { @@ -359,7 +300,7 @@ struct UniformBufferObject { }; ``` -These gotchas are a good reason to always be explicit about alignment. That way you won't be caught offguard by the strange symptoms of alignment errors. +이러한 함정들은 항상 정렬을 명시적으로 하는 것이 좋은 이유입니다. 그렇게 하면 정렬 오류의 이상한 증상에 당황하지 않을 것입니다. ```c++ struct UniformBufferObject { @@ -369,23 +310,18 @@ struct UniformBufferObject { }; ``` -Don't forget to recompile your shader after removing the `foo` field. +`foo` 필드를 제거한 후 셰이더를 다시 컴파일하는 것을 잊지 마세요. -## Multiple descriptor sets +## 여러 개의 디스크립터 셋 (Multiple descriptor sets) -As some of the structures and function calls hinted at, it is actually possible -to bind multiple descriptor sets simultaneously. You need to specify a descriptor set layout for -each descriptor set when creating the pipeline layout. Shaders can then -reference specific descriptor sets like this: +일부 구조체와 함수 호출에서 암시되었듯이, 여러 디스크립터 셋을 동시에 바인딩하는 것도 가능합니다. 파이프라인 레이아웃을 생성할 때 각 디스크립터 셋에 대한 디스크립터 셋 레이아웃을 지정해야 합니다. 그러면 셰이더는 다음과 같이 특정 디스크립터 셋을 참조할 수 있습니다. ```c++ layout(set = 0, binding = 0) uniform UniformBufferObject { ... } ``` -You can use this feature to put descriptors that vary per-object and descriptors -that are shared into separate descriptor sets. In that case you avoid rebinding -most of the descriptors across draw calls which is potentially more efficient. +이 기능을 사용하면 객체별로 다른 디스크립터와 공유되는 디스크립터를 별도의 디스크립터 셋에 넣을 수 있습니다. 이 경우 드로우 콜 간에 대부분의 디스크립터를 다시 바인딩하는 것을 피할 수 있어 잠재적으로 더 효율적입니다. -[C++ code](/code/23_descriptor_sets.cpp) / -[Vertex shader](/code/22_shader_ubo.vert) / -[Fragment shader](/code/22_shader_ubo.frag) +[C++ 코드](/code/23_descriptor_sets.cpp) / +[정점 셰이더](/code/22_shader_ubo.vert) / +[프래그먼트 셰이더](/code/22_shader_ubo.frag) \ No newline at end of file diff --git a/ko/06_Texture_mapping/00_Images.md b/ko/06_Texture_mapping/00_Images.md index 8c9967f6..ae845ea1 100644 --- a/ko/06_Texture_mapping/00_Images.md +++ b/ko/06_Texture_mapping/00_Images.md @@ -1,74 +1,39 @@ -## Introduction - -The geometry has been colored using per-vertex colors so far, which is a rather -limited approach. In this part of the tutorial we're going to implement texture -mapping to make the geometry look more interesting. This will also allow us to -load and draw basic 3D models in a future chapter. - -Adding a texture to our application will involve the following steps: - -* Create an image object backed by device memory -* Fill it with pixels from an image file -* Create an image sampler -* Add a combined image sampler descriptor to sample colors from the texture - -We've already worked with image objects before, but those were automatically -created by the swap chain extension. This time we'll have to create one by -ourselves. Creating an image and filling it with data is similar to vertex -buffer creation. We'll start by creating a staging resource and filling it with -pixel data and then we copy this to the final image object that we'll use for -rendering. Although it is possible to create a staging image for this purpose, -Vulkan also allows you to copy pixels from a `VkBuffer` to an image and the API -for this is actually [faster on some hardware](https://developer.nvidia.com/vulkan-memory-management). -We'll first create this buffer and fill it with pixel values, and then we'll -create an image to copy the pixels to. Creating an image is not very different -from creating buffers. It involves querying the memory requirements, allocating -device memory and binding it, just like we've seen before. - -However, there is something extra that we'll have to take care of when working -with images. Images can have different *layouts* that affect how the pixels are -organized in memory. Due to the way graphics hardware works, simply storing the -pixels row by row may not lead to the best performance, for example. When -performing any operation on images, you must make sure that they have the layout -that is optimal for use in that operation. We've actually already seen some of -these layouts when we specified the render pass: - -* `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`: Optimal for presentation -* `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`: Optimal as attachment for writing -colors from the fragment shader -* `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`: Optimal as source in a transfer -operation, like `vkCmdCopyImageToBuffer` -* `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`: Optimal as destination in a transfer -operation, like `vkCmdCopyBufferToImage` -* `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`: Optimal for sampling from a shader - -One of the most common ways to transition the layout of an image is a *pipeline -barrier*. Pipeline barriers are primarily used for synchronizing access to -resources, like making sure that an image was written to before it is read, but -they can also be used to transition layouts. In this chapter we'll see how -pipeline barriers are used for this purpose. Barriers can additionally be used -to transfer queue family ownership when using `VK_SHARING_MODE_EXCLUSIVE`. - -## Image library - -There are many libraries available for loading images, and you can even write -your own code to load simple formats like BMP and PPM. In this tutorial we'll be -using the stb_image library from the [stb collection](https://github.com/nothings/stb). -The advantage of it is that all of the code is in a single file, so it doesn't -require any tricky build configuration. Download `stb_image.h` and store it in a -convenient location, like the directory where you saved GLFW and GLM. Add the -location to your include path. +## 서론 + +지금까지는 정점별(per-vertex) 색상을 사용해 지오메트리를 색칠해왔는데, 이는 다소 제한적인 방법입니다. 이번 장에서는 텍스처 매핑을 구현하여 지오메트리가 더 흥미롭게 보이도록 만들 것입니다. 이를 통해 다음 장에서는 기본적인 3D 모델을 로드하고 그릴 수도 있게 됩니다. + +애플리케이션에 텍스처를 추가하는 작업은 다음 단계를 포함합니다. + +* 디바이스 메모리를 기반으로 하는 이미지 객체 생성 +* 이미지 파일의 픽셀로 채우기 +* 이미지 샘플러 생성 +* 텍스처에서 색상을 샘플링하기 위한 결합 이미지 샘플러 디스크립터 추가 + +이전에도 이미지 객체를 다룬 적이 있지만, 그것들은 스왑체인 확장에 의해 자동으로 생성되었습니다. 이번에는 직접 하나를 만들어야 합니다. 이미지를 생성하고 데이터를 채우는 과정은 정점 버퍼 생성과 유사합니다. 먼저 스테이징 리소스를 생성하고 픽셀 데이터로 채운 다음, 렌더링에 사용할 최종 이미지 객체로 복사합니다. 이 목적으로 스테이징 이미지를 생성할 수도 있지만, Vulkan은 `VkBuffer`에서 이미지로 픽셀을 복사하는 것도 허용하며, 이 API는 [일부 하드웨어에서 실제로 더 빠릅니다](https://developer.nvidia.com/vulkan-memory-management). 우리는 먼저 이 버퍼를 생성하고 픽셀 값으로 채운 다음, 픽셀을 복사할 이미지를 생성할 것입니다. 이미지 생성은 버퍼 생성과 크게 다르지 않습니다. 이전에 보았듯이 메모리 요구사항을 쿼리하고, 디바이스 메모리를 할당하고, 바인딩하는 과정이 포함됩니다. + +하지만 이미지 작업 시에는 추가적으로 신경 써야 할 것이 있습니다. 이미지는 메모리에서 픽셀이 구성되는 방식에 영향을 미치는 다양한 *레이아웃(layouts)*을 가질 수 있습니다. 그래픽 하드웨어의 작동 방식 때문에, 단순히 픽셀을 행 단위로 저장하는 것이 최상의 성능으로 이어지지 않을 수 있습니다. 이미지에 대한 어떠한 작업을 수행할 때든, 해당 작업에 최적화된 레이아웃을 가지고 있는지 확인해야 합니다. 렌더 패스를 지정할 때 이미 이러한 레이아웃 중 일부를 본 적이 있습니다: + +* `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`: 화면 표시에 최적화 +* `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`: 프래그먼트 셰이더에서 색상을 쓰는 어태치먼트로 최적화 +* `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`: `vkCmdCopyImageToBuffer`와 같은 전송 작업의 소스로 최적화 +* `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`: `vkCmdCopyBufferToImage`와 같은 전송 작업의 대상으로 최적화 +* `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`: 셰이더에서 샘플링하기에 최적화 + +이미지의 레이아웃을 전환하는 가장 일반적인 방법 중 하나는 *파이프라인 배리어(pipeline barrier)*입니다. 파이프라인 배리어는 주로 리소스 접근을 동기화하는 데 사용됩니다. 예를 들어, 이미지를 읽기 전에 쓰기가 완료되었는지 확인하는 것과 같지만, 레이아웃을 전환하는 데에도 사용할 수 있습니다. 이번 장에서는 파이프라인 배리어가 이 목적으로 어떻게 사용되는지 볼 것입니다. 배리어는 `VK_SHARING_MODE_EXCLUSIVE`를 사용할 때 큐 패밀리 소유권을 이전하는 데에도 추가적으로 사용될 수 있습니다. + +## 이미지 라이브러리 + +이미지를 로드하기 위한 많은 라이브러리가 있으며, BMP나 PPM과 같은 간단한 포맷을 로드하는 코드를 직접 작성할 수도 있습니다. 이 튜토리얼에서는 [stb collection](https://github.com/nothings/stb)의 stb_image 라이브러리를 사용할 것입니다. 이 라이브러리의 장점은 모든 코드가 단일 파일에 있어 빌드 구성을 복잡하게 할 필요가 없다는 것입니다. `stb_image.h`를 다운로드하여 GLFW와 GLM을 저장한 디렉토리와 같이 편리한 위치에 저장하고, 해당 위치를 인클루드 경로에 추가하십시오. **Visual Studio** -Add the directory with `stb_image.h` in it to the `Additional Include -Directories` paths. +`stb_image.h`가 있는 디렉토리를 `Additional Include Directories` 경로에 추가합니다. ![](/images/include_dirs_stb.png) **Makefile** -Add the directory with `stb_image.h` to the include directories for GCC: +`stb_image.h`가 있는 디렉토리를 GCC의 인클루드 디렉토리에 추가합니다: ```text VULKAN_SDK_PATH = /home/user/VulkanSDK/x.x.x.x/x86_64 @@ -79,18 +44,16 @@ STB_INCLUDE_PATH = /home/user/libraries/stb CFLAGS = -std=c++17 -I$(VULKAN_SDK_PATH)/include -I$(STB_INCLUDE_PATH) ``` -## Loading an image +## 이미지 로딩하기 -Include the image library like this: +다음과 같이 이미지 라이브러리를 포함시킵니다: ```c++ #define STB_IMAGE_IMPLEMENTATION #include ``` -The header only defines the prototypes of the functions by default. One code -file needs to include the header with the `STB_IMAGE_IMPLEMENTATION` definition -to include the function bodies, otherwise we'll get linking errors. +헤더는 기본적으로 함수의 프로토타입만 정의합니다. 하나의 코드 파일에서 `STB_IMAGE_IMPLEMENTATION` 정의와 함께 헤더를 포함해야 함수 본문이 포함되어 링킹 오류가 발생하지 않습니다. ```c++ void initVulkan() { @@ -108,20 +71,13 @@ void createTextureImage() { } ``` -Create a new function `createTextureImage` where we'll load an image and upload -it into a Vulkan image object. We're going to use command buffers, so it should -be called after `createCommandPool`. +`createTextureImage`라는 새 함수를 만들어 이미지를 로드하고 Vulkan 이미지 객체에 업로드할 것입니다. 커맨드 버퍼를 사용할 것이므로 `createCommandPool` 이후에 호출되어야 합니다. -Create a new directory `textures` next to the `shaders` directory to store -texture images in. We're going to load an image called `texture.jpg` from that -directory. I've chosen to use the following -[CC0 licensed image](https://pixabay.com/en/statue-sculpture-fig-historically-1275469/) -resized to 512 x 512 pixels, but feel free to pick any image you want. The -library supports most common image file formats, like JPEG, PNG, BMP and GIF. +`shaders` 디렉토리 옆에 `textures`라는 새 디렉토리를 만들어 텍스처 이미지를 저장합니다. 해당 디렉토리에서 `texture.jpg`라는 이미지를 로드할 것입니다. 저는 512 x 512 픽셀로 리사이즈된 다음 [CC0 라이선스 이미지](https://pixabay.com/en/statue-sculpture-fig-historically-1275469/)를 사용하기로 했지만, 원하는 이미지를 자유롭게 선택해도 좋습니다. 이 라이브러리는 JPEG, PNG, BMP, GIF와 같은 대부분의 일반적인 이미지 파일 형식을 지원합니다. ![](/images/texture.jpg) -Loading an image with this library is really easy: +이 라이브러리로 이미지를 로드하는 것은 정말 쉽습니다: ```c++ void createTextureImage() { @@ -135,35 +91,24 @@ void createTextureImage() { } ``` -The `stbi_load` function takes the file path and number of channels to load as -arguments. The `STBI_rgb_alpha` value forces the image to be loaded with an -alpha channel, even if it doesn't have one, which is nice for consistency with -other textures in the future. The middle three parameters are outputs for the -width, height and actual number of channels in the image. The pointer that is -returned is the first element in an array of pixel values. The pixels are laid -out row by row with 4 bytes per pixel in the case of `STBI_rgb_alpha` for a -total of `texWidth * texHeight * 4` values. +`stbi_load` 함수는 파일 경로와 로드할 채널 수를 인자로 받습니다. `STBI_rgb_alpha` 값은 이미지가 알파 채널을 가지고 있지 않더라도 강제로 알파 채널과 함께 로드하도록 하여, 나중에 다른 텍스처와의 일관성을 유지하는 데 좋습니다. 가운데 세 개의 매개변수는 이미지의 너비, 높이, 실제 채널 수에 대한 출력입니다. 반환되는 포인터는 픽셀 값 배열의 첫 번째 요소입니다. 픽셀은 `STBI_rgb_alpha`의 경우 픽셀당 4바이트로 행 단위로 배치되어 총 `texWidth * texHeight * 4`개의 값을 가집니다. -## Staging buffer +## 스테이징 버퍼 -We're now going to create a buffer in host visible memory so that we can use -`vkMapMemory` and copy the pixels to it. Add variables for this temporary buffer -to the `createTextureImage` function: +이제 호스트 가시성(host visible) 메모리에 버퍼를 만들어 `vkMapMemory`를 사용하고 픽셀을 복사할 수 있도록 하겠습니다. 이 임시 버퍼를 위한 변수들을 `createTextureImage` 함수에 추가합니다: ```c++ VkBuffer stagingBuffer; VkDeviceMemory stagingBufferMemory; ``` -The buffer should be in host visible memory so that we can map it and it should -be usable as a transfer source so that we can copy it to an image later on: +버퍼는 매핑할 수 있도록 호스트 가시성 메모리에 있어야 하며, 나중에 이미지로 복사할 수 있도록 전송 소스로 사용될 수 있어야 합니다: ```c++ createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); ``` -We can then directly copy the pixel values that we got from the image loading -library to the buffer: +그런 다음 이미지 로딩 라이브러리에서 얻은 픽셀 값을 버퍼에 직접 복사할 수 있습니다: ```c++ void* data; @@ -172,26 +117,22 @@ vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data); vkUnmapMemory(device, stagingBufferMemory); ``` -Don't forget to clean up the original pixel array now: +이제 원본 픽셀 배열을 정리하는 것을 잊지 마세요: ```c++ stbi_image_free(pixels); ``` -## Texture Image +## 텍스처 이미지 -Although we could set up the shader to access the pixel values in the buffer, -it's better to use image objects in Vulkan for this purpose. Image objects will -make it easier and faster to retrieve colors by allowing us to use 2D -coordinates, for one. Pixels within an image object are known as texels and -we'll use that name from this point on. Add the following new class members: +셰이더가 버퍼의 픽셀 값에 접근하도록 설정할 수도 있지만, Vulkan에서는 이 목적으로 이미지 객체를 사용하는 것이 더 좋습니다. 이미지 객체는 2D 좌표를 사용할 수 있게 하여 색상을 더 쉽고 빠르게 가져올 수 있게 해줍니다. 이미지 객체 내의 픽셀은 텍셀(texel)이라고 하며, 지금부터 이 용어를 사용하겠습니다. 다음의 새 클래스 멤버를 추가합니다: ```c++ VkImage textureImage; VkDeviceMemory textureImageMemory; ``` -The parameters for an image are specified in a `VkImageCreateInfo` struct: +이미지의 매개변수는 `VkImageCreateInfo` 구조체에 지정됩니다: ```c++ VkImageCreateInfo imageInfo{}; @@ -204,89 +145,54 @@ imageInfo.mipLevels = 1; imageInfo.arrayLayers = 1; ``` -The image type, specified in the `imageType` field, tells Vulkan with what kind -of coordinate system the texels in the image are going to be addressed. It is -possible to create 1D, 2D and 3D images. One dimensional images can be used to -store an array of data or gradient, two dimensional images are mainly used for -textures, and three dimensional images can be used to store voxel volumes, for -example. The `extent` field specifies the dimensions of the image, basically how -many texels there are on each axis. That's why `depth` must be `1` instead of -`0`. Our texture will not be an array and we won't be using mipmapping for now. +`imageType` 필드에 지정된 이미지 타입은 Vulkan에게 이미지의 텍셀이 어떤 종류의 좌표계로 주소 지정될 것인지를 알려줍니다. 1D, 2D, 3D 이미지를 생성할 수 있습니다. 1차원 이미지는 데이터 배열이나 그래디언트를 저장하는 데 사용될 수 있고, 2차원 이미지는 주로 텍스처에 사용되며, 3차원 이미지는 복셀 볼륨 등을 저장하는 데 사용될 수 있습니다. `extent` 필드는 이미지의 차원, 즉 각 축에 얼마나 많은 텍셀이 있는지를 지정합니다. 이것이 `depth`가 `0`이 아닌 `1`이어야 하는 이유입니다. 우리 텍스처는 배열이 아니며, 지금은 밉매핑을 사용하지 않을 것입니다. ```c++ imageInfo.format = VK_FORMAT_R8G8B8A8_SRGB; ``` -Vulkan supports many possible image formats, but we should use the same format -for the texels as the pixels in the buffer, otherwise the copy operation will -fail. +Vulkan은 다양한 이미지 형식을 지원하지만, 버퍼의 픽셀과 동일한 형식을 텍셀에 사용해야 합니다. 그렇지 않으면 복사 작업이 실패합니다. ```c++ imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; ``` -The `tiling` field can have one of two values: +`tiling` 필드는 다음 두 가지 값 중 하나를 가질 수 있습니다: -* `VK_IMAGE_TILING_LINEAR`: Texels are laid out in row-major order like our -`pixels` array -* `VK_IMAGE_TILING_OPTIMAL`: Texels are laid out in an implementation defined -order for optimal access +* `VK_IMAGE_TILING_LINEAR`: 텍셀이 우리의 `pixels` 배열처럼 행 우선 순서(row-major order)로 배치됩니다. +* `VK_IMAGE_TILING_OPTIMAL`: 최적의 접근을 위해 구현에 따라 정의된 순서로 텍셀이 배치됩니다. -Unlike the layout of an image, the tiling mode cannot be changed at a later -time. If you want to be able to directly access texels in the memory of the -image, then you must use `VK_IMAGE_TILING_LINEAR`. We will be using a staging -buffer instead of a staging image, so this won't be necessary. We will be using -`VK_IMAGE_TILING_OPTIMAL` for efficient access from the shader. +이미지의 레이아웃과 달리 타일링 모드는 나중에 변경할 수 없습니다. 이미지 메모리에서 텍셀에 직접 접근하고 싶다면 `VK_IMAGE_TILING_LINEAR`를 사용해야 합니다. 우리는 스테이징 이미지 대신 스테이징 버퍼를 사용할 것이므로 이것은 필요하지 않습니다. 셰이더에서의 효율적인 접근을 위해 `VK_IMAGE_TILING_OPTIMAL`을 사용할 것입니다. ```c++ imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; ``` -There are only two possible values for the `initialLayout` of an image: +이미지의 `initialLayout`에는 두 가지 가능한 값만 있습니다: -* `VK_IMAGE_LAYOUT_UNDEFINED`: Not usable by the GPU and the very first -transition will discard the texels. -* `VK_IMAGE_LAYOUT_PREINITIALIZED`: Not usable by the GPU, but the first -transition will preserve the texels. +* `VK_IMAGE_LAYOUT_UNDEFINED`: GPU에서 사용할 수 없으며, 첫 번째 전환 시 텍셀이 폐기됩니다. +* `VK_IMAGE_LAYOUT_PREINITIALIZED`: GPU에서 사용할 수 없지만, 첫 번째 전환 시 텍셀이 보존됩니다. -There are few situations where it is necessary for the texels to be preserved -during the first transition. One example, however, would be if you wanted to use -an image as a staging image in combination with the `VK_IMAGE_TILING_LINEAR` -layout. In that case, you'd want to upload the texel data to it and then -transition the image to be a transfer source without losing the data. In our -case, however, we're first going to transition the image to be a transfer -destination and then copy texel data to it from a buffer object, so we don't -need this property and can safely use `VK_IMAGE_LAYOUT_UNDEFINED`. +첫 번째 전환 중에 텍셀이 보존되어야 하는 경우는 거의 없습니다. 그러나 한 가지 예는 `VK_IMAGE_TILING_LINEAR` 레이아웃과 함께 이미지를 스테이징 이미지로 사용하려는 경우입니다. 이 경우 텍셀 데이터를 업로드한 다음 데이터를 잃지 않고 이미지를 전송 소스로 전환하고 싶을 것입니다. 하지만 우리의 경우에는 먼저 이미지를 전송 대상으로 전환한 다음 버퍼 객체에서 텍셀 데이터를 복사할 것이므로 이 속성이 필요 없으며 `VK_IMAGE_LAYOUT_UNDEFINED`를 안전하게 사용할 수 있습니다. ```c++ imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; ``` -The `usage` field has the same semantics as the one during buffer creation. The -image is going to be used as destination for the buffer copy, so it should be -set up as a transfer destination. We also want to be able to access the image -from the shader to color our mesh, so the usage should include -`VK_IMAGE_USAGE_SAMPLED_BIT`. +`usage` 필드는 버퍼 생성 시와 동일한 의미를 갖습니다. 이미지는 버퍼 복사의 대상으로 사용될 것이므로 전송 대상으로 설정되어야 합니다. 또한 셰이더에서 이미지에 접근하여 메쉬를 색칠하고 싶으므로 `VK_IMAGE_USAGE_SAMPLED_BIT`를 포함해야 합니다. ```c++ imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; ``` -The image will only be used by one queue family: the one that supports graphics -(and therefore also) transfer operations. +이미지는 그래픽(따라서 전송도) 작업을 지원하는 단일 큐 패밀리에서만 사용될 것입니다. ```c++ imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; imageInfo.flags = 0; // Optional ``` -The `samples` flag is related to multisampling. This is only relevant for images -that will be used as attachments, so stick to one sample. There are some -optional flags for images that are related to sparse images. Sparse images are -images where only certain regions are actually backed by memory. If you were -using a 3D texture for a voxel terrain, for example, then you could use this to -avoid allocating memory to store large volumes of "air" values. We won't be -using it in this tutorial, so leave it to its default value of `0`. +`samples` 플래그는 멀티샘플링과 관련이 있습니다. 이것은 어태치먼트로 사용될 이미지에만 관련이 있으므로 하나의 샘플로 고정합니다. 희소 이미지(sparse images)와 관련된 몇 가지 선택적 플래그가 있습니다. 희소 이미지는 특정 영역만 실제로 메모리에 의해 지원되는 이미지입니다. 예를 들어, 복셀 지형에 3D 텍스처를 사용하는 경우, 이를 사용하여 "공기" 값의 큰 볼륨을 저장하기 위한 메모리 할당을 피할 수 있습니다. 이 튜토리얼에서는 사용하지 않을 것이므로 기본값인 `0`으로 둡니다. ```c++ if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) { @@ -294,13 +200,7 @@ if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) { } ``` -The image is created using `vkCreateImage`, which doesn't have any particularly -noteworthy parameters. It is possible that the `VK_FORMAT_R8G8B8A8_SRGB` format -is not supported by the graphics hardware. You should have a list of acceptable -alternatives and go with the best one that is supported. However, support for -this particular format is so widespread that we'll skip this step. Using -different formats would also require annoying conversions. We will get back to -this in the depth buffer chapter, where we'll implement such a system. +이미지는 `vkCreateImage`를 사용하여 생성되며, 특별히 주목할 만한 매개변수는 없습니다. `VK_FORMAT_R8G8B8A8_SRGB` 형식이 그래픽 하드웨어에서 지원되지 않을 수 있습니다. 수용 가능한 대안 목록을 가지고 지원되는 최상의 것을 선택해야 합니다. 그러나 이 특정 형식에 대한 지원은 매우 널리 퍼져 있으므로 이 단계를 건너뛰겠습니다. 다른 형식을 사용하려면 번거로운 변환이 필요할 수도 있습니다. 깊이 버퍼 장에서 이러한 시스템을 구현할 때 이 문제로 다시 돌아올 것입니다. ```c++ VkMemoryRequirements memRequirements; @@ -318,15 +218,9 @@ if (vkAllocateMemory(device, &allocInfo, nullptr, &textureImageMemory) != VK_SUC vkBindImageMemory(device, textureImage, textureImageMemory, 0); ``` -Allocating memory for an image works in exactly the same way as allocating -memory for a buffer. Use `vkGetImageMemoryRequirements` instead of -`vkGetBufferMemoryRequirements`, and use `vkBindImageMemory` instead of -`vkBindBufferMemory`. +이미지용 메모리 할당은 버퍼용 메모리 할당과 정확히 동일한 방식으로 작동합니다. `vkGetBufferMemoryRequirements` 대신 `vkGetImageMemoryRequirements`를 사용하고, `vkBindBufferMemory` 대신 `vkBindImageMemory`를 사용합니다. -This function is already getting quite large and there'll be a need to create -more images in later chapters, so we should abstract image creation into a -`createImage` function, like we did for buffers. Create the function and move -the image object creation and memory allocation to it: +이 함수는 이미 상당히 커지고 있으며, 이후 장에서 더 많은 이미지를 생성할 필요가 있으므로, 버퍼에서 했던 것처럼 이미지 생성을 `createImage` 함수로 추상화해야 합니다. 함수를 만들고 이미지 객체 생성 및 메모리 할당을 그곳으로 옮깁니다: ```c++ void createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) { @@ -365,11 +259,9 @@ void createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling } ``` -I've made the width, height, format, tiling mode, usage, and memory properties -parameters, because these will all vary between the images we'll be creating -throughout this tutorial. +너비, 높이, 형식, 타일링 모드, 사용법 및 메모리 속성을 매개변수로 만들었는데, 이는 이 튜토리얼 전체에서 생성할 이미지마다 달라질 것이기 때문입니다. -The `createTextureImage` function can now be simplified to: +이제 `createTextureImage` 함수를 다음과 같이 단순화할 수 있습니다: ```c++ void createTextureImage() { @@ -396,11 +288,9 @@ void createTextureImage() { } ``` -## Layout transitions +## 레이아웃 전환 -The function we're going to write now involves recording and executing a command -buffer again, so now's a good time to move that logic into a helper function or -two: +지금 작성할 함수는 커맨드 버퍼를 다시 기록하고 실행하는 것을 포함하므로, 이 로직을 한두 개의 헬퍼 함수로 옮기기에 좋은 시점입니다: ```c++ VkCommandBuffer beginSingleTimeCommands() { @@ -437,8 +327,7 @@ void endSingleTimeCommands(VkCommandBuffer commandBuffer) { } ``` -The code for these functions is based on the existing code in `copyBuffer`. You -can now simplify that function to: +이 함수들의 코드는 `copyBuffer`의 기존 코드를 기반으로 합니다. 이제 해당 함수를 다음과 같이 단순화할 수 있습니다: ```c++ void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { @@ -452,10 +341,7 @@ void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { } ``` -If we were still using buffers, then we could now write a function to record and -execute `vkCmdCopyBufferToImage` to finish the job, but this command requires -the image to be in the right layout first. Create a new function to handle -layout transitions: +만약 여전히 버퍼를 사용하고 있었다면, `vkCmdCopyBufferToImage`를 기록하고 실행하는 함수를 작성하여 작업을 마칠 수 있었겠지만, 이 명령어는 이미지가 먼저 올바른 레이아웃에 있어야 합니다. 레이아웃 전환을 처리할 새 함수를 만듭니다: ```c++ void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout) { @@ -465,12 +351,7 @@ void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayo } ``` -One of the most common ways to perform layout transitions is using an *image -memory barrier*. A pipeline barrier like that is generally used to synchronize -access to resources, like ensuring that a write to a buffer completes before -reading from it, but it can also be used to transition image layouts and -transfer queue family ownership when `VK_SHARING_MODE_EXCLUSIVE` is used. There -is an equivalent *buffer memory barrier* to do this for buffers. +레이아웃 전환을 수행하는 가장 일반적인 방법 중 하나는 *이미지 메모리 배리어(image memory barrier)*를 사용하는 것입니다. 이러한 파이프라인 배리어는 일반적으로 리소스 접근을 동기화하는 데 사용됩니다(예: 버퍼에서 읽기 전에 쓰기가 완료되었는지 확인). 하지만 `VK_SHARING_MODE_EXCLUSIVE`가 사용될 때 이미지 레이아웃을 전환하고 큐 패밀리 소유권을 이전하는 데에도 사용할 수 있습니다. 버퍼에 대해 이를 수행하기 위한 동등한 *버퍼 메모리 배리어*가 있습니다. ```c++ VkImageMemoryBarrier barrier{}; @@ -479,18 +360,14 @@ barrier.oldLayout = oldLayout; barrier.newLayout = newLayout; ``` -The first two fields specify layout transition. It is possible to use -`VK_IMAGE_LAYOUT_UNDEFINED` as `oldLayout` if you don't care about the existing -contents of the image. +첫 두 필드는 레이아웃 전환을 지정합니다. 이미지의 기존 내용에 신경 쓰지 않는다면 `oldLayout`으로 `VK_IMAGE_LAYOUT_UNDEFINED`를 사용할 수 있습니다. ```c++ barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; ``` -If you are using the barrier to transfer queue family ownership, then these two -fields should be the indices of the queue families. They must be set to -`VK_QUEUE_FAMILY_IGNORED` if you don't want to do this (not the default value!). +큐 패밀리 소유권을 이전하기 위해 배리어를 사용하는 경우, 이 두 필드는 큐 패밀리의 인덱스가 되어야 합니다. 이를 원하지 않는 경우 (기본값이 아님!) `VK_QUEUE_FAMILY_IGNORED`로 설정해야 합니다. ```c++ barrier.image = image; @@ -501,21 +378,14 @@ barrier.subresourceRange.baseArrayLayer = 0; barrier.subresourceRange.layerCount = 1; ``` -The `image` and `subresourceRange` specify the image that is affected and the -specific part of the image. Our image is not an array and does not have mipmapping -levels, so only one level and layer are specified. +`image`와 `subresourceRange`는 영향을 받는 이미지와 이미지의 특정 부분을 지정합니다. 우리 이미지는 배열이 아니고 밉매핑 레벨도 없으므로, 하나의 레벨과 레이어만 지정됩니다. ```c++ barrier.srcAccessMask = 0; // TODO barrier.dstAccessMask = 0; // TODO ``` -Barriers are primarily used for synchronization purposes, so you must specify -which types of operations that involve the resource must happen before the -barrier, and which operations that involve the resource must wait on the -barrier. We need to do that despite already using `vkQueueWaitIdle` to manually -synchronize. The right values depend on the old and new layout, so we'll get -back to this once we've figured out which transitions we're going to use. +배리어는 주로 동기화 목적으로 사용되므로, 배리어 이전에 발생해야 하는 리소스 관련 작업 유형과 배리어를 기다려야 하는 리소스 관련 작업 유형을 지정해야 합니다. 이미 `vkQueueWaitIdle`을 사용하여 수동으로 동기화하고 있음에도 불구하고 이를 수행해야 합니다. 올바른 값은 이전 및 새 레이아웃에 따라 달라지므로, 어떤 전환을 사용할지 파악한 후에 이 부분으로 돌아오겠습니다. ```c++ vkCmdPipelineBarrier( @@ -528,36 +398,15 @@ vkCmdPipelineBarrier( ); ``` -All types of pipeline barriers are submitted using the same function. The first -parameter after the command buffer specifies in which pipeline stage the -operations occur that should happen before the barrier. The second parameter -specifies the pipeline stage in which operations will wait on the barrier. The -pipeline stages that you are allowed to specify before and after the barrier -depend on how you use the resource before and after the barrier. The allowed -values are listed in [this table](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-access-types-supported) -of the specification. For example, if you're going to read from a uniform after -the barrier, you would specify a usage of `VK_ACCESS_UNIFORM_READ_BIT` and the -earliest shader that will read from the uniform as pipeline stage, for example -`VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT`. It would not make sense to specify -a non-shader pipeline stage for this type of usage and the validation layers -will warn you when you specify a pipeline stage that does not match the type of -usage. +모든 유형의 파이프라인 배리어는 동일한 함수를 사용하여 제출됩니다. 커맨드 버퍼 다음의 첫 번째 매개변수는 배리어 이전에 발생해야 하는 작업이 일어나는 파이프라인 스테이지를 지정합니다. 두 번째 매개변수는 배리어를 기다릴 작업이 일어나는 파이프라인 스테이지를 지정합니다. 배리어 전후에 지정할 수 있는 파이프라인 스테이지는 배리어 전후에 리소스를 어떻게 사용하느냐에 따라 다릅니다. 허용되는 값은 사양의 [이 표](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-access-types-supported)에 나열되어 있습니다. 예를 들어, 배리어 이후에 유니폼에서 읽으려는 경우 `VK_ACCESS_UNIFORM_READ_BIT` 사용법과 유니폼을 읽을 가장 빠른 셰이더(예: `VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT`)를 파이프라인 스테이지로 지정합니다. 이 유형의 사용법에 대해 비-셰이더 파이프라인 스테이지를 지정하는 것은 의미가 없으며, 유효성 검사 계층은 사용 유형과 일치하지 않는 파이프라인 스테이지를 지정할 때 경고합니다. -The third parameter is either `0` or `VK_DEPENDENCY_BY_REGION_BIT`. The latter -turns the barrier into a per-region condition. That means that the -implementation is allowed to already begin reading from the parts of a resource -that were written so far, for example. +세 번째 매개변수는 `0` 또는 `VK_DEPENDENCY_BY_REGION_BIT`입니다. 후자는 배리어를 영역별 조건으로 바꿉니다. 이는 구현이 예를 들어, 이미 쓰여진 리소스의 부분부터 읽기 시작할 수 있음을 의미합니다. -The last three pairs of parameters reference arrays of pipeline barriers of the -three available types: memory barriers, buffer memory barriers, and image memory -barriers like the one we're using here. Note that we're not using the `VkFormat` -parameter yet, but we'll be using that one for special transitions in the depth -buffer chapter. +마지막 세 쌍의 매개변수는 사용 가능한 세 가지 유형의 파이프라인 배리어(메모리 배리어, 버퍼 메모리 배리어, 그리고 우리가 사용하는 이미지 메모리 배리어)의 배열을 참조합니다. 아직 `VkFormat` 매개변수를 사용하지 않았지만, 깊이 버퍼 장에서 특별한 전환을 위해 사용할 것입니다. -## Copying buffer to image +## 버퍼를 이미지로 복사하기 -Before we get back to `createTextureImage`, we're going to write one more helper -function: `copyBufferToImage`: +`createTextureImage`로 돌아가기 전에, `copyBufferToImage`라는 헬퍼 함수를 하나 더 작성하겠습니다: ```c++ void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) { @@ -567,9 +416,7 @@ void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t } ``` -Just like with buffer copies, you need to specify which part of the buffer is -going to be copied to which part of the image. This happens through -`VkBufferImageCopy` structs: +버퍼 복사와 마찬가지로, 버퍼의 어느 부분을 이미지의 어느 부분으로 복사할지 지정해야 합니다. 이것은 `VkBufferImageCopy` 구조체를 통해 이루어집니다: ```c++ VkBufferImageCopy region{}; @@ -590,16 +437,9 @@ region.imageExtent = { }; ``` -Most of these fields are self-explanatory. The `bufferOffset` specifies the byte -offset in the buffer at which the pixel values start. The `bufferRowLength` and -`bufferImageHeight` fields specify how the pixels are laid out in memory. For -example, you could have some padding bytes between rows of the image. Specifying -`0` for both indicates that the pixels are simply tightly packed like they are -in our case. The `imageSubresource`, `imageOffset` and `imageExtent` fields -indicate to which part of the image we want to copy the pixels. +이 필드들의 대부분은 자명합니다. `bufferOffset`은 픽셀 값이 시작되는 버퍼의 바이트 오프셋을 지정합니다. `bufferRowLength`와 `bufferImageHeight` 필드는 픽셀이 메모리에 어떻게 배치되는지를 지정합니다. 예를 들어, 이미지의 행 사이에 패딩 바이트가 있을 수 있습니다. 두 필드 모두 `0`으로 지정하면 우리 경우처럼 픽셀이 단순히 빽빽하게 채워져 있음을 나타냅니다. `imageSubresource`, `imageOffset`, `imageExtent` 필드는 픽셀을 이미지의 어느 부분으로 복사할지를 나타냅니다. -Buffer to image copy operations are enqueued using the `vkCmdCopyBufferToImage` -function: +버퍼에서 이미지로의 복사 작업은 `vkCmdCopyBufferToImage` 함수를 사용하여 큐에 추가됩니다: ```c++ vkCmdCopyBufferToImage( @@ -612,57 +452,40 @@ vkCmdCopyBufferToImage( ); ``` -The fourth parameter indicates which layout the image is currently using. I'm -assuming here that the image has already been transitioned to the layout that is -optimal for copying pixels to. Right now we're only copying one chunk of pixels -to the whole image, but it's possible to specify an array of `VkBufferImageCopy` -to perform many different copies from this buffer to the image in one operation. +네 번째 매개변수는 이미지가 현재 사용하고 있는 레이아웃을 나타냅니다. 여기서는 이미지가 픽셀을 복사하기에 최적화된 레이아웃으로 이미 전환되었다고 가정합니다. 지금은 픽셀 덩어리 하나를 전체 이미지에 복사하고 있지만, `VkBufferImageCopy`의 배열을 지정하여 이 버퍼에서 이미지로 여러 다른 복사를 한 번의 작업으로 수행할 수도 있습니다. -## Preparing the texture image +## 텍스처 이미지 준비하기 -We now have all of the tools we need to finish setting up the texture image, so -we're going back to the `createTextureImage` function. The last thing we did -there was creating the texture image. The next step is to copy the staging -buffer to the texture image. This involves two steps: +이제 텍스처 이미지 설정을 마치는 데 필요한 모든 도구를 갖추었으므로 `createTextureImage` 함수로 돌아갑니다. 거기서 마지막으로 한 일은 텍스처 이미지를 생성하는 것이었습니다. 다음 단계는 스테이징 버퍼를 텍스처 이미지로 복사하는 것입니다. 이는 두 단계로 이루어집니다: -* Transition the texture image to `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL` -* Execute the buffer to image copy operation +* 텍스처 이미지를 `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`로 전환 +* 버퍼에서 이미지로의 복사 작업 실행 -This is easy to do with the functions we just created: +방금 만든 함수들로 이 작업은 쉽게 할 수 있습니다: ```c++ transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast(texHeight)); ``` -The image was created with the `VK_IMAGE_LAYOUT_UNDEFINED` layout, so that one -should be specified as old layout when transitioning `textureImage`. Remember -that we can do this because we don't care about its contents before performing -the copy operation. +이미지는 `VK_IMAGE_LAYOUT_UNDEFINED` 레이아웃으로 생성되었으므로, `textureImage`를 전환할 때 이전 레이아웃으로 지정되어야 합니다. 복사 작업을 수행하기 전에 그 내용에 신경 쓰지 않기 때문에 이렇게 할 수 있다는 것을 기억하세요. -To be able to start sampling from the texture image in the shader, we need one -last transition to prepare it for shader access: +셰이더에서 텍스처 이미지로부터 샘플링을 시작하려면, 셰이더 접근을 위해 준비하는 마지막 전환이 한 번 더 필요합니다: ```c++ transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); ``` -## Transition barrier masks +## 전환 배리어 마스크 -If you run your application with validation layers enabled now, then you'll see that -it complains about the access masks and pipeline stages in -`transitionImageLayout` being invalid. We still need to set those based on the -layouts in the transition. +이제 유효성 검사 계층을 활성화한 상태로 애플리케이션을 실행하면, `transitionImageLayout`의 접근 마스크와 파이프라인 스테이지가 유효하지 않다고 불평하는 것을 볼 수 있습니다. 전환의 레이아웃에 따라 이들을 아직 설정해야 합니다. -There are two transitions we need to handle: +처리해야 할 두 가지 전환이 있습니다: -* Undefined → transfer destination: transfer writes that don't need to wait on -anything -* Transfer destination → shader reading: shader reads should wait on transfer -writes, specifically the shader reads in the fragment shader, because that's -where we're going to use the texture +* 정의되지 않음 → 전송 대상: 어떤 것도 기다릴 필요 없는 전송 쓰기 +* 전송 대상 → 셰이더 읽기: 셰이더 읽기는 전송 쓰기를 기다려야 하며, 특히 프래그먼트 셰이더에서의 셰이더 읽기를 기다려야 합니다. 왜냐하면 우리가 텍스처를 사용할 곳이 거기이기 때문입니다. -These rules are specified using the following access masks and pipeline stages: +이러한 규칙은 다음 접근 마스크와 파이프라인 스테이지를 사용하여 지정됩니다: ```c++ VkPipelineStageFlags sourceStage; @@ -694,51 +517,21 @@ vkCmdPipelineBarrier( ); ``` -As you can see in the aforementioned table, transfer writes must occur in the -pipeline transfer stage. Since the writes don't have to wait on anything, you -may specify an empty access mask and the earliest possible pipeline stage -`VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT` for the pre-barrier operations. It should be -noted that `VK_PIPELINE_STAGE_TRANSFER_BIT` is not a *real* stage within the -graphics and compute pipelines. It is more of a pseudo-stage where transfers -happen. See [the documentation](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#VkPipelineStageFlagBits) -for more information and other examples of pseudo-stages. - -The image will be written in the same pipeline stage and subsequently read by -the fragment shader, which is why we specify shader reading access in the -fragment shader pipeline stage. - -If we need to do more transitions in the future, then we'll extend the function. -The application should now run successfully, although there are of course no -visual changes yet. - -One thing to note is that command buffer submission results in implicit -`VK_ACCESS_HOST_WRITE_BIT` synchronization at the beginning. Since the -`transitionImageLayout` function executes a command buffer with only a single -command, you could use this implicit synchronization and set `srcAccessMask` to -`0` if you ever needed a `VK_ACCESS_HOST_WRITE_BIT` dependency in a layout -transition. It's up to you if you want to be explicit about it or not, but I'm -personally not a fan of relying on these OpenGL-like "hidden" operations. - -There is actually a special type of image layout that supports all operations, -`VK_IMAGE_LAYOUT_GENERAL`. The problem with it, of course, is that it doesn't -necessarily offer the best performance for any operation. It is required for -some special cases, like using an image as both input and output, or for reading -an image after it has left the preinitialized layout. - -All of the helper functions that submit commands so far have been set up to -execute synchronously by waiting for the queue to become idle. For practical -applications it is recommended to combine these operations in a single command -buffer and execute them asynchronously for higher throughput, especially the -transitions and copy in the `createTextureImage` function. Try to experiment -with this by creating a `setupCommandBuffer` that the helper functions record -commands into, and add a `flushSetupCommands` to execute the commands that have -been recorded so far. It's best to do this after the texture mapping works to -check if the texture resources are still set up correctly. - -## Cleanup - -Finish the `createTextureImage` function by cleaning up the staging buffer and -its memory at the end: +앞서 언급한 표에서 볼 수 있듯이, 전송 쓰기는 파이프라인 전송 스테이지에서 발생해야 합니다. 쓰기는 어떤 것도 기다릴 필요가 없으므로, 빈 접근 마스크와 배리어 이전 작업을 위한 가장 빠른 파이프라인 스테이지인 `VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT`를 지정할 수 있습니다. `VK_PIPELINE_STAGE_TRANSFER_BIT`는 그래픽 및 컴퓨트 파이프라인 내의 *실제* 스테이지가 아니라는 점에 유의해야 합니다. 이것은 전송이 일어나는 의사-스테이지(pseudo-stage)에 가깝습니다. 더 많은 정보와 다른 의사-스테이지의 예는 [문서](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#VkPipelineStageFlagBits)를 참조하십시오. + +이미지는 동일한 파이프라인 스테이지에서 쓰여지고, 이후 프래그먼트 셰이더에 의해 읽혀질 것이므로, 프래그먼트 셰이더 파이프라인 스테이지에서 셰이더 읽기 접근을 지정합니다. + +만약 앞으로 더 많은 전환이 필요하다면, 이 함수를 확장할 것입니다. 애플리케이션은 이제 성공적으로 실행되어야 하지만, 물론 아직 시각적인 변화는 없습니다. + +한 가지 주목할 점은 커맨드 버퍼 제출이 시작 시 암시적인 `VK_ACCESS_HOST_WRITE_BIT` 동기화를 초래한다는 것입니다. `transitionImageLayout` 함수는 단일 명령만 있는 커맨드 버퍼를 실행하므로, 레이아웃 전환에서 `VK_ACCESS_HOST_WRITE_BIT` 의존성이 필요할 경우 이 암시적 동기화를 사용할 수 있습니다. 이를 명시적으로 할지 여부는 여러분에게 달려있지만, 저는 개인적으로 이러한 OpenGL과 같은 "숨겨진" 작업에 의존하는 것을 좋아하지 않습니다. + +실제로 모든 작업을 지원하는 특별한 유형의 이미지 레이아웃인 `VK_IMAGE_LAYOUT_GENERAL`이 있습니다. 물론 이것의 문제점은 어떤 작업에 대해서도 반드시 최상의 성능을 제공하지는 않는다는 것입니다. 이미지를 입력과 출력으로 동시에 사용하거나, 사전 초기화된 레이아웃을 벗어난 이미지를 읽는 것과 같은 일부 특수한 경우에 필요합니다. + +지금까지 명령을 제출하는 모든 헬퍼 함수는 큐가 유휴 상태가 될 때까지 기다림으로써 동기적으로 실행되도록 설정되었습니다. 실제 애플리케이션에서는 이러한 작업들을 단일 커맨드 버퍼에 결합하고 더 높은 처리량을 위해 비동기적으로 실행하는 것이 권장됩니다. 특히 `createTextureImage` 함수의 전환 및 복사 작업이 그렇습니다. 헬퍼 함수들이 명령을 기록하는 `setupCommandBuffer`를 만들고, 지금까지 기록된 명령을 실행하는 `flushSetupCommands`를 추가하여 이를 실험해 보세요. 텍스처 매핑이 작동한 후에 텍스처 리소스가 여전히 올바르게 설정되었는지 확인하는 것이 가장 좋습니다. + +## 정리 + +`createTextureImage` 함수를 마무리하기 위해, 끝에서 스테이징 버퍼와 그 메모리를 정리합니다: ```c++ transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); @@ -748,7 +541,7 @@ its memory at the end: } ``` -The main texture image is used until the end of the program: +주 텍스처 이미지는 프로그램이 끝날 때까지 사용됩니다: ```c++ void cleanup() { @@ -761,9 +554,8 @@ void cleanup() { } ``` -The image now contains the texture, but we still need a way to access it from -the graphics pipeline. We'll work on that in the next chapter. +이제 이미지는 텍스처를 포함하고 있지만, 그래픽 파이프라인에서 접근할 방법이 아직 필요합니다. 다음 장에서 그 작업을 할 것입니다. -[C++ code](/code/24_texture_image.cpp) / -[Vertex shader](/code/22_shader_ubo.vert) / -[Fragment shader](/code/22_shader_ubo.frag) +[C++ 코드](/code/24_texture_image.cpp) / +[정점 셰이더](/code/22_shader_ubo.vert) / +[프래그먼트 셰이더](/code/22_shader_ubo.frag) \ No newline at end of file diff --git a/ko/06_Texture_mapping/01_Image_view_and_sampler.md b/ko/06_Texture_mapping/01_Image_view_and_sampler.md index 9d98c9e4..2f68f105 100644 --- a/ko/06_Texture_mapping/01_Image_view_and_sampler.md +++ b/ko/06_Texture_mapping/01_Image_view_and_sampler.md @@ -1,16 +1,10 @@ -In this chapter we're going to create two more resources that are needed for the -graphics pipeline to sample an image. The first resource is one that we've -already seen before while working with the swap chain images, but the second one -is new - it relates to how the shader will read texels from the image. +이번 장에서는 그래픽스 파이프라인이 이미지를 샘플링하는 데 필요한 두 가지 리소스를 더 만들 것입니다. 첫 번째 리소스는 스왑 체인 이미지에서 이미 다루었던 것이지만, 두 번째 리소스는 새로운 것으로 셰이더가 이미지에서 텍셀(texel)을 어떻게 읽을지와 관련이 있습니다. -## Texture image view +## 텍스처 이미지 뷰 -We've seen before, with the swap chain images and the framebuffer, that images -are accessed through image views rather than directly. We will also need to -create such an image view for the texture image. +우리는 이전에 스왑 체인 이미지와 프레임버퍼에서 이미지가 직접 접근되는 대신 이미지 뷰를 통해 접근된다는 것을 보았습니다. 텍스처 이미지에 대해서도 이러한 이미지 뷰를 만들어야 합니다. -Add a class member to hold a `VkImageView` for the texture image and create a -new function `createTextureImageView` where we'll create it: +텍스처 이미지의 `VkImageView`를 저장할 클래스 멤버를 추가하고, 이를 생성할 `createTextureImageView` 함수를 새로 만듭니다. ```c++ VkImageView textureImageView; @@ -32,8 +26,7 @@ void createTextureImageView() { } ``` -The code for this function can be based directly on `createImageViews`. The only -two changes you have to make are the `format` and the `image`: +이 함수의 코드는 `createImageViews` 함수를 거의 그대로 가져와서 만들 수 있습니다. 변경해야 할 부분은 `format`과 `image` 단 두 가지뿐입니다. ```c++ VkImageViewCreateInfo viewInfo{}; @@ -48,9 +41,7 @@ viewInfo.subresourceRange.baseArrayLayer = 0; viewInfo.subresourceRange.layerCount = 1; ``` -I've left out the explicit `viewInfo.components` initialization, because -`VK_COMPONENT_SWIZZLE_IDENTITY` is defined as `0` anyway. Finish creating the -image view by calling `vkCreateImageView`: +`VK_COMPONENT_SWIZZLE_IDENTITY`가 어차피 `0`으로 정의되어 있으므로, `viewInfo.components`의 명시적인 초기화는 생략했습니다. `vkCreateImageView`를 호출하여 이미지 뷰 생성을 마칩니다. ```c++ if (vkCreateImageView(device, &viewInfo, nullptr, &textureImageView) != VK_SUCCESS) { @@ -58,8 +49,7 @@ if (vkCreateImageView(device, &viewInfo, nullptr, &textureImageView) != VK_SUCCE } ``` -Because so much of the logic is duplicated from `createImageViews`, you may wish -to abstract it into a new `createImageView` function: +`createImageViews`와 많은 로직이 중복되므로, 이를 새로운 `createImageView` 함수로 추상화할 수 있습니다. ```c++ VkImageView createImageView(VkImage image, VkFormat format) { @@ -83,7 +73,7 @@ VkImageView createImageView(VkImage image, VkFormat format) { } ``` -The `createTextureImageView` function can now be simplified to: +이제 `createTextureImageView` 함수는 다음과 같이 단순화할 수 있습니다. ```c++ void createTextureImageView() { @@ -91,7 +81,7 @@ void createTextureImageView() { } ``` -And `createImageViews` can be simplified to: +그리고 `createImageViews`도 다음과 같이 단순화됩니다. ```c++ void createImageViews() { @@ -103,8 +93,7 @@ void createImageViews() { } ``` -Make sure to destroy the image view at the end of the program, right before -destroying the image itself: +프로그램이 끝날 때, 이미지 자체를 파괴하기 직전에 이미지 뷰를 파괴하도록 해야 합니다. ```c++ void cleanup() { @@ -114,48 +103,31 @@ void cleanup() { vkDestroyImage(device, textureImage, nullptr); vkFreeMemory(device, textureImageMemory, nullptr); + ... +} ``` -## Samplers +## 샘플러 -It is possible for shaders to read texels directly from images, but that is not -very common when they are used as textures. Textures are usually accessed -through samplers, which will apply filtering and transformations to compute the -final color that is retrieved. +셰이더가 이미지에서 직접 텍셀을 읽는 것도 가능하지만, 이미지가 텍스처로 사용될 때는 흔한 방식이 아닙니다. 텍스처는 보통 샘플러를 통해 접근되며, 샘플러는 최종적으로 검색될 색상을 계산하기 위해 필터링과 변환을 적용합니다. -These filters are helpful to deal with problems like oversampling. Consider a -texture that is mapped to geometry with more fragments than texels. If you -simply took the closest texel for the texture coordinate in each fragment, then -you would get a result like the first image: +이러한 필터들은 오버샘플링(oversampling) 같은 문제를 해결하는 데 유용합니다. 텍셀보다 더 많은 프래그먼트가 있는 지오메트리에 텍스처가 매핑되는 경우를 생각해보세요. 만약 각 프래그먼트의 텍스처 좌표에 가장 가까운 텍셀을 단순히 가져온다면, 아래 첫 번째 이미지와 같은 결과를 얻게 될 것입니다. ![](/images/texture_filtering.png) -If you combined the 4 closest texels through linear interpolation, then you -would get a smoother result like the one on the right. Of course your -application may have art style requirements that fit the left style more (think -Minecraft), but the right is preferred in conventional graphics applications. A -sampler object automatically applies this filtering for you when reading a color -from the texture. +만약 가장 가까운 4개의 텍셀을 선형 보간(linear interpolation)으로 혼합한다면, 오른쪽 이미지처럼 더 부드러운 결과를 얻을 수 있습니다. 물론 애플리케이션의 아트 스타일에 따라 왼쪽 스타일(마인크래프트처럼)이 더 적합할 수도 있지만, 일반적인 그래픽스 애플리케이션에서는 오른쪽 방식이 선호됩니다. 샘플러 객체는 텍스처에서 색상을 읽을 때 이 필터링을 자동으로 적용해줍니다. -Undersampling is the opposite problem, where you have more texels than -fragments. This will lead to artifacts when sampling high frequency patterns -like a checkerboard texture at a sharp angle: +언더샘플링(undersampling)은 그 반대의 문제로, 프래그먼트보다 텍셀이 더 많은 경우입니다. 이는 체커보드 텍스처처럼 고주파 패턴을 예리한 각도에서 샘플링할 때 아티팩트를 유발합니다. ![](/images/anisotropic_filtering.png) -As shown in the left image, the texture turns into a blurry mess in the -distance. The solution to this is [anisotropic filtering](https://en.wikipedia.org/wiki/Anisotropic_filtering), -which can also be applied automatically by a sampler. +왼쪽 이미지에서 보듯이, 텍스처가 멀어질수록 흐릿한 덩어리로 변합니다. 이에 대한 해결책은 [비등방성 필터링(anisotropic filtering)](https://ko.wikipedia.org/wiki/%EB%B9%84%EB%93%B1%EB%B0%A9%EC%84%B1_%ED%95%84%ED%84%B0%EB%A7%81)이며, 이 또한 샘플러에 의해 자동으로 적용될 수 있습니다. -Aside from these filters, a sampler can also take care of transformations. It -determines what happens when you try to read texels outside the image through -its *addressing mode*. The image below displays some of the possibilities: +이러한 필터 외에도, 샘플러는 변환도 처리할 수 있습니다. 샘플러는 *주소 지정 모드(addressing mode)*를 통해 이미지 외부의 텍셀을 읽으려고 할 때 어떤 일이 일어날지를 결정합니다. 아래 이미지는 몇 가지 가능한 옵션을 보여줍니다. ![](/images/texture_addressing.png) -We will now create a function `createTextureSampler` to set up such a sampler -object. We'll be using that sampler to read colors from the texture in the -shader later on. +이제 이러한 샘플러 객체를 설정하기 위해 `createTextureSampler` 함수를 만들 것입니다. 나중에 셰이더에서 이 샘플러를 사용해 텍스처로부터 색상을 읽게 됩니다. ```c++ void initVulkan() { @@ -173,8 +145,7 @@ void createTextureSampler() { } ``` -Samplers are configured through a `VkSamplerCreateInfo` structure, which -specifies all filters and transformations that it should apply. +샘플러는 `VkSamplerCreateInfo` 구조체를 통해 구성되며, 이 구조체는 샘플러가 적용해야 할 모든 필터와 변환을 명시합니다. ```c++ VkSamplerCreateInfo samplerInfo{}; @@ -183,11 +154,7 @@ samplerInfo.magFilter = VK_FILTER_LINEAR; samplerInfo.minFilter = VK_FILTER_LINEAR; ``` -The `magFilter` and `minFilter` fields specify how to interpolate texels that -are magnified or minified. Magnification concerns the oversampling problem -describes above, and minification concerns undersampling. The choices are -`VK_FILTER_NEAREST` and `VK_FILTER_LINEAR`, corresponding to the modes -demonstrated in the images above. +`magFilter`와 `minFilter` 필드는 텍셀이 확대(magnified)되거나 축소(minified)될 때 어떻게 보간할지를 지정합니다. 확대는 위에서 설명한 오버샘플링 문제와 관련이 있고, 축소는 언더샘플링 문제와 관련이 있습니다. 선택지는 `VK_FILTER_NEAREST`와 `VK_FILTER_LINEAR`이며, 이는 위 이미지에서 보여준 모드에 해당합니다. ```c++ samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; @@ -195,81 +162,54 @@ samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; ``` -The addressing mode can be specified per axis using the `addressMode` fields. -The available values are listed below. Most of these are demonstrated in the -image above. Note that the axes are called U, V and W instead of X, Y and Z. -This is a convention for texture space coordinates. - -* `VK_SAMPLER_ADDRESS_MODE_REPEAT`: Repeat the texture when going beyond the -image dimensions. -* `VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT`: Like repeat, but inverts the -coordinates to mirror the image when going beyond the dimensions. -* `VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE`: Take the color of the edge closest to -the coordinate beyond the image dimensions. -* `VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE`: Like clamp to edge, but -instead uses the edge opposite to the closest edge. -* `VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER`: Return a solid color when sampling -beyond the dimensions of the image. - -It doesn't really matter which addressing mode we use here, because we're not -going to sample outside of the image in this tutorial. However, the repeat mode -is probably the most common mode, because it can be used to tile textures like -floors and walls. +주소 지정 모드는 `addressMode` 필드를 사용하여 축별로 지정할 수 있습니다. 사용 가능한 값은 다음과 같습니다. 대부분은 위 이미지에서 시연되었습니다. 축이 X, Y, Z 대신 U, V, W로 불리는 점에 유의하세요. 이는 텍스처 공간 좌표의 관례입니다. + +* `VK_SAMPLER_ADDRESS_MODE_REPEAT`: 이미지 크기를 벗어날 때 텍스처를 반복합니다. +* `VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT`: 반복과 같지만, 크기를 벗어날 때 좌표를 반전시켜 이미지를 거울처럼 반사합니다. +* `VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE`: 이미지 크기를 벗어나는 좌표에 대해 가장 가까운 가장자리의 색상을 사용합니다. +* `VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE`: 가장자리 클램프와 비슷하지만, 가장 가까운 가장자리가 아닌 반대쪽 가장자리를 사용합니다. +* `VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER`: 이미지 크기 밖을 샘플링할 때 지정된 단색을 반환합니다. + +이 튜토리얼에서는 이미지 외부를 샘플링하지 않을 것이므로 어떤 주소 지정 모드를 사용하든 큰 차이는 없습니다. 하지만 바닥이나 벽처럼 텍스처를 타일링하는 데 사용될 수 있기 때문에 반복 모드가 아마 가장 일반적일 것입니다. ```c++ samplerInfo.anisotropyEnable = VK_TRUE; samplerInfo.maxAnisotropy = ???; ``` -These two fields specify if anisotropic filtering should be used. There is no -reason not to use this unless performance is a concern. The `maxAnisotropy` -field limits the amount of texel samples that can be used to calculate the final -color. A lower value results in better performance, but lower quality results. -To figure out which value we can use, we need to retrieve the properties of the physical device like so: +이 두 필드는 비등방성 필터링을 사용할지 여부를 지정합니다. 성능이 우려되는 경우가 아니라면 사용하지 않을 이유가 없습니다. `maxAnisotropy` 필드는 최종 색상을 계산하는 데 사용될 수 있는 텍셀 샘플의 양을 제한합니다. 값이 낮을수록 성능은 좋아지지만 결과물의 품질은 떨어집니다. 우리가 사용할 수 있는 값을 알아내려면, 다음과 같이 물리 장치의 속성을 가져와야 합니다. ```c++ VkPhysicalDeviceProperties properties{}; vkGetPhysicalDeviceProperties(physicalDevice, &properties); ``` -If you look at the documentation for the `VkPhysicalDeviceProperties` structure, you'll see that it contains a `VkPhysicalDeviceLimits` member named `limits`. This struct in turn has a member called `maxSamplerAnisotropy` and this is the maximum value we can specify for `maxAnisotropy`. If we want to go for maximum quality, we can simply use that value directly: +`VkPhysicalDeviceProperties` 구조체의 문서를 보면 `limits`라는 이름의 `VkPhysicalDeviceLimits` 멤버가 포함되어 있습니다. 이 구조체는 다시 `maxSamplerAnisotropy`라는 멤버를 가지고 있으며, 이것이 `maxAnisotropy`에 지정할 수 있는 최대값입니다. 최고의 품질을 원한다면 이 값을 직접 사용하면 됩니다. ```c++ samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy; ``` -You can either query the properties at the beginning of your program and pass them around to the functions that need them, or query them in the `createTextureSampler` function itself. +프로그램 시작 시에 속성을 조회하여 필요한 함수에 전달하거나, `createTextureSampler` 함수 내에서 직접 조회할 수 있습니다. ```c++ samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; ``` -The `borderColor` field specifies which color is returned when sampling beyond -the image with clamp to border addressing mode. It is possible to return black, -white or transparent in either float or int formats. You cannot specify an -arbitrary color. +`borderColor` 필드는 `clamp to border` 주소 지정 모드로 이미지 외부를 샘플링할 때 반환될 색상을 지정합니다. float 또는 int 형식으로 검은색, 흰색 또는 투명색을 반환할 수 있습니다. 임의의 색상을 지정할 수는 없습니다. ```c++ samplerInfo.unnormalizedCoordinates = VK_FALSE; ``` -The `unnormalizedCoordinates` field specifies which coordinate system you want -to use to address texels in an image. If this field is `VK_TRUE`, then you can -simply use coordinates within the `[0, texWidth)` and `[0, texHeight)` range. If -it is `VK_FALSE`, then the texels are addressed using the `[0, 1)` range on all -axes. Real-world applications almost always use normalized coordinates, because -then it's possible to use textures of varying resolutions with the exact same -coordinates. +`unnormalizedCoordinates` 필드는 이미지의 텍셀 주소를 지정하는 데 사용할 좌표계를 지정합니다. 이 필드가 `VK_TRUE`이면 `[0, texWidth)`와 `[0, texHeight)` 범위 내의 좌표를 그대로 사용할 수 있습니다. `VK_FALSE`이면 텍셀은 모든 축에서 `[0, 1)` 범위의 정규화된 좌표를 사용하여 주소 지정됩니다. 실제 애플리케이션에서는 다양한 해상도의 텍스처를 동일한 좌표로 사용할 수 있기 때문에 거의 항상 정규화된 좌표를 사용합니다. ```c++ samplerInfo.compareEnable = VK_FALSE; samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS; ``` -If a comparison function is enabled, then texels will first be compared to a -value, and the result of that comparison is used in filtering operations. This -is mainly used for [percentage-closer filtering](https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch11.html) -on shadow maps. We'll look at this in a future chapter. +비교 함수가 활성화되면, 텍셀은 먼저 특정 값과 비교되고 그 비교 결과가 필터링 연산에 사용됩니다. 이는 주로 섀도 맵의 [백분율 근접 필터링(percentage-closer filtering)](https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch11.html)에 사용됩니다. 이 내용은 나중 장에서 다룰 것입니다. ```c++ samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; @@ -278,12 +218,9 @@ samplerInfo.minLod = 0.0f; samplerInfo.maxLod = 0.0f; ``` -All of these fields apply to mipmapping. We will look at mipmapping in a [later -chapter](/Generating_Mipmaps), but basically it's another type of filter that can be applied. +이 필드들은 모두 밉매핑(mipmapping)에 적용됩니다. 밉매핑은 [다음 장](/Generating_Mipmaps)에서 다룰 것이지만, 기본적으로 적용할 수 있는 또 다른 유형의 필터입니다. -The functioning of the sampler is now fully defined. Add a class member to -hold the handle of the sampler object and create the sampler with -`vkCreateSampler`: +이제 샘플러의 작동 방식이 완전히 정의되었습니다. 샘플러 객체의 핸들을 저장할 클래스 멤버를 추가하고 `vkCreateSampler`로 샘플러를 생성합니다. ```c++ VkImageView textureImageView; @@ -300,14 +237,9 @@ void createTextureSampler() { } ``` -Note the sampler does not reference a `VkImage` anywhere. The sampler is a -distinct object that provides an interface to extract colors from a texture. It -can be applied to any image you want, whether it is 1D, 2D or 3D. This is -different from many older APIs, which combined texture images and filtering into -a single state. +샘플러는 어디에도 `VkImage`를 참조하지 않는다는 점에 유의하세요. 샘플러는 텍스처에서 색상을 추출하는 인터페이스를 제공하는 별개의 객체입니다. 1D, 2D, 3D 등 원하는 어떤 이미지에도 적용할 수 있습니다. 이는 텍스처 이미지와 필터링을 단일 상태로 결합했던 많은 구형 API와는 다른 점입니다. -Destroy the sampler at the end of the program when we'll no longer be accessing -the image: +더 이상 이미지를 접근하지 않을 프로그램의 끝에서 샘플러를 파괴합니다. ```c++ void cleanup() { @@ -320,23 +252,20 @@ void cleanup() { } ``` -## Anisotropy device feature +## 비등방성 장치 기능 -If you run your program right now, you'll see a validation layer message like -this: +지금 프로그램을 실행하면 다음과 같은 검증 레이어 메시지를 볼 수 있습니다. ![](/images/validation_layer_anisotropy.png) -That's because anisotropic filtering is actually an optional device feature. We -need to update the `createLogicalDevice` function to request it: +이는 비등방성 필터링이 사실 선택적(optional) 장치 기능이기 때문입니다. 이를 요청하도록 `createLogicalDevice` 함수를 업데이트해야 합니다. ```c++ VkPhysicalDeviceFeatures deviceFeatures{}; deviceFeatures.samplerAnisotropy = VK_TRUE; ``` -And even though it is very unlikely that a modern graphics card will not support -it, we should update `isDeviceSuitable` to check if it is available: +그리고 최신 그래픽 카드가 이를 지원하지 않을 가능성은 매우 낮지만, `isDeviceSuitable` 함수를 업데이트하여 사용 가능한지 확인해야 합니다. ```c++ bool isDeviceSuitable(VkPhysicalDevice device) { @@ -349,21 +278,17 @@ bool isDeviceSuitable(VkPhysicalDevice device) { } ``` -The `vkGetPhysicalDeviceFeatures` repurposes the `VkPhysicalDeviceFeatures` -struct to indicate which features are supported rather than requested by setting -the boolean values. +`vkGetPhysicalDeviceFeatures`는 `VkPhysicalDeviceFeatures` 구조체를 재사용하여, 요청된 기능이 아닌 지원되는 기능을 불리언 값으로 설정하여 나타냅니다. -Instead of enforcing the availability of anisotropic filtering, it's also -possible to simply not use it by conditionally setting: +비등방성 필터링의 사용 가능성을 강제하는 대신, 조건부로 사용하지 않도록 설정할 수도 있습니다. ```c++ samplerInfo.anisotropyEnable = VK_FALSE; samplerInfo.maxAnisotropy = 1.0f; ``` -In the next chapter we will expose the image and sampler objects to the shaders -to draw the texture onto the square. +다음 장에서는 이미지와 샘플러 객체를 셰이더에 노출하여 사각형에 텍스처를 그릴 것입니다. -[C++ code](/code/25_sampler.cpp) / -[Vertex shader](/code/22_shader_ubo.vert) / -[Fragment shader](/code/22_shader_ubo.frag) +[C++ 코드](/code/25_sampler.cpp) / +[정점 셰이더](/code/22_shader_ubo.vert) / +[프래그먼트 셰이더](/code/22_shader_ubo.frag) \ No newline at end of file diff --git a/ko/06_Texture_mapping/02_Combined_image_sampler.md b/ko/06_Texture_mapping/02_Combined_image_sampler.md index 0f1e5496..2d84eb7e 100644 --- a/ko/06_Texture_mapping/02_Combined_image_sampler.md +++ b/ko/06_Texture_mapping/02_Combined_image_sampler.md @@ -1,21 +1,12 @@ -## Introduction +## 소개 -We looked at descriptors for the first time in the uniform buffers part of the -tutorial. In this chapter we will look at a new type of descriptor: *combined -image sampler*. This descriptor makes it possible for shaders to access an image -resource through a sampler object like the one we created in the previous -chapter. +우리는 유니폼 버퍼(uniform buffers) 파트에서 처음으로 디스크립터(descriptor)를 살펴보았습니다. 이번 챕터에서는 새로운 유형의 디스크립터인 **결합 이미지 샘플러(combined image sampler)**에 대해 알아보겠습니다. 이 디스크립터를 사용하면 셰이더가 이전 챕터에서 생성한 것과 같은 샘플러 객체를 통해 이미지 리소스에 접근할 수 있습니다. -We'll start by modifying the descriptor set layout, descriptor pool and descriptor -set to include such a combined image sampler descriptor. After that, we're going -to add texture coordinates to `Vertex` and modify the fragment shader to read -colors from the texture instead of just interpolating the vertex colors. +우선 디스크립터 셋 레이아웃, 디스크립터 풀, 디스크립터 셋을 수정하여 결합 이미지 샘플러 디스크립터를 포함하는 것부터 시작하겠습니다. 그 후, `Vertex`에 텍스처 좌표를 추가하고 프래그먼트 셰이더를 수정하여 단순히 정점 색상을 보간하는 대신 텍스처에서 색상을 읽도록 할 것입니다. -## Updating the descriptors +## 디스크립터 업데이트하기 -Browse to the `createDescriptorSetLayout` function and add a -`VkDescriptorSetLayoutBinding` for a combined image sampler descriptor. We'll -simply put it in the binding after the uniform buffer: +`createDescriptorSetLayout` 함수로 가서 결합 이미지 샘플러 디스크립터를 위한 `VkDescriptorSetLayoutBinding`을 추가합니다. 단순히 유니폼 버퍼 다음 바인딩에 추가하겠습니다. ```c++ VkDescriptorSetLayoutBinding samplerLayoutBinding{}; @@ -32,17 +23,9 @@ layoutInfo.bindingCount = static_cast(bindings.size()); layoutInfo.pBindings = bindings.data(); ``` -Make sure to set the `stageFlags` to indicate that we intend to use the combined -image sampler descriptor in the fragment shader. That's where the color of the -fragment is going to be determined. It is possible to use texture sampling in -the vertex shader, for example to dynamically deform a grid of vertices by a -[heightmap](https://en.wikipedia.org/wiki/Heightmap). +`stageFlags`를 설정하여 프래그먼트 셰이더에서 결합 이미지 샘플러 디스크립터를 사용하려는 의도를 나타내야 합니다. 프래그먼트의 색상이 결정되는 곳이 바로 여기입니다. 버텍스 셰이더에서 텍스처 샘플링을 사용하는 것도 가능합니다. 예를 들어, [하이트맵(heightmap)](https://en.wikipedia.org/wiki/Heightmap)을 사용하여 정점 그리드를 동적으로 변형시킬 수 있습니다. -We must also create a larger descriptor pool to make room for the allocation -of the combined image sampler by adding another `VkPoolSize` of type -`VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER` to the -`VkDescriptorPoolCreateInfo`. Go to the `createDescriptorPool` function and -modify it to include a `VkDescriptorPoolSize` for this descriptor: +또한 `VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER` 타입의 `VkPoolSize`를 `VkDescriptorPoolCreateInfo`에 추가하여 결합 이미지 샘플러 할당을 위한 공간을 만들기 위해 더 큰 디스크립터 풀을 생성해야 합니다. `createDescriptorPool` 함수로 가서 이 디스크립터를 위한 `VkDescriptorPoolSize`를 포함하도록 수정합니다. ```c++ std::array poolSizes{}; @@ -58,26 +41,11 @@ poolInfo.pPoolSizes = poolSizes.data(); poolInfo.maxSets = static_cast(MAX_FRAMES_IN_FLIGHT); ``` -Inadequate descriptor pools are a good example of a problem that the validation -layers will not catch: As of Vulkan 1.1, `vkAllocateDescriptorSets` may fail -with the error code `VK_ERROR_POOL_OUT_OF_MEMORY` if the pool is not -sufficiently large, but the driver may also try to solve the problem internally. -This means that sometimes (depending on hardware, pool size and allocation size) -the driver will let us get away with an allocation that exceeds the limits of -our descriptor pool. Other times, `vkAllocateDescriptorSets` will fail and -return `VK_ERROR_POOL_OUT_OF_MEMORY`. This can be particularly frustrating if -the allocation succeeds on some machines, but fails on others. - -Since Vulkan shifts the responsiblity for the allocation to the driver, it is no -longer a strict requirement to only allocate as many descriptors of a certain -type (`VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER`, etc.) as specified by the -corresponding `descriptorCount` members for the creation of the descriptor pool. -However, it remains best practise to do so, and in the future, -`VK_LAYER_KHRONOS_validation` will warn about this type of problem if you enable -[Best Practice Validation](https://vulkan.lunarg.com/doc/view/1.4.304.0/linux/best_practices.html). - -The final step is to bind the actual image and sampler resources to the -descriptors in the descriptor set. Go to the `createDescriptorSets` function. +부적절한 디스크립터 풀은 검증 레이어가 잡아내지 못하는 문제의 좋은 예입니다. Vulkan 1.1부터, 풀이 충분히 크지 않으면 `vkAllocateDescriptorSets`가 `VK_ERROR_POOL_OUT_OF_MEMORY` 오류 코드로 실패할 수 있지만, 드라이버가 내부적으로 이 문제를 해결하려고 시도할 수도 있습니다. 이는 때때로 (하드웨어, 풀 크기, 할당 크기에 따라) 드라이버가 디스크립터 풀의 한도를 초과하는 할당을 허용할 수 있음을 의미합니다. 다른 경우에는 `vkAllocateDescriptorSets`가 실패하고 `VK_ERROR_POOL_OUT_OF_MEMORY`를 반환합니다. 이는 일부 머신에서는 할당이 성공하고 다른 머신에서는 실패할 경우 특히 좌절스러울 수 있습니다. + +Vulkan은 할당에 대한 책임을 드라이버에게 넘기므로, 디스크립터 풀 생성 시 해당 `descriptorCount` 멤버로 지정된 만큼만 특정 유형(`VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER` 등)의 디스크립터를 할당하는 것이 더 이상 엄격한 요구 사항은 아닙니다. 하지만, 여전히 그렇게 하는 것이 모범 사례로 남아 있으며, 앞으로 `VK_LAYER_KHRONOS_validation`은 [최적 실행 검증(Best Practice Validation)](https://vulkan.lunarg.com/doc/view/1.4.304.0/linux/best_practices.html)을 활성화하면 이런 유형의 문제에 대해 경고할 것입니다. + +마지막 단계는 실제 이미지와 샘플러 리소스를 디스크립터 셋의 디스크립터에 바인딩하는 것입니다. `createDescriptorSets` 함수로 가세요. ```c++ for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { @@ -95,10 +63,7 @@ for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { } ``` -The resources for a combined image sampler structure must be specified in a -`VkDescriptorImageInfo` struct, just like the buffer resource for a uniform -buffer descriptor is specified in a `VkDescriptorBufferInfo` struct. This is -where the objects from the previous chapter come together. +결합 이미지 샘플러 구조를 위한 리소스는 유니폼 버퍼 디스크립터의 버퍼 리소스가 `VkDescriptorBufferInfo` 구조체에 지정되는 것과 마찬가지로, `VkDescriptorImageInfo` 구조체에 지정되어야 합니다. 여기서 이전 챕터의 객체들이 함께 사용됩니다. ```c++ std::array descriptorWrites{}; @@ -122,15 +87,11 @@ descriptorWrites[1].pImageInfo = &imageInfo; vkUpdateDescriptorSets(device, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); ``` -The descriptors must be updated with this image info, just like the buffer. This -time we're using the `pImageInfo` array instead of `pBufferInfo`. The descriptors -are now ready to be used by the shaders! +버퍼와 마찬가지로 이 이미지 정보로 디스크립터를 업데이트해야 합니다. 이번에는 `pBufferInfo` 대신 `pImageInfo` 배열을 사용합니다. 이제 디스크립터는 셰이더에서 사용할 준비가 되었습니다! -## Texture coordinates +## 텍스처 좌표 -There is one important ingredient for texture mapping that is still missing, and -that's the actual texture coordinates for each vertex. The texture coordinates determine how the -image is actually mapped to the geometry. +텍스처 매핑에 있어 아직 빠진 중요한 요소가 하나 있는데, 바로 각 정점에 대한 실제 텍스처 좌표입니다. 텍스처 좌표는 이미지가 지오메트리에 실제로 어떻게 매핑될지 결정합니다. ```c++ struct Vertex { @@ -170,11 +131,7 @@ struct Vertex { }; ``` -Modify the `Vertex` struct to include a `vec2` for texture coordinates. Make -sure to also add a `VkVertexInputAttributeDescription` so that we can use access -texture coordinates as input in the vertex shader. That is necessary to be able -to pass them to the fragment shader for interpolation across the surface of the -square. +`Vertex` 구조체를 수정하여 텍스처 좌표를 위한 `vec2`를 포함시킵니다. 또한 버텍스 셰이더에서 텍스처 좌표를 입력으로 접근할 수 있도록 `VkVertexInputAttributeDescription`을 추가해야 합니다. 이는 사각형 표면 전체에 걸쳐 보간을 위해 프래그먼트 셰이더로 전달하는 데 필요합니다. ```c++ const std::vector vertices = { @@ -185,16 +142,11 @@ const std::vector vertices = { }; ``` -In this tutorial, I will simply fill the square with the texture by using -coordinates from `0, 0` in the top-left corner to `1, 1` in the bottom-right -corner. Feel free to experiment with different coordinates. Try using -coordinates below `0` or above `1` to see the addressing modes in action! +이 튜토리얼에서는 왼쪽 위 모서리의 `0, 0`에서 오른쪽 아래 모서리의 `1, 1`까지의 좌표를 사용하여 텍스처로 사각형을 채울 것입니다. 자유롭게 다른 좌표로 실험해보세요. `0` 미만 또는 `1` 초과의 좌표를 사용하여 주소 지정 모드가 실제로 어떻게 작동하는지 확인해보세요! -## Shaders +## 셰이더 -The final step is modifying the shaders to sample colors from the texture. We -first need to modify the vertex shader to pass through the texture coordinates -to the fragment shader: +마지막 단계는 셰이더를 수정하여 텍스처에서 색상을 샘플링하는 것입니다. 먼저 버텍스 셰이더를 수정하여 텍스처 좌표를 프래그먼트 셰이더로 전달해야 합니다. ```glsl layout(location = 0) in vec2 inPosition; @@ -211,9 +163,7 @@ void main() { } ``` -Just like the per vertex colors, the `fragTexCoord` values will be smoothly -interpolated across the area of the square by the rasterizer. We can visualize -this by having the fragment shader output the texture coordinates as colors: +정점별 색상과 마찬가지로 `fragTexCoord` 값은 래스터라이저에 의해 사각형 영역 전체에 걸쳐 부드럽게 보간됩니다. 프래그먼트 셰이더가 텍스처 좌표를 색상으로 출력하게 하여 이를 시각화할 수 있습니다. ```glsl #version 450 @@ -228,26 +178,19 @@ void main() { } ``` -You should see something like the image below. Don't forget to recompile the -shaders! +셰이더를 다시 컴파일하는 것을 잊지 마세요! 아래와 같은 이미지를 보게 될 것입니다. ![](/images/texcoord_visualization.png) -The green channel represents the horizontal coordinates and the red channel the -vertical coordinates. The black and yellow corners confirm that the texture -coordinates are correctly interpolated from `0, 0` to `1, 1` across the square. -Visualizing data using colors is the shader programming equivalent of `printf` -debugging, for lack of a better option! +녹색 채널은 수평 좌표를, 적색 채널은 수직 좌표를 나타냅니다. 검은색과 노란색 모서리는 텍스처 좌표가 사각형에 걸쳐 `0, 0`에서 `1, 1`까지 올바르게 보간되었음을 확인시켜 줍니다. 색상을 사용한 데이터 시각화는 셰이더 프로그래밍에서 더 나은 대안이 없을 때 사용하는 `printf` 디버깅과 같습니다! -A combined image sampler descriptor is represented in GLSL by a sampler uniform. -Add a reference to it in the fragment shader: +결합 이미지 샘플러 디스크립터는 GLSL에서 샘플러 유니폼으로 표현됩니다. 프래그먼트 셰이더에 이에 대한 참조를 추가하세요. ```glsl layout(binding = 1) uniform sampler2D texSampler; ``` -There are equivalent `sampler1D` and `sampler3D` types for other types of -images. Make sure to use the correct binding here. +다른 유형의 이미지를 위한 `sampler1D` 및 `sampler3D`와 같은 타입도 있습니다. 여기서 올바른 바인딩을 사용해야 합니다. ```glsl void main() { @@ -255,16 +198,11 @@ void main() { } ``` -Textures are sampled using the built-in `texture` function. It takes a `sampler` -and coordinate as arguments. The sampler automatically takes care of the -filtering and transformations in the background. You should now see the texture -on the square when you run the application: +텍스처는 내장 함수 `texture`를 사용하여 샘플링됩니다. 이 함수는 `sampler`와 좌표를 인수로 받습니다. 샘플러는 백그라운드에서 필터링과 변환을 자동으로 처리합니다. 이제 애플리케이션을 실행하면 사각형 위에 텍스처가 표시될 것입니다. ![](/images/texture_on_square.png) -Try experimenting with the addressing modes by scaling the texture coordinates -to values higher than `1`. For example, the following fragment shader produces -the result in the image below when using `VK_SAMPLER_ADDRESS_MODE_REPEAT`: +텍스처 좌표를 `1`보다 큰 값으로 조정하여 주소 지정 모드를 실험해보세요. 예를 들어, 다음 프래그먼트 셰이더는 `VK_SAMPLER_ADDRESS_MODE_REPEAT`를 사용할 때 아래 이미지와 같은 결과를 생성합니다. ```glsl void main() { @@ -274,7 +212,7 @@ void main() { ![](/images/texture_on_square_repeated.png) -You can also manipulate the texture colors using the vertex colors: +정점 색상을 사용하여 텍스처 색상을 조작할 수도 있습니다. ```glsl void main() { @@ -282,15 +220,12 @@ void main() { } ``` -I've separated the RGB and alpha channels here to not scale the alpha channel. +알파 채널이 변하지 않도록 RGB와 알파 채널을 분리했습니다. ![](/images/texture_on_square_colorized.png) -You now know how to access images in shaders! This is a very powerful technique -when combined with images that are also written to in framebuffers. You can use -these images as inputs to implement cool effects like post-processing and camera -displays within the 3D world. +이제 셰이더에서 이미지에 접근하는 방법을 알게 되었습니다! 이 기술은 프레임버퍼에 쓰여지기도 하는 이미지와 결합될 때 매우 강력합니다. 이러한 이미지를 입력으로 사용하여 후처리(post-processing)나 3D 세계 내 카메라 디스플레이와 같은 멋진 효과를 구현할 수 있습니다. -[C++ code](/code/26_texture_mapping.cpp) / -[Vertex shader](/code/26_shader_textures.vert) / -[Fragment shader](/code/26_shader_textures.frag) +[C++ 코드](/code/26_texture_mapping.cpp) / +[버텍스 셰이더](/code/26_shader_textures.vert) / +[프래그먼트 셰이더](/code/26_shader_textures.frag) \ No newline at end of file From 436a155f1f4c4c397d6bd7f93602076b28b202eb Mon Sep 17 00:00:00 2001 From: erenengine Date: Sat, 21 Jun 2025 12:38:28 +0900 Subject: [PATCH 4/4] Add Korean language support to the configuration --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index edb3a0b3..b800561e 100644 --- a/config.json +++ b/config.json @@ -34,7 +34,7 @@ "files": ["README.md", "build_ebook.py","daux.patch",".gitignore"], "folders": ["ebook"] }, - "languages": {"en": "English", "fr": "Français"}, + "languages": {"en": "English", "fr": "Français", "ko": "한국어", "ko-rust": "한국어 - Rust"}, "language": "en", "processor": "VulkanLinkProcessor" }