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" } 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..7d904248 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/00_Base_code.md @@ -0,0 +1,171 @@ +## 일반적인 구조 + +이전 장에서 여러분은 모든 설정을 마친 Vulkan 프로젝트를 만들고 예제 코드로 테스트했습니다. 이번 장에서는 다음 코드를 가지고 처음부터 시작합니다. + +```rust +use ash::vk; + +use std::error::Error; + +struct HelloTriangleApplication { + +} + +impl HelloTriangleApplication { + pub fn new() -> Self { + HelloTriangleApplication {} + } + + pub fn run(&mut self) -> Result<(), Box> { + self.init_vulkan()?; + self.main_loop(); + self.cleanup(); + Ok(()) + } + + fn init_vulkan(&mut self) -> Result<(), Box> { + Ok(()) + } + + fn main_loop(&mut self) { + + } + + fn cleanup(&mut self) { + + } +} + +fn main() { + let mut app = HelloTriangleApplication::new(); + + if let Err(e) = app.run() { + eprintln!("오류 발생: {}", e); + std::process::exit(1); + } +} +``` + +먼저 `ash` 크레이트에서 Vulkan 타입, 함수, 열거형을 가져옵니다. `std::error::Error` 트레이트는 Rust의 관용적인 오류 처리 방식에 사용됩니다. + +프로그램 자체는 `struct`로 감싸져 있습니다. Vulkan 객체들을 구조체 필드로 저장하고, 각 객체를 초기화하는 메서드들을 추가하여 `run` 메서드에서 순서대로 호출할 것입니다. 모든 준비가 끝나면 메인 루프에 진입하여 프레임 렌더링을 시작합니다. 잠시 후에 창이 닫힐 때까지 이벤트를 처리하는 `main_loop`를 채워 넣을 것입니다. 루프가 종료되면, `cleanup` 메서드에서 사용했던 리소스들을 반드시 해제할 것입니다. + +실행 중 치명적인 오류가 발생하면, `Result` 열거형을 반환합니다. 이 예제에서는 `Box`를 사용하여 다양한 타입의 오류를 처리합니다. 오류가 발생하면 `main` 함수로 전파되어 터미널에 출력됩니다. 곧 다룰 오류의 한 예는 특정 필수 익스텐션이 지원되지 않는다는 것을 발견하는 경우입니다. + +이 장 이후의 거의 모든 장에서는 `init_vulkan`에서 호출될 새로운 초기화 로직과, `cleanup`에서 마지막에 해제해야 할 하나 이상의 새로운 Vulkan 객체를 구조체 필드에 추가할 것입니다. + +## 리소스 관리 + +C/C++에서 `malloc`으로 할당된 모든 메모리에 `free` 호출이 필요한 것처럼, 우리가 생성하는 모든 Vulkan 객체는 더 이상 필요하지 않을 때 명시적으로 파괴되어야 합니다. 하지만 Rust는 RAII(Resource Acquisition Is Initialization) 패턴을 언어 차원에서 강력하게 지원합니다. 객체의 수명은 소유권(ownership) 시스템에 의해 관리되며, 객체가 스코프를 벗어날 때 `Drop` 트레이트가 자동으로 호출됩니다. + +Vulkan 핸들을 감싸는 구조체를 만들고 그 구조체에 `Drop`을 구현하면 리소스 관리를 자동화할 수 있습니다. 이는 더 큰 Rust 프로그램에서 권장되는 모델입니다. + +하지만 이 튜토리얼에서는 C++ 원본의 교육적 목표를 따라, Vulkan 객체의 할당과 해제를 명시적으로 다루겠습니다. `cleanup` 메서드에서 `ash`가 제공하는 `destroy_...` 함수들을 직접 호출할 것입니다. 이를 통해 Vulkan API가 내부적으로 어떻게 동작하는지 명확하게 배울 수 있습니다. + +Vulkan 객체는 `create_...` 함수로 직접 생성되거나, `allocate_...` 함수를 통해 다른 객체에서 할당됩니다. 사용이 끝난 객체는 그에 상응하는 `destroy_...` 및 `free_...` 함수로 파괴해야 합니다. 이 함수들의 매개변수는 일반적으로 객체 유형에 따라 다르지만, 마지막 매개변수로 `pAllocator`에 해당하는 인자를 받는 경우가 많습니다. 이는 사용자 정의 메모리 할당자를 위한 콜백을 지정하는 선택적 매개변수입니다. 이 튜토리얼에서는 이 매개변수를 무시하고 항상 `None`을 전달할 것입니다. + +## Winit 통합 + +Vulkan은 오프스크린 렌더링을 위해 창 없이도 완벽하게 작동하지만, 화면에 무언가를 보여주는 것이 훨씬 더 흥미롭습니다! Rust 생태계에서는 창 생성을 위해 `winit` 라이브러리를 주로 사용합니다. + +먼저 `Cargo.toml` 파일에 `ash`와 `winit` 의존성을 추가해야 합니다. + +```toml +[dependencies] +ash = "0.37" +winit = "0.28" +``` + +C++ 버전과 달리, Rust에서는 `run` 메서드가 창과 이벤트 루프의 생명주기를 직접 관리하는 것이 더 관용적입니다. `HelloTriangleApplication` 구조체는 Vulkan 관련 객체들을 소유하고, 창(window)은 Vulkan 초기화에 필요한 정보를 제공하는 역할을 합니다. + +다음과 같이 `run` 메서드를 수정하고 `main_loop`의 시그니처를 변경합니다. `init_window` 함수는 필요하지 않으며, `run` 메서드 내에서 직접 창을 생성합니다. + +```rust +// use 문들을 파일 상단에 추가하세요. +use winit::event::{Event, WindowEvent}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::window::{Window, WindowBuilder}; + +// ... HelloTriangleApplication 구조체 정의 ... + +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)?; + + // 2. Vulkan 초기화 + self.init_vulkan(&window)?; + + // 3. 메인 루프 실행 + self.main_loop(event_loop, window); + + // 4. 리소스 정리 + self.cleanup(); + + Ok(()) + } + + fn init_vulkan(&mut self, window: &Window) -> Result<(), Box> { + // 이 메서드는 나중에 채웁니다. + // window 파라미터는 표면(surface) 생성에 필요합니다. + Ok(()) + } + + fn main_loop(&mut self, event_loop: EventLoop<()>, window: Window) { + // ... + } + // ... +} +``` + +`glfwInit()`을 호출하는 대신 `EventLoop::new()`를 호출하여 이벤트 루프를 만듭니다. `glfwWindowHint`를 설정하는 대신, `WindowBuilder`를 사용하여 창의 속성을 설정합니다. `with_resizable(false)`는 창 크기 조절을 비활성화합니다. + +창의 크기를 위해 상수를 사용하는 것이 좋습니다. 구조체 정의 위에 상수를 추가합시다. + +```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` 분기에서 매 프레임을 그리는 함수를 호출하게 될 것입니다. + +Winit에서는 창과 관련된 리소스가 `window` 객체의 스코프가 끝날 때 자동으로 정리됩니다. `event_loop.run`이 종료되면 `run` 메서드도 종료되고 `window`가 소멸(drop)되므로, `glfwDestroyWindow`나 `glfwTerminate`에 해당하는 명시적인 호출이 필요 없습니다. `cleanup` 메서드는 순수하게 Vulkan 리소스를 정리하는 데 사용될 것입니다. 지금은 비워둡니다. + +```rust +fn cleanup(&mut self) { + // 이 메서드는 나중에 Vulkan 객체들을 파괴하는 데 사용됩니다. +} +``` + +이제 프로그램을 실행하면 "Vulkan"이라는 제목의 창이 나타나고, 창을 닫아 애플리케이션이 종료될 때까지 유지됩니다. 이제 Vulkan 애플리케이션의 골격을 갖추었으니, [첫 번째 Vulkan 객체 생성하기](!ko/Drawing_a_triangle/Setup/Instance)로 넘어갑시다! + +[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 new file mode 100644 index 00000000..e8de0a03 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/01_Instance.md @@ -0,0 +1,169 @@ +## 인스턴스 생성 (Rust/Ash) + +가장 먼저 해야 할 일은 *인스턴스(instance)*를 생성하여 Vulkan 라이브러리를 초기화하는 것입니다. 인스턴스는 애플리케이션과 Vulkan 라이브러리 간의 연결고리이며, 이 과정에서 애플리케이션에 대한 몇 가지 세부 정보를 드라이버에 지정해야 합니다. + +먼저 애플리케이션 구조체에 `create_instance` 함수를 추가하고, 나중에 만들 `init_vulkan` 함수에서 호출하도록 구성합니다. + +```rust +fn init_vulkan(&mut self) -> Result<(), Box> { + self.create_instance()?; + Ok(()) +} +``` + +또한 인스턴스 핸들을 저장할 필드를 구조체에 추가합니다. `ash::Instance` 타입은 Vulkan 인스턴스를 나타냅니다. + +```rust +struct HelloTriangleApplication { + // ... 다른 필드들 + instance: ash::Instance, +} +``` + +이제 인스턴스를 생성하기 위해, 먼저 애플리케이션에 대한 정보가 담긴 구조체를 채워야 합니다. 이 데이터는 기술적으로 선택 사항이지만, 드라이버가 우리의 특정 애플리케이션을 최적화하는 데 유용한 정보를 제공할 수 있습니다(예: 특정 특수 동작을 하는 잘 알려진 그래픽 엔진을 사용하는 경우). 이 구조체는 `vk::ApplicationInfo`입니다. + +Ash에서는 빌더(builder) 패턴을 사용하여 구조체를 안전하고 편리하게 생성합니다. + +```rust +// create_instance 함수 내부 +use std::ffi::CStr; +use ash::vk; + +// ... + +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(); + +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); +``` + +Vulkan의 많은 구조체와 마찬가지로, `sType` 멤버는 빌더가 자동으로 설정해 줍니다. Ash의 빌더는 `pNext` 멤버를 다루는 확장 기능도 지원하지만, 여기서는 기본값인 null 포인터로 둡니다. + +다음으로, 인스턴스 생성을 위한 더 중요한 정보를 담은 구조체를 채워야 합니다. 이 구조체는 필수이며, 우리가 사용할 전역 확장(global extensions)과 유효성 검사 레이어(validation layers)를 Vulkan 드라이버에 알려줍니다. + +```rust +let create_info = vk::InstanceCreateInfo::builder() + .application_info(&app_info); +``` + +이제 원하는 전역 확장을 지정해야 합니다. Vulkan은 플랫폼에 구애받지 않는 API이므로, 창 시스템과 상호작용하려면 확장이 필요합니다. C++의 GLFW와 마찬가지로, Rust 생태계에서는 `winit` 창 라이브러리와 `ash-window` 크레이트를 함께 사용하여 필요한 확장 목록을 쉽게 얻을 수 있습니다. + +```rust +// winit::window::Window 객체가 있다고 가정합니다. +// let window: winit::window::Window = ...; + +let required_extensions = ash_window::enumerate_required_extensions(window.display_handle().unwrap().as_raw()) + .unwrap() + .to_vec(); +``` + +이제 이 확장 목록을 `InstanceCreateInfo` 빌더에 추가합니다. + +```rust +let mut create_info = vk::InstanceCreateInfo::builder() + .application_info(&app_info) + .enabled_extension_names(&required_extensions); +``` + +구조체의 마지막 부분은 활성화할 전역 유효성 검사 레이어를 결정합니다. 다음 장에서 자세히 다룰 것이므로 지금은 비워둡니다. + +```rust +// create_info 빌더 체인에 추가 +// .enabled_layer_count(0) +// .pp_enabled_layer_names(std::ptr::null()) +``` + +이제 Vulkan이 인스턴스를 생성하는 데 필요한 모든 것을 지정했으므로, 마침내 `create_instance`를 호출할 수 있습니다. Ash에서는 Vulkan 라이브러리 로딩을 담당하는 `ash::Entry` 객체를 통해 이 함수를 호출합니다. + +```rust +// entry: &ash::Entry 는 함수 인자로 전달받았다고 가정 +let instance = unsafe { + entry + .create_instance(&create_info, None) + .expect("Failed to create instance!") +}; +self.instance = instance; +``` + +Rust/Ash에서 객체 생성 함수의 패턴은 다음과 같습니다. + +1. 생성 정보가 담긴 구조체의 빌더를 사용합니다. +2. `ash::Entry` (전역 함수용) 또는 다른 Vulkan 객체(자식 객체용)의 메서드를 호출합니다. +3. 첫 번째 인자로 생성 정보 구조체에 대한 참조를 전달합니다. +4. 두 번째 인자는 사용자 정의 할당자 콜백으로, 이 튜토리얼에서는 `None`을 사용합니다. +5. 이 함수는 `Result`를 반환하므로, Rust의 오류 처리 메커니즘(`?` 연산자, `match`, `expect` 등)을 사용하여 결과를 처리합니다. + +`create_instance` 호출은 `unsafe` 블록 안에 있습니다. 이는 Ash가 우리가 제공한 포인터(예: 확장 이름)가 유효하고 올바른 생명주기를 가졌는지 보장할 수 없기 때문입니다. 우리는 이 조건들을 충족함을 보장해야 합니다. + +### VK_ERROR_INCOMPATIBLE_DRIVER 오류 발생 시 (macOS) +최신 MoltenVK SDK를 사용하는 macOS에서는 `VK_KHR_portability_subset` 확장이 필수로 요구될 수 있습니다. 이로 인해 `create_instance`가 실패할 수 있습니다. + +이 문제를 해결하려면, `VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR` 플래그를 추가하고, `VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME` 확장을 활성화해야 합니다. + +Rust에서는 `cfg` 속성을 사용하여 플랫폼별 코드를 작성할 수 있습니다. + +```rust +let mut required_extensions = ash_window::enumerate_required_extensions(window.display_handle().unwrap().as_raw()) + .unwrap() + .to_vec(); + +let mut create_info = vk::InstanceCreateInfo::builder() + .application_info(&app_info); + +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); +} + +create_info = create_info.enabled_extension_names(&required_extensions); + +// ... create_instance 호출 +``` + +### 확장 지원 여부 확인 + +선택적 기능에 대한 지원 여부를 확인하고 싶다면, 인스턴스를 생성하기 전에 `enumerate_instance_extension_properties`를 사용하여 지원되는 확장 목록을 가져올 수 있습니다. Ash는 이 과정을 매우 간단하게 만들어 줍니다. + +```rust +// entry: &ash::Entry + +let available_extensions = entry + .enumerate_instance_extension_properties(None) + .expect("Failed to enumerate instance extensions"); + +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()); +} +``` + +Ash는 C++처럼 두 번 호출할 필요 없이 지원되는 모든 확장을 `Vec`로 편리하게 반환해 줍니다. + +도전 과제로, `ash-window`가 요구하는 모든 확장이 `available_extensions` 목록에 포함되어 있는지 확인하는 코드를 작성해 보세요. Rust의 `HashSet`과 이터레이터를 사용하면 효율적으로 구현할 수 있습니다. + +### 정리 (Cleaning up) + +`ash::Instance`는 프로그램이 종료되기 직전에만 파괴되어야 합니다. Rust에서는 RAII(Resource Acquisition Is Initialization) 패턴을 따르는 것이 가장 일반적입니다. 애플리케이션 구조체에 대해 `Drop` 트레이트를 구현하여 리소스가 범위를 벗어날 때 자동으로 정리되도록 합니다. + +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + // 다른 모든 Vulkan 리소스가 파괴된 후에 인스턴스를 파괴해야 합니다. + self.instance.destroy_instance(None); + } + } +} +``` + +`destroy_instance` 호출은 `unsafe`입니다. 왜냐하면 이 인스턴스로부터 생성된 다른 모든 Vulkan 리소스(디바이스, 버퍼 등)가 이미 파괴되었음을 프로그래머가 보장해야 하기 때문입니다. `drop` 메서드 내에서 필드의 소멸 순서를 올바르게 지정하면 이 요구사항을 충족할 수 있습니다. + +인스턴스 생성 후의 더 복잡한 단계로 넘어가기 전에, [유효성 검사 레이어](!ko/Drawing_a_triangle/Setup/Validation_layers)를 살펴봄으로써 디버깅 옵션을 평가해 볼 시간입니다. + +[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 new file mode 100644 index 00000000..b031f835 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md @@ -0,0 +1,247 @@ +## 밸리데이션 레이어란 무엇인가? + +Vulkan API는 최소한의 드라이버 오버헤드를 목표로 설계되었으며, 이 목표가 드러나는 부분 중 하나는 API에 기본적으로 내장된 오류 검사가 매우 제한적이라는 점입니다. 열거형(enum) 값을 잘못 설정하거나 필수 파라미터에 null 포인터를 전달하는 것과 같은 간단한 실수조차도 일반적으로 명시적으로 처리되지 않으며, 크래시나 정의되지 않은 동작(undefined behavior)으로 이어질 뿐입니다. Vulkan은 개발자가 수행하는 모든 작업을 매우 명시적으로 지정해야 하므로, 논리 장치(logical device)를 생성할 때 새로운 GPU 기능을 사용하면서 해당 기능 사용을 요청하는 것을 잊는 등 많은 사소한 실수를 하기 쉽습니다. + +하지만 이러한 검사를 API에 추가할 수 없다는 의미는 아닙니다. Vulkan은 이를 위해 *밸리데이션 레이어*라는 멋진 시스템을 도입했습니다. 밸리데이션 레이어는 Vulkan 함수 호출에 끼어들어(hook into) 추가적인 작업을 적용하는 선택적 컴포넌트입니다. 밸리데이션 레이어의 일반적인 작업은 다음과 같습니다. + +* 사양에 명시된 값과 파라미터 값을 비교하여 오용을 감지 +* 객체의 생성 및 소멸을 추적하여 리소스 누수(resource leak)를 발견 +* 호출이 발생한 스레드를 추적하여 스레드 안전성(thread safety)을 검사 +* 모든 호출과 그 파라미터를 표준 출력으로 로깅 +* 프로파일링 및 재현(replaying)을 위해 Vulkan 호출을 추적 + +이러한 밸리데이션 레이어들은 원하는 모든 디버깅 기능을 포함하도록 자유롭게 쌓아서(stacked) 사용할 수 있습니다. 디버그 빌드에서는 밸리데이션 레이어를 활성화하고 릴리즈 빌드에서는 완전히 비활성화하면, 두 가지 장점을 모두 누릴 수 있습니다! + +Vulkan은 내장된 밸리데이션 레이어를 제공하지 않지만, LunarG Vulkan SDK는 일반적인 오류를 검사하는 훌륭한 레이어 세트를 제공합니다. 이 레이어들은 완전히 [오픈 소스](https://github.com/KhronosGroup/Vulkan-ValidationLayers)이므로, 어떤 종류의 실수를 검사하는지 확인하고 기여할 수도 있습니다. 밸리데이션 레이어를 사용하는 것은 실수로 정의되지 않은 동작에 의존하여 애플리케이션이 다른 드라이버에서 깨지는 것을 방지하는 가장 좋은 방법입니다. + +밸리데이션 레이어는 시스템에 설치된 경우에만 사용할 수 있습니다. 예를 들어, LunarG 밸리데이션 레이어는 Vulkan SDK가 설치된 PC에서만 사용할 수 있습니다. + +## 밸리데이션 레이어 사용하기 (Rust & Ash) + +이 섹션에서는 Vulkan SDK가 제공하는 표준 진단 레이어를 활성화하는 방법을 살펴보겠습니다. 확장(extension)과 마찬가지로, 밸리데이션 레이어도 이름을 지정하여 활성화해야 합니다. 모든 유용한 표준 밸리데이션은 SDK에 포함된 `VK_LAYER_KHRONOS_validation`이라는 레이어에 번들로 제공됩니다. + +먼저 활성화할 레이어와 그 활성화 여부를 결정하는 상수를 정의합시다. Rust에서는 C++의 `#ifdef NDEBUG`와 유사한 역할을 하는 조건부 컴파일 속성 `#[cfg(debug_assertions)]`를 사용합니다. `debug_assertions`는 디버그 빌드에서 활성화됩니다. + +Vulkan C API는 C 스타일 문자열(null로 끝나는 바이트 배열)을 요구하므로, `std::ffi::CString`을 사용해 Rust 문자열을 변환해야 합니다. + +```rust +use std::ffi::{c_char, CStr, CString}; + +const WIDTH: u32 = 800; +const HEIGHT: u32 = 600; + +// 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 문자열 형식에 맞춥니다. + +다음으로, 요청된 모든 레이어가 사용 가능한지 확인하는 `check_validation_layer_support` 함수를 추가합니다. `ash`의 `Entry::enumerate_instance_layer_properties` 함수를 사용합니다. + +```rust +// 이 함수는 C 문자열 포인터를 다루므로 unsafe 블록이 필요합니다. +unsafe fn check_validation_layer_support(entry: &ash::Entry) -> bool { + let available_layers = entry + .enumerate_instance_layer_properties() + .expect("인스턴스 레이어 속성을 가져오지 못했습니다."); + + for &layer_name_ptr in VALIDATION_LAYERS.iter() { + let layer_name = CStr::from_ptr(layer_name_ptr); + let mut layer_found = false; + + 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 !layer_found { + return false; + } + } + + true +} +``` + +이제 이 함수를 인스턴스 생성 로직에 통합합니다. + +```rust +// in create_instance() +if ENABLE_VALIDATION_LAYERS && !unsafe { check_validation_layer_support(&self.entry) } { + panic!("요청한 밸리데이션 레이어를 사용할 수 없습니다!"); +} +``` + +마지막으로 `ash`의 빌더 패턴을 사용하여 `InstanceCreateInfo` 구조체를 수정하고, 밸리데이션 레이어를 활성화합니다. + +```rust +// in create_instance() +let mut create_info = vk::InstanceCreateInfo::builder() + .application_info(&app_info) + .enabled_extension_names(&extensions); + +if ENABLE_VALIDATION_LAYERS { + create_info = create_info.enabled_layer_names(&VALIDATION_LAYERS); +} +// ... +``` +`ash`의 빌더는 슬라이스(`&VALIDATION_LAYERS`)를 받아 자동으로 `enabledLayerCount`와 `ppEnabledLayerNames`를 설정해 주므로 매우 편리합니다. + +## 메시지 콜백 (Message Callback) + +밸리데이션 레이어는 기본적으로 디버그 메시지를 표준 출력으로 인쇄하지만, Rust에서 직접 콜백을 제공하여 처리할 수 있습니다. 이를 위해 `VK_EXT_debug_utils` 확장이 필요합니다. + +먼저 필요한 확장 목록을 반환하는 함수를 수정합니다. `ash`는 `ash::extensions::ext::DebugUtils::name()`을 통해 확장 이름을 `&'static CStr`로 제공하여 편리하게 사용할 수 있습니다. + +```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(); + + if ENABLE_VALIDATION_LAYERS { + extensions.push(ash::extensions::ext::DebugUtils::name().as_ptr()); + } + + extensions +} +``` + +이제 디버그 콜백 함수를 정의합니다. 이 함수는 C ABI를 따라야 하므로 `extern "system"`으로 선언합니다. `ash`는 `PFN_vkDebugUtilsMessengerCallbackEXT` 타입 별칭을 제공합니다. + +```rust +use ash::vk; // vk 네임스페이스를 가져옵니다. +use std::os::raw::c_void; + +// 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); + + vk::FALSE +} +``` +**안전성(Safety):** 이 함수는 `unsafe`입니다. C 코드로부터 호출되며, `p_callback_data` 같은 원시 포인터를 역참조하기 때문입니다. `CStr::from_ptr`을 사용하여 메모리를 안전하게 읽습니다. + +이제 이 콜백을 등록하는 `setup_debug_messenger` 함수를 만듭니다. `ash`에서는 확장 기능 로드가 매우 간단합니다. `DebugUtils` 구조체를 생성하기만 하면 됩니다. 이 구조체와 메신저 핸들은 애플리케이션이 살아있는 동안 유지되어야 하므로, 주 애플리케이션 구조체에 멤버로 저장합니다. + +```rust +// 애플리케이션 구조체에 추가 +struct HelloTriangleApplication { + // ... + debug_utils: Option, + debug_messenger: Option, +} + +impl HelloTriangleApplication { + fn setup_debug_messenger(&mut self) { + if !ENABLE_VALIDATION_LAYERS { + return; + } + + 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`를 호출할 필요가 없습니다. + +메신저는 리소스이므로, 애플리케이션이 종료될 때 반드시 정리해야 합니다. Rust에서는 `Drop` 트레잇을 구현하여 이를 자동화하는 것이 가장 이상적입니다(RAII 패턴). + +```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); + } + } +} +``` + +## 인스턴스 생성 및 소멸 디버깅하기 + +`vkCreateInstance`와 `vkDestroyInstance` 호출 자체의 오류를 디버깅하려면, 인스턴스 생성 정보에 디버그 메신저 생성 정보를 연결해야 합니다. `ash` 빌더의 `.p_next()` 메서드를 사용합니다. + +```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); + +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`보다 먼저 선언되어야 합니다. + +이 메신저는 인스턴스 생성 및 소멸 시에만 사용되고 자동으로 정리되므로, 별도로 `destroy`를 호출할 필요가 없습니다. + +## 테스트하기 + +의도적으로 실수를 만들어 밸리데이션 레이어가 작동하는지 확인해 봅시다. `Drop` 구현에서 `destroy_debug_utils_messenger` 호출을 일시적으로 주석 처리하고 디버그 모드로 프로그램을 실행하세요. 프로그램이 종료될 때 다음과 유사한 오류 메시지가 터미널에 출력될 것입니다. + +``` +[Vulkan Validation] [error]: Validation Error: [ UNASSIGNED-ObjectTracker-ObjectLeak ] Object 0x... (type: DEBUG_UTILS_MESSENGER_EXT) was not destroyed. +``` +이 메시지는 디버그 메신저가 제대로 정리되지 않았음을 명확히 알려줍니다. + +## 설정 + +`vk_layer_settings.txt` 파일을 이용한 밸리데이션 레이어 설정은 C++과 동일하게 적용됩니다. 이 파일은 언어에 구애받지 않고 Vulkan SDK가 읽어 들이기 때문입니다. + +`ash`와 Rust를 사용하면 빌더 패턴, 타입 안전성, `Drop`을 통한 자동 리소스 관리(RAII) 등 Rust의 강력한 기능들을 활용하여 C++보다 더 안전하고 간결하게 밸리데이션 레이어를 설정할 수 있습니다. 이제 시스템의 [Vulkan 장치](!ko/Drawing_a_triangle/Setup/Physical_devices_and_queue_families)에 대해 알아볼 시간입니다. + +[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 new file mode 100644 index 00000000..d4f5da7d --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md @@ -0,0 +1,286 @@ +## 물리 디바이스 선택하기 + +`Instance`를 통해 벌칸 라이브러리를 초기화한 후에는, 시스템에서 우리가 필요로 하는 기능을 지원하는 그래픽 카드를 찾아 선택해야 합니다. 사실 여러 개의 그래픽 카드를 선택하여 동시에 사용할 수도 있지만, 이 튜토리얼에서는 우리에게 필요한 첫 번째 그래픽 카드만 사용하겠습니다. + +`pick_physical_device` 함수를 추가하고 `init_vulkan` 함수에서 호출하도록 하겠습니다. Rust에서는 메서드를 `impl` 블록 안에 정의합니다. + +```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) { + // ... + } +} +``` + +최종적으로 선택할 그래픽 카드는 `vk::PhysicalDevice` 핸들에 저장되며, 이 핸들을 구조체의 새 필드로 추가합니다. 이 객체는 `Instance`가 소멸될 때 암시적으로 함께 소멸되므로, `drop` 트레이트에서 따로 처리할 필요는 없습니다. + +`vk::PhysicalDevice::null()`은 C++의 `VK_NULL_HANDLE`에 해당하는 값입니다. + +```rust +struct HelloTriangleApplication { + // ... + physical_device: vk::PhysicalDevice, +} + +impl HelloTriangleApplication { + pub fn new() -> Self { + // ... + Self { + // ... + physical_device: vk::PhysicalDevice::null(), + } + } +} +``` + +그래픽 카드를 나열하는 것은 `ash`를 사용하면 매우 간단합니다. `Instance`의 `enumerate_physical_devices` 메서드는 사용 가능한 모든 물리 디바이스의 `Vec`을 반환합니다. + +```rust +fn pick_physical_device(&mut self) { + let devices = unsafe { + self.instance + .enumerate_physical_devices() + .expect("물리 디바이스를 찾는데 실패했습니다!") + }; +} +``` + +만약 벌칸을 지원하는 디바이스가 하나도 없다면 벡터는 비어있을 것입니다. + +```rust +if devices.is_empty() { + panic!("벌칸을 지원하는 GPU를 찾지 못했습니다!"); +} +``` + +이제 각 디바이스를 평가하여 우리가 수행하려는 작업에 적합한지 확인해야 합니다. 모든 그래픽 카드가 동일하게 만들어지지는 않기 때문입니다. 이를 위해 새로운 헬퍼(helper) 메서드를 도입하겠습니다. + +```rust +fn is_device_suitable(&self, device: vk::PhysicalDevice) -> bool { + true +} +``` + +그리고 물리 디바이스 중 어느 것이든 이 메서드의 요구사항을 충족하는지 확인할 것입니다. + +```rust +for &device in &devices { + if self.is_device_suitable(device) { + self.physical_device = device; + break; + } +} + +if self.physical_device == vk::PhysicalDevice::null() { + panic!("적합한 GPU를 찾지 못했습니다!"); +} +``` + +Rust의 이터레이터(iterator)를 사용하면 더 관용적으로 작성할 수 있습니다. + +```rust +let device = devices.into_iter().find(|&p_device| self.is_device_suitable(p_device)); + +match device { + Some(p_device) => self.physical_device = p_device, + None => panic!("적합한 GPU를 찾지 못했습니다!"), +} +``` + +이 튜토리얼에서는 이해하기 쉽도록 간단한 `for` 루프를 사용하겠습니다. + +## 기본적인 디바이스 적합성 검사 + +디바이스의 적합성을 평가하기 위해 몇 가지 세부 정보를 쿼리하는 것으로 시작할 수 있습니다. `ash`에서는 디바이스 속성을 가져오는 것이 더 간단합니다. + +```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) }; + // ... +} +``` + +예를 들어, 우리 애플리케이션이 지오메트리 셰이더를 지원하는 외장 그래픽 카드에서만 사용 가능하다고 가정해 봅시다. 그렇다면 `is_device_suitable` 함수는 다음과 같이 보일 것입니다. + +```rust +return device_properties.device_type == vk::PhysicalDeviceType::DISCRETE_GPU + && device_features.geometry_shader == vk::TRUE; +``` + +디바이스가 적합한지 아닌지만 확인하고 첫 번째 것을 사용하는 대신, 각 디바이스에 점수를 매겨 가장 높은 점수를 받은 디바이스를 선택할 수도 있습니다. 이런 방식을 사용하면 외장 그래픽 카드에 더 높은 점수를 주어 선호하되, 사용 가능한 유일한 GPU가 내장 GPU일 경우 차선책으로 선택할 수 있습니다. + +```rust +use std::collections::BTreeMap; + +// ... + +fn pick_physical_device(&mut self) { + // ... + + // 후보들을 점수 기준으로 정렬하기 위해 BTreeMap 사용 + let mut candidates = BTreeMap::new(); + + for &device in &devices { + let score = self.rate_device_suitability(device); + candidates.insert(score, device); + } + + // 가장 높은 점수를 받은 후보가 적합한지 확인 + if let Some((score, device)) = candidates.iter().rev().next() { + if *score > 0 { + self.physical_device = *device; + } else { + panic!("적합한 GPU를 찾지 못했습니다!"); + } + } else { + panic!("적합한 GPU를 찾지 못했습니다!"); + } +} + +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) }; + + let mut score = 0; + + // 외장 GPU는 상당한 성능 이점을 가짐 + if device_properties.device_type == vk::PhysicalDeviceType::DISCRETE_GPU { + score += 1000; + } + + // 텍스처의 최대 크기는 그래픽 품질에 영향을 줌 + score += device_properties.limits.max_image_dimension2d; + + // 애플리케이션은 지오메트리 셰이더 없이는 작동할 수 없음 + if device_features.geometry_shader != vk::TRUE { + return 0; + } + + score +} +``` + +이 튜토리얼에서 이 모든 것을 구현할 필요는 없지만, 디바이스 선택 프로세스를 어떻게 설계할 수 있는지에 대한 아이디어를 제공하기 위한 것입니다. + +우리는 이제 막 시작하는 단계이므로, 벌칸 지원 여부만이 유일한 요구사항입니다. 따라서 어떤 GPU든 상관없이 사용하겠습니다. + +```rust +fn is_device_suitable(&self, device: vk::PhysicalDevice) -> bool { + true +} +``` + +다음 섹션에서는 확인해야 할 첫 번째 실질적인 필수 기능에 대해 논의할 것입니다. + +## 큐 패밀리 (Queue families) + +이전에도 잠시 언급했듯이, 드로잉부터 텍스처 업로드에 이르기까지 벌칸의 거의 모든 작업은 명령(command)을 큐(queue)에 제출해야 합니다. 큐에는 여러 종류가 있으며, 이들은 각기 다른 *큐 패밀리(queue families)*에서 비롯됩니다. + +우리는 디바이스가 어떤 큐 패밀리를 지원하는지, 그리고 그중 어떤 큐 패밀리가 우리가 사용하려는 명령을 지원하는지 확인해야 합니다. 이를 위해, 우리가 필요로 하는 모든 큐 패밀리를 찾는 새로운 헬퍼 함수 `find_queue_families`를 추가하겠습니다. + +다음 챕터에서 다른 종류의 큐를 찾게 될 것이므로, 미리 대비하여 인덱스들을 구조체로 묶는 것이 좋습니다. + +```rust +struct QueueFamilyIndices { + graphics_family: Option, +} +``` + +큐 패밀리가 존재하지 않는 경우를 처리해야 합니다. C++17의 `std::optional`과 같이, Rust에는 이를 위한 완벽한 타입인 `Option`가 내장되어 있습니다. `Option`은 값이 존재함(`Some(value)`) 또는 존재하지 않음(`None`)을 나타내는 열거형(enum)입니다. `is_some()` 메서드를 사용하여 값이 있는지 확인할 수 있습니다. + +이제 `find_queue_families`를 실제로 구현해 보겠습니다. + +```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) + }; + + // ... + + indices +} +``` + +`ash`의 `get_physical_device_queue_family_properties` 메서드는 큐 패밀리 속성 정보가 담긴 `Vec`을 반환합니다. 우리는 `VK_QUEUE_GRAPHICS_BIT`를 지원하는 큐 패밀리를 최소 하나 이상 찾아야 합니다. + +`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); + } + + i += 1; +} +``` + +Rust의 `enumerate()`를 사용하면 더 깔끔하게 작성할 수 있습니다. + +```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); + } +} +``` + +이제 이 큐 패밀리 조회 함수를 `is_device_suitable` 메서드에서 검사 항목으로 사용하여, 디바이스가 우리가 사용하려는 명령을 처리할 수 있는지 확인할 수 있습니다. + +```rust +fn is_device_suitable(&self, device: vk::PhysicalDevice) -> bool { + let indices = self.find_queue_families(device); + + indices.graphics_family.is_some() +} +``` + +이를 좀 더 편리하게 만들기 위해, 구조체 자체에 헬퍼 메서드를 추가하겠습니다. + +```rust +impl QueueFamilyIndices { + pub fn is_complete(&self) -> bool { + self.graphics_family.is_some() + } +} + +// ... + +fn is_device_suitable(&self, device: vk::PhysicalDevice) -> bool { + let indices = self.find_queue_families(device); + + indices.is_complete() +} +``` + +이 메서드를 `find_queue_families`에서 조기 탈출하는 데에도 사용할 수 있습니다. + +```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.is_complete() { + break; + } +} +``` + +좋습니다, 이것으로 적절한 물리 디바이스를 찾는 데 필요한 모든 작업이 끝났습니다! 다음 단계는 [논리 디바이스를 생성하여](!ko/Drawing_a_triangle/Setup/Logical_device_and_queues) 물리 디바이스와 상호작용하는 것입니다. + +[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 new file mode 100644 index 00000000..772ce0c6 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md @@ -0,0 +1,165 @@ +## 서론 + +사용할 물리 장치를 선택한 후에는, 이와 상호작용하기 위한 *논리 장치*를 설정해야 합니다. 논리 장치 생성 과정은 인스턴스 생성 과정과 유사하며, 우리가 사용하고자 하는 기능들을 기술합니다. 또한, 어떤 큐 패밀리를 사용할 수 있는지 질의했으므로 이제 어떤 큐를 생성할지 명시해야 합니다. 요구 사항이 다양하다면 동일한 물리 장치에서 여러 개의 논리 장치를 생성할 수도 있습니다. + +먼저 애플리케이션 구조체에 논리 장치를 저장할 새 필드를 추가합니다. `ash::Device`는 논리 장치를 나타내는 타입입니다. + +```rust +use ash::{Device, vk}; + +struct HelloTriangleApplication { + // ... 기존 필드들 ... + physical_device: vk::PhysicalDevice, + device: Device, +} +``` + +다음으로, `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) { + // ... 구현 ... + } +} +``` + +## 생성할 큐 명시하기 + +논리 장치 생성은 여러 구조체를 채우는 것으로 시작합니다. 그 첫 번째는 `vk::DeviceQueueCreateInfo`입니다. 이 구조체는 단일 큐 패밀리에 대해 우리가 원하는 큐의 개수를 기술합니다. 지금은 그래픽스 기능이 있는 큐에만 관심이 있습니다. + +```rust +// create_logical_device 메서드 내부 +let indices = self.find_queue_families(self.physical_device); + +let queue_create_info = vk::DeviceQueueCreateInfo::builder() + .queue_family_index(indices.graphics_family.unwrap()) + .queue_priorities(&[1.0]) // 큐가 하나뿐이라도 우선순위는 필수 + .build(); +``` + +Rust와 `ash`에서는 빌더 패턴을 사용하여 구조체를 더 안전하고 명확하게 생성할 수 있습니다. C++의 `std::optional::value()`는 Rust의 `Option::unwrap()`에 해당합니다. 여기서는 큐 패밀리를 이미 찾았다고 확신할 수 있으므로 `unwrap()`을 사용합니다. + +사용 가능한 최신 드라이버들은 각 큐 패밀리마다 소수의 큐만 생성하도록 허용하며, 실제로 하나보다 더 많이 필요한 경우는 드뭅니다. 여러 스레드에서 모든 커맨드 버퍼를 생성한 뒤, 메인 스레드에서 단 한 번의 저비용 호출로 모두 제출할 수 있기 때문입니다. + +Vulkan은 `0.0`에서 `1.0` 사이의 부동 소수점 값을 사용하여 큐에 우선순위를 할당하고 커맨드 버퍼 실행 스케줄링에 영향을 줄 수 있습니다. 큐가 하나만 있어도 이 설정은 필수입니다. 위 코드에서는 `&[1.0]` 슬라이스를 전달하여 이를 설정했습니다. + +## 사용할 장치 기능 명시하기 + +다음 정보는 우리가 사용할 장치 기능의 집합입니다. 이는 이전 장에서 `vkGetPhysicalDeviceFeatures`로 지원 여부를 확인했던 지오메트리 셰이더 같은 기능들입니다. 지금 당장은 특별한 기능이 필요 없으므로, 모든 필드가 기본값(`false`)으로 설정된 구조체를 생성합니다. + +```rust +let device_features = vk::PhysicalDeviceFeatures::builder().build(); +``` + +더 흥미로운 Vulkan 기능을 사용하기 시작할 때 이 구조체로 다시 돌아올 것입니다. + +## 논리 장치 생성하기 + +이제 `vk::DeviceCreateInfo` 구조체를 채울 준비가 되었습니다. + +```rust +// C-호환 문자열을 위한 준비 +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; + +// 필요한 장치 확장 (스왑체인) +let device_extensions = [ + ash::extensions::khr::Swapchain::name().as_ptr(), +]; + +// 유효성 검사 레이어 설정 (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(); + +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); + +if ENABLE_VALIDATION_LAYERS { + create_info = create_info + .enabled_layer_names(&validation_layer_name_ptrs); +} +``` + +큐 생성 정보와 장치 기능 구조체에 대한 참조를 빌더에 전달합니다. `queue_create_infos`는 슬라이스를 받으므로, `std::slice::from_ref`를 사용하여 단일 항목으로부터 슬라이스를 만듭니다. + +나머지 정보는 `vk::InstanceCreateInfo`와 유사하며 확장과 유효성 검사 레이어를 지정합니다. 차이점은 이번에는 장치에 한정된다는 점입니다. + +장치별 확장의 예로 `VK_KHR_swapchain`이 있습니다. 이 확장은 렌더링된 이미지를 창에 표시하는 데 필수적입니다. 이 튜토리얼에서는 나중에 다루지만, 지금 활성화해 두는 것이 일반적입니다. + +이전 Vulkan 구현은 인스턴스와 장치별 유효성 검사 레이어를 구분했지만 [더 이상은 그렇지 않습니다](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap40.html#extendingvulkan-layers-devicelayerdeprecation). 즉, 최신 구현에서는 `vk::DeviceCreateInfo`의 `enabled_layer_names` 필드가 무시됩니다. 하지만 구버전 구현과의 호환성을 위해 설정해 두는 것이 좋습니다. + +이제 `create_device` 함수를 호출하여 논리 장치를 인스턴스화할 수 있습니다. 이 함수는 FFI(Foreign Function Interface) 호출이므로 `unsafe` 블록 안에서 호출해야 합니다. + +```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 에 저장하거나 직접 할당 +``` + +`ash`에서는 `create_device`가 `Instance`의 메서드입니다. 파라미터는 물리 장치, 생성 정보, 그리고 선택적인 할당 콜백입니다. `expect`를 사용하여 오류 발생 시 프로그램을 패닉시킵니다. + +장치는 애플리케이션이 종료될 때 소멸되어야 합니다. Rust에서는 `Drop` 트레잇을 구현하여 이 작업을 자동화하는 것이 관례입니다. + +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + // ... 다른 리소스 정리 ... + self.device.destroy_device(None); + // ... 인스턴스 정리 ... + } + } +} +``` +논리 장치는 인스턴스와 직접 상호작용하지 않으므로 생성 시 인스턴스가 파라미터로 필요하지 않습니다. + +## 큐 핸들 가져오기 + +큐는 논리 장치와 함께 자동으로 생성되지만, 상호작용할 핸들이 아직 없습니다. 구조체에 그래픽스 큐 핸들을 저장할 필드를 추가합니다. + +```rust +struct HelloTriangleApplication { + // ... + device: Device, + graphics_queue: vk::Queue, +} +``` + +장치 큐는 장치가 파괴될 때 암시적으로 정리되므로, `Drop` 트레잇에서 별도로 정리할 필요가 없습니다. + +`get_device_queue` 함수를 사용하여 큐 핸들을 가져올 수 있습니다. 파라미터는 큐 패밀리 인덱스와 큐 인덱스입니다. 우리는 이 패밀리에서 큐를 하나만 생성했으므로 인덱스 `0`을 사용합니다. + +```rust +// create_logical_device 메서드 마지막 부분 +let graphics_queue = unsafe { self.device.get_device_queue(indices.graphics_family.unwrap(), 0) }; +self.graphics_queue = graphics_queue; +``` + +이제 논리 장치와 큐 핸들을 사용해 그래픽 카드로 작업을 시작할 준비가 되었습니다! 다음 장들에서는 결과를 창 시스템에 표시하기 위한 리소스를 설정할 것입니다. + +[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 new file mode 100644 index 00000000..83566973 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md @@ -0,0 +1,199 @@ +Vulkan은 플랫폼에 구애받지 않는 API이므로, 자체적으로 윈도우 시스템과 직접 통신할 수 없습니다. Vulkan과 윈도우 시스템을 연결하여 렌더링 결과를 화면에 표시하려면, WSI(Window System Integration) 확장을 사용해야 합니다. 이 장에서는 그 첫 번째 확장인 `VK_KHR_surface`에 대해 논의합니다. 이 확장은 렌더링된 이미지를 표시하기 위한 추상적인 표면 타입을 나타내는 `vk::SurfaceKHR` 객체를 제공합니다. 우리 프로그램의 서피스는 `winit`으로 이미 열어 둔 창을 기반으로 합니다. + +`VK_KHR_surface` 확장은 인스턴스 수준 확장(instance-level extension)이며, `ash_window::enumerate_required_extensions`가 반환하는 목록에 포함되어 있으므로 이미 활성화했습니다. 이 목록에는 다음 장들에서 사용할 다른 WSI 확장들도 포함됩니다. + +윈도우 서피스는 물리 장치 선택에 영향을 줄 수 있으므로, 인스턴스 생성 직후에 만들어야 합니다. 이 과정을 뒤로 미룬 이유는 윈도우 서피스가 렌더 타겟 및 프레젠테이션이라는 더 큰 주제의 일부이며, 이를 기본 설정 과정에서 다루면 내용이 복잡해지기 때문입니다. 또한, 오프스크린 렌더링(off-screen rendering)만 필요하다면 윈도우 서피스는 전적으로 선택 사항입니다. Vulkan은 OpenGL처럼 보이지 않는 창을 만드는 꼼수 없이도 오프스크린 렌더링을 허용합니다. + +## 윈도우 서피스 생성 + +먼저 메인 애플리케이션 구조체에 `surface_loader`와 `surface` 필드를 추가합니다. `ash`에서는 확장 기능 함수들을 사용하기 위해 해당 확장 로더(loader)가 필요합니다. + +```rust +use ash::extensions::khr; + +struct VulkanApp { + // ... 기존 필드들 + surface_loader: khr::Surface, + surface: vk::SurfaceKHR, + // ... +} +``` + +`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"); + } +} +``` + +`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(); + } + + fn create_surface(&mut self, window: &winit::window::Window) { + // 서피스 확장 로더 생성 + self.surface_loader = khr::Surface::new(&self.entry, &self.instance); + + // 서피스 생성 + self.surface = unsafe { + ash_window::create_surface(&self.entry, &self.instance, window, None) + .expect("Failed to create window surface") + }; + } +} +``` + +`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); + } + } +} +``` + +## 프레젠테이션 지원 여부 쿼리 + +Vulkan 구현이 윈도우 시스템 통합을 지원하더라도, 시스템의 모든 장치가 이를 지원하는 것은 아닙니다. 따라서 `is_device_suitable`을 확장하여 장치가 우리가 생성한 서피스로 이미지를 출력(present)할 수 있는지 확인해야 합니다. 프레젠테이션은 큐에 특화된 기능이므로, 이 문제는 결국 우리가 만든 서피스로의 프레젠테이션을 지원하는 큐 패밀리를 찾는 문제가 됩니다. + +드로잉 커맨드를 지원하는 큐 패밀리와 프레젠테이션을 지원하는 큐 패밀리가 다를 수 있습니다. 따라서 별도의 프레젠테이션 큐가 존재할 수 있다는 점을 고려하여 `QueueFamilyIndices` 구조체를 수정합니다. + +```rust +#[derive(Default)] +struct QueueFamilyIndices { + graphics_family: Option, + present_family: Option, +} + +impl QueueFamilyIndices { + fn is_complete(&self) -> bool { + self.graphics_family.is_some() && self.present_family.is_some() + } +} +``` + +다음으로, `find_queue_families` 함수를 수정하여 우리 윈도우 서피스로 프레젠테이션할 수 있는 큐 패밀리를 찾도록 합니다. 이를 확인하는 함수는 `get_physical_device_surface_support`이며, `surface_loader`를 통해 호출합니다. + +```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) + }; + + if present_support { + indices.present_family = Some(i); + } + + if indices.is_complete() { + break; + } +} +``` + +두 큐 패밀리가 결국 동일한 큐 패밀리로 결정될 가능성이 높지만, 프로그램 전반에 걸쳐 일관된 접근을 위해 별개의 큐인 것처럼 다룰 것입니다. 물론, 성능 향상을 위해 드로잉과 프레젠테이션을 동일한 큐에서 지원하는 물리 장치를 선호하도록 로직을 추가할 수도 있습니다. + +## 프레젠테이션 큐 생성하기 + +이제 남은 일은 논리 장치 생성 절차를 수정하여 프레젠테이션 큐를 만들고 `vk::Queue` 핸들을 가져오는 것입니다. 구조체에 핸들을 저장할 필드를 추가합니다. + +```rust +struct VulkanApp { + // ... + present_queue: vk::Queue, + // ... +} +``` + +다음으로, 두 큐 패밀리로부터 큐를 생성하기 위해 여러 개의 `vk::DeviceQueueCreateInfo` 구조체가 필요할 수 있습니다. 이를 Rust답게 처리하는 방법은 `HashSet`을 사용하여 필요한 큐 패밀리의 고유한 인덱스 집합을 만드는 것입니다. + +```rust +use std::collections::HashSet; + +// create_logical_device 메서드 내부 +let indices = self.find_queue_families(self.physical_device); + +let mut unique_queue_families = HashSet::new(); +unique_queue_families.insert(indices.graphics_family.unwrap()); +unique_queue_families.insert(indices.present_family.unwrap()); + +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(); +``` + +그리고 `vk::DeviceCreateInfo`가 이 `Vec`을 가리키도록 수정합니다. + +```rust +let device_create_info = vk::DeviceCreateInfo::builder() + .queue_create_infos(&queue_create_infos) + // ... 다른 빌더 호출들 + .build(); +``` + +만약 큐 패밀리가 같다면, `HashSet` 덕분에 해당 인덱스에 대한 정보는 한 번만 전달됩니다. 마지막으로, 프레젠테이션 큐 핸들을 가져오는 호출을 추가합니다. + +```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) }; +``` + +만약 두 큐 패밀리가 같다면, `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 new file mode 100644 index 00000000..a3eb419f --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md @@ -0,0 +1,352 @@ +Vulkan에는 '기본 프레임버퍼(default framebuffer)'라는 개념이 없습니다. 따라서 우리가 렌더링할 버퍼를 화면에 시각화하기 전에 소유할 인프라가 필요합니다. 이 인프라를 **스왑 체인(swap chain)** 이라고 하며, Vulkan에서는 명시적으로 생성해야 합니다. + +스왑 체인은 본질적으로 화면에 표시되기를 기다리는 이미지들의 큐(queue)입니다. 우리 애플리케이션은 렌더링할 이미지를 이 큐에서 가져와(acquire) 렌더링한 다음, 다시 큐에 반환합니다. 큐가 정확히 어떻게 작동하고 큐에서 이미지를 표시하는 조건은 스왑 체인 설정 방식에 따라 다르지만, 스왑 체인의 일반적인 목적은 이미지 표시를 화면의 주사율(refresh rate)과 동기화하는 것입니다. + +## 스왑 체인 지원 확인 + +모든 그래픽 카드가 이미지를 화면에 직접 표시할 수 있는 것은 아닙니다. 예를 들어 서버용으로 설계되어 디스플레이 출력이 없는 경우가 그렇습니다. 둘째로, 이미지 표시는 창 시스템(window system) 및 창과 관련된 표면(surface)과 밀접하게 연관되어 있으므로 실제 Vulkan 코어의 일부가 아닙니다. 따라서 `VK_KHR_swapchain` 장치 확장 기능의 지원 여부를 쿼리한 후 활성화해야 합니다. + +이를 위해 먼저 `is_device_suitable` 함수를 확장하여 이 확장이 지원되는지 확인합니다. `ash` 라이브러리는 `ash::extensions::khr::Swapchain::name()`과 같이 확장 기능의 이름을 안전하게 가져올 수 있는 상수를 제공하여 오타를 방지합니다. + +먼저, 필요한 장치 확장 기능 목록을 상수로 정의합니다. `CStr` 타입을 사용하여 C 문자열과의 호환성을 보장합니다. + +```rust +use std::ffi::CStr; +// ... 다른 use 구문들 + +const DEVICE_EXTENSIONS: [&'static CStr; 1] = [ash::extensions::khr::Swapchain::name()]; +``` + +다음으로, `is_device_suitable`에서 추가 검사로 호출될 새로운 함수 `check_device_extension_support`를 만듭니다. + +```rust +// is_device_suitable 함수 내부에서... +let extensions_supported = + check_device_extension_support(&self.instance, physical_device); + +// ... + +indices.is_complete() && extensions_supported +``` + +```rust +use std::collections::HashSet; +use ash::Instance; +use ash::vk; + +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)); + + // 장치가 지원하는 확장 기능들을 가져옵니다. + // ash는 C++의 2중 호출 패턴 대신 Result>를 바로 반환해 편리합니다. + let available_extensions = unsafe { + instance + .enumerate_device_extension_properties(physical_device) + .expect("Failed to enumerate device extension properties.") + }; + + // 사용 가능한 확장 기능들을 순회하며 필요한 확장 기능 목록에서 제거합니다. + 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); + } + + // 모든 필요한 확장 기능이 제거되었다면 지원하는 것입니다. + required_extensions.is_empty() +} +``` + +Rust에서는 C++의 `std::set` 대신 `std::collections::HashSet`을 사용했습니다. 로직은 동일합니다. 사용 가능한 확장을 순회하면서 필요한 확장 목록에서 하나씩 제거하고, 최종적으로 목록이 비었는지 확인합니다. 이제 코드를 실행하여 그래픽 카드가 실제로 스왑 체인을 생성할 수 있는지 확인하십시오. 이전 장에서 확인했던 표현 큐(presentation queue)의 가용성은 스왑 체인 확장이 지원되어야 함을 의미하지만, 명시적으로 확인하고 활성화하는 것이 좋습니다. + +## 장치 확장 기능 활성화 + +스왑 체인을 사용하려면 먼저 `VK_KHR_swapchain` 확장을 활성화해야 합니다. 논리 장치를 생성할 때 `ash`의 빌더 패턴을 사용하여 활성화할 수 있습니다. + +```rust +// CStr 슬라이스에서 C-호환 포인터 슬라이스를 만듭니다. +let extension_names_raw: Vec<*const i8> = DEVICE_EXTENSIONS + .iter() + .map(|s| s.as_ptr()) + .collect(); + +let device_create_info = vk::DeviceCreateInfo::builder() + .queue_create_infos(&queue_create_infos) + .enabled_features(&device_features) + .enabled_extension_names(&extension_names_raw); // 여기서 확장 기능을 지정합니다. +``` + +## 스왑 체인 지원 상세 정보 쿼리 + +스왑 체인이 사용 가능한지 확인하는 것만으로는 충분하지 않습니다. 스왑 체인이 우리 창 표면(window surface)과 호환되지 않을 수 있기 때문입니다. 이제 스왑 체인 생성에 필요한 세부 정보들을 쿼리해야 합니다. + +* 기본 표면 기능 (스왑 체인의 최소/최대 이미지 수, 이미지의 최소/최대 너비 및 높이) +* 표면 형식 (픽셀 형식, 색 공간) +* 사용 가능한 표현 모드 + +이 정보들을 담을 구조체를 정의합니다. C++ 버전과 유사하지만 Rust 스타일을 따릅니다. + +```rust +struct SwapChainSupportDetails { + capabilities: vk::SurfaceCapabilitiesKHR, + formats: Vec, + present_modes: Vec, +} +``` + +이 구조체를 채울 `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, + } + } +} +``` + +이제 `is_device_suitable` 함수를 다시 수정하여, 확장 기능 지원이 확인된 후에 스왑 체인 지원이 충분한지 확인합니다. 형식이 하나 이상, 표현 모드가 하나 이상이면 충분하다고 간주합니다. + +```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 +} +``` + +## 스왑 체인에 적합한 설정 선택하기 + +`swapchain_adequate` 조건이 충족되었다면, 이제 사용 가능한 옵션 중에서 최적의 설정을 선택해야 합니다. + +### 표면 형식 + +가장 이상적인 형식은 `B8G8R8A8_SRGB` 형식과 `SRGB_NONLINEAR` 색 공간의 조합입니다. 이 조합을 우선적으로 찾고, 없다면 첫 번째 사용 가능한 형식을 선택합니다. + +```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`)를 사용하면 더 간결하고 표현력 있는 코드를 작성할 수 있습니다. + +### 표현 모드 + +표현 모드는 스왑 체인의 동작을 결정하는 가장 중요한 설정입니다. 네 가지 모드가 있습니다: + +* `IMMEDIATE`: 즉시 표시 (티어링 가능성 있음) +* `FIFO`: 수직 동기화와 유사한 큐 방식 (보장된 가용성) +* `FIFO_RELAXED`: `FIFO`의 변형으로, 큐가 비었을 때 지연 없이 표시 (티어링 가능성 있음) +* `MAILBOX`: 삼중 버퍼링과 유사. 큐가 가득 차면 새 이미지로 교체 (낮은 지연 시간, 티어링 없음) + +`MAILBOX` 모드가 성능과 품질 면에서 훌륭한 절충안이므로 우선적으로 선택하고, 없다면 보장된 `FIFO` 모드를 사용합니다. + +```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) +} +``` + +### 스왑 범위 + +스왑 범위는 스왑 체인 이미지의 해상도이며, 보통 창의 픽셀 단위 해상도와 같습니다. + +창 관리자가 해상도를 정해주는 경우(`current_extent`의 너비가 `u32::MAX`가 아닌 경우), 그 값을 그대로 사용합니다. 그렇지 않으면, `winit` (또는 사용하는 창 라이브러리)에서 프레임버퍼의 픽셀 크기를 직접 쿼리하여 사용해야 합니다. 고해상도(HiDPI) 디스플레이에서는 창의 논리적 크기와 픽셀 크기가 다를 수 있기 때문입니다. + +```rust +use winit::window::Window; + +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()가 픽셀 단위 + + let mut actual_extent = vk::Extent2D { + width: framebuffer_size.width, + height: framebuffer_size.height, + }; + + // 해상도를 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 + } +} +``` + +## 스왑 체인 생성 + +이제 모든 준비가 끝났습니다. 지금까지 만든 헬퍼 함수들을 사용하여 스왑 체인을 생성해 봅시다. 주 애플리케이션 구조체에 스왑 체인 관련 필드들을 추가해야 합니다. + +```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` 함수를 구현합니다. + +```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); + + 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); + + // 스왑 체인에 포함될 이미지 개수를 결정합니다. + // 최소값보다 하나 더 요청하는 것이 일반적입니다. + 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); + } + + 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]) + }; + + 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` 로더를 생성해야 합니다. 이 로더는 인스턴스와 논리 장치를 기반으로 만들어지며, 애플리케이션의 상태로 저장되어야 나중에 스왑 체인을 파괴할 때 사용할 수 있습니다. + +애플리케이션이 종료될 때 스왑 체인을 정리하기 위해 `Drop` 트레잇을 구현합니다. + +```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); + // ... 나머지 리소스 정리 + } + } +} +``` + +## 스왑 체인 이미지 가져오기 + +`ash`를 사용하면 스왑 체인 이미지를 매우 간단하게 가져올 수 있습니다. `get_swapchain_images` 함수는 이미지 핸들이 담긴 `Vec`를 바로 반환합니다. 위 `create_swapchain` 함수에서 이미 이 과정을 포함시켰습니다. + +```rust +let swapchain_images = unsafe { + swapchain_loader + .get_swapchain_images(swapchain) + .expect("Failed to get Swapchain Images.") +}; +``` +이미지들은 스왑 체인에 의해 소유되므로, 스왑 체인이 파괴될 때 자동으로 정리됩니다. 별도로 이미지를 파괴할 필요는 없습니다. + +이제 우리는 렌더링하고 창에 표시할 수 있는 이미지 집합을 갖게 되었습니다. 다음 장에서는 이 이미지들을 렌더 타겟으로 설정하고, 실제 그래픽 파이프라인과 그리기 명령에 대해 알아보기 시작하겠습니다! + +[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 new file mode 100644 index 00000000..2a7b9951 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/01_Presentation/02_Image_views.md @@ -0,0 +1,158 @@ +### Rust와 Ash 라이브러리를 사용한 이미지 뷰 생성 + +스왑 체인에 있는 이미지를 포함한 모든 `VkImage`를 렌더 파이프라인에서 사용하려면 `VkImageView` 객체를 생성해야 합니다. 이미지 뷰는 말 그대로 이미지로의 뷰(view)입니다. 이는 이미지에 접근하는 방법과 접근할 이미지의 부분을 기술합니다. 예를 들어, 밉매핑 레벨이 없는 2D 텍스처나 깊이 텍스처로 취급해야 하는지 등을 명시합니다. + +이번 장에서는 스왑 체인의 모든 이미지에 대한 기본적인 이미지 뷰를 만드는 `create_image_views` 함수를 작성할 것입니다. 이렇게 하면 나중에 이미지 뷰들을 컬러 타겟으로 사용할 수 있습니다. + +먼저, 이미지 뷰를 저장할 구조체 필드를 추가합니다. + +```rust +struct VulkanApp { + // ... 다른 필드들 + swapchain_images: Vec, + swapchain_format: vk::Format, + swapchain_extent: vk::Extent2D, + swapchain_image_views: Vec, // 이 필드를 추가합니다. +} +``` + +`create_image_views` 함수를 만들고 스왑 체인 생성 직후에 호출하도록 합니다. Rust에서는 보통 `new` 생성자나 초기화 함수 내에서 순서대로 호출합니다. + +```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) { + // 이 함수를 구현합니다. + } +} +``` + +이제 `create_image_views` 함수를 구현해 보겠습니다. C++ 버전처럼 벡터의 크기를 미리 조정하는 대신, 각 스왑 체인 이미지를 순회하면서 생성된 이미지 뷰를 새 벡터에 추가하는 방식을 사용하겠습니다. + +```rust +fn create_image_views(&mut self) { + self.swapchain_image_views = self.swapchain_images + .iter() + .map(|&image| { + // 여기에 각 이미지에 대한 이미지 뷰 생성 로직이 들어갑니다. + }) + .collect(); +} +``` + +`map` 클로저 내부에서 각 이미지에 대한 이미지 뷰를 생성합니다. 이미지 뷰 생성을 위한 파라미터는 `vk::ImageViewCreateInfo` 구조체에 명시됩니다. `ash` 라이브러리는 빌더(builder) 패턴을 제공하여 이 구조체를 더 안전하고 편리하게 생성할 수 있습니다. + +```rust +// map 클로저 내부 +let components = vk::ComponentMapping { + r: vk::ComponentSwizzle::IDENTITY, + g: vk::ComponentSwizzle::IDENTITY, + b: vk::ComponentSwizzle::IDENTITY, + a: vk::ComponentSwizzle::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, +}; +``` + +`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 클로저의 반환 값 +``` + +`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(); +} +``` + +이미지와 달리, 이미지 뷰는 우리가 명시적으로 생성했으므로, 프로그램이 끝날 때 이를 정리하는 코드를 추가해야 합니다. 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); + } + // ... 다른 리소스 정리 + } + } +} +``` + +`destroy_image_view` 또한 `unsafe` 함수입니다. + +이미지 뷰는 이미지를 텍스처로 사용하기 시작하기에는 충분하지만, 렌더 타겟으로 사용되기에는 아직 완전히 준비되지 않았습니다. 이를 위해서는 프레임버퍼(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 new file mode 100644 index 00000000..d8f1b543 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md @@ -0,0 +1,54 @@ +앞으로 몇 개의 챕터에 걸쳐 첫 번째 삼각형을 그리기 위해 구성된 그래픽스 파이프라인을 설정할 것입니다. 그래픽스 파이프라인은 메시의 정점(vertices)과 텍스처(textures)를 가져와 렌더 타겟의 픽셀에 이르는 일련의 작업 순서입니다. 아래에 간소화된 개요가 표시되어 있습니다: + +![](/images/vulkan_simplified_pipeline.svg) + +* **입력 조립기(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를 사용해 본 적이 있다면, 파이프라인 설정을 마음대로 변경하는 데 익숙할 것입니다. 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 new file mode 100644 index 00000000..cfbb31d6 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md @@ -0,0 +1,179 @@ +이전 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 바이너리를 생성하도록 설계되었습니다. 우리는 `glslc` 컴파일러를 사용하여 GLSL 셰이더를 SPIR-V로 변환할 것입니다. `glslc`는 GCC나 Clang과 같은 잘 알려진 컴파일러와 동일한 파라미터 형식을 사용하고, *인클루드*와 같은 추가 기능을 포함합니다. 이 컴파일러는 Vulkan SDK에 이미 포함되어 있으므로 추가로 다운로드할 필요가 없습니다. + +*(이후 GLSL에 대한 설명, Vertex/Fragment 셰이더 코드, 컴파일 방법은 원문과 동일하므로 생략하고 Rust 코드 구현 부분부터 시작하겠습니다. 아래의 GLSL 코드를 각각 `shaders/shader.vert`와 `shaders/shader.frag` 파일로 저장하고 컴파일해 `vert.spv`와 `frag.spv` 파일을 준비했다고 가정합니다.)* + +**Vertex Shader (`shader.vert`)** +```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]; +} +``` + +**Fragment Shader (`shader.frag`)** +```glsl +#version 450 + +layout(location = 0) in vec3 fragColor; + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(fragColor, 1.0); +} +``` + +## 셰이더 로드하기 + +이제 SPIR-V 셰이더를 생성했으니, 프로그램에 로드하여 그래픽 파이프라인에 연결할 시간입니다. Rust의 표준 라이브러리를 사용하면 파일에서 바이너리 데이터를 매우 쉽게 읽을 수 있습니다. + +`create_graphics_pipeline` 함수에서 두 셰이더의 바이트코드를 로드합니다. + +```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!"); + // ... +} +``` + +`std::fs::read` 함수는 파일의 모든 바이트를 읽어 `Vec` (바이트 벡터)로 반환합니다. C++ 예제처럼 수동으로 파일 크기를 계산하고 버퍼를 할당할 필요가 없습니다. 파일 읽기에 실패하면 `expect`가 프로그램을 패닉시키므로, 실제 애플리케이션에서는 `Result`를 적절히 처리해야 합니다. + +## 셰이더 모듈 생성하기 + +코드를 파이프라인에 전달하기 전에 `ash::vk::ShaderModule` 객체로 감싸야 합니다. 이를 위해 `create_shader_module` 헬퍼 함수를 만들어 보겠습니다. + +```rust +// In `impl VulkanApp` +fn create_shader_module(&self, code: &[u8]) -> vk::ShaderModule { + // ... +} +``` + +이 함수는 바이트코드가 담긴 슬라이스를 파라미터로 받아 `vk::ShaderModule`을 생성합니다. + +셰이더 모듈을 만드는 것은 간단합니다. 바이트코드와 그 길이를 지정하면 됩니다. C++ 버전에서는 `char` 포인터를 `uint32_t` 포인터로 `reinterpret_cast`해야 했고, 이 과정에서 데이터 정렬(alignment) 문제가 발생할 수 있었습니다. SPIR-V는 `u32`의 슬라이스를 기대하기 때문입니다. + +Rust에서는 `ash`가 제공하는 유틸리티 함수를 사용해 이 과정을 안전하고 간단하게 처리할 수 있습니다. `ash::util::read_spv`는 바이트 슬라이스(`&[u8]`)를 받아 `Vec`로 변환해주므로 정렬 문제를 걱정할 필요가 없습니다. + +```rust +use ash::{util, vk}; +use std::io::Cursor; + +// ... + +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); + + unsafe { + self.device + .create_shader_module(&create_info, None) + .expect("Failed to create shader module!") + } +} +``` + +이제 `create_shader_module` 함수를 `create_graphics_pipeline` 내에서 호출합니다. + +```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); + + // ... +} +``` + +셰이더 모듈은 우리가 이전에 파일에서 로드한 셰이더 바이트코드와 그 안에 정의된 함수들을 얇게 감싼 래퍼일 뿐입니다. SPIR-V 바이트코드를 GPU가 실행할 수 있는 기계어 코드로 컴파일하고 링크하는 작업은 그래픽 파이프라인이 생성될 때까지 일어나지 않습니다. + +이는 파이프라인 생성이 완료되는 즉시 셰이더 모듈을 파괴해도 된다는 의미입니다. Rust의 소유권(ownership) 모델 덕분에 이 과정은 자동으로 처리됩니다. `vert_shader_module`과 `frag_shader_module` 변수는 `create_graphics_pipeline` 함수가 끝날 때 범위를 벗어나고, 이때 자동으로 `vkDestroyShaderModule`이 호출됩니다. C++ 예제처럼 수동으로 `vkDestroyShaderModule`을 호출할 필요가 없어 코드가 더 깔끔하고 안전합니다. + +## 셰이더 스테이지 생성 + +셰이더를 실제로 사용하려면 실제 파이프라인 생성 과정의 일부로서 `vk::PipelineShaderStageCreateInfo` 구조체를 통해 특정 파이프라인 단계에 할당해야 합니다. + +`create_graphics_pipeline` 함수에서 버텍스 셰이더를 위한 구조체를 채우는 것으로 시작하겠습니다. `ash`의 빌더 패턴을 사용하면 코드가 더 읽기 쉬워집니다. + +```rust +// ... inside create_graphics_pipeline, after creating shader modules + +// Vulkan은 C 문자열을 기대하므로 CString으로 변환해야 합니다. +let main_function_name = std::ffi::CString::new("main").unwrap(); + +let vert_shader_stage_info = vk::PipelineShaderStageCreateInfo::builder() + .stage(vk::ShaderStageFlags::VERTEX) + .module(vert_shader_module) + .name(&main_function_name); +``` + +첫 번째 단계는 셰이더가 사용될 파이프라인 단계(`stage`)를 알려주는 것입니다. 여기서는 `VERTEX` 단계를 지정합니다. 그 다음 코드를 포함하는 셰이더 모듈(`module`)과 호출할 함수, 즉 *엔트리포인트(entrypoint)*의 이름(`name`)을 지정합니다. 여기서는 표준적인 `main`을 사용합니다. + +`pSpecializationInfo`라는 선택적 멤버도 있지만, 여기서는 사용하지 않으므로 빌더에서 설정하지 않으면 기본값인 `null`로 처리됩니다. + +프래그먼트 셰이더에 맞게 구조체를 수정하는 것은 쉽습니다. + +```rust +let frag_shader_stage_info = vk::PipelineShaderStageCreateInfo::builder() + .stage(vk::ShaderStageFlags::FRAGMENT) + .module(frag_shader_module) + .name(&main_function_name); +``` + +마지막으로 이 두 구조체를 포함하는 슬라이스를 정의합니다. 이 슬라이스는 나중에 실제 파이프라인 생성 단계에서 이들을 참조하는 데 사용될 것입니다. + +```rust +let shader_stages = [vert_shader_stage_info.build(), frag_shader_stage_info.build()]; +``` +`.build()`를 호출하여 빌더를 최종 구조체로 변환하는 것을 잊지 마세요. + +이제 파이프라인의 프로그래밍 가능 단계를 모두 기술했습니다. 함수 마지막에 셰이더 모듈을 수동으로 정리하는 코드를 추가합니다. + +```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에서는 변수가 범위를 벗어날 때 자동으로 리소스가 해제되도록 래퍼 타입을 만들어 관리하는 것이 일반적입니다.* + +다음 장에서는 고정 함수 단계를 살펴보겠습니다. + +[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 new file mode 100644 index 00000000..b0fa4a09 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md @@ -0,0 +1,229 @@ +이전의 그래픽 API들은 그래픽 파이프라인의 대부분 단계에 대한 기본 상태를 제공했습니다. Vulkan에서는 대부분의 파이프라인 상태를 명시적으로 지정해야 하며, 이 상태들은 불변(immutable)의 파이프라인 상태 객체(PSO)로 구워지기(baked) 때문입니다. 이번 장에서는 이러한 고정 함수(fixed-function) 연산을 구성하기 위한 모든 구조체를 채워 넣을 것입니다. Rust와 `ash` 라이브러리를 사용하면 빌더 패턴을 통해 이 과정을 더 명확하게 진행할 수 있습니다. + +## 동적 상태 (Dynamic state) + +*대부분의* 파이프라인 상태는 파이프라인 상태 객체에 구워져야 하지만, 제한된 일부 상태는 파이프라인을 다시 만들지 않고도 드로우 타임(draw time)에 변경할 수 *있습니다*. 뷰포트의 크기, 선 두께, 블렌딩 상수 등이 그 예입니다. 만약 동적 상태를 사용하고 이러한 속성들을 파이프라인 생성 시에 고정하지 않으려면, `vk::PipelineDynamicStateCreateInfo` 구조체를 채워야 합니다. `ash`의 빌더를 사용하면 다음과 같이 작성할 수 있습니다. + +```rust +let dynamic_states = [vk::DynamicState::VIEWPORT, vk::DynamicState::SCISSOR]; + +let dynamic_state_info = vk::PipelineDynamicStateCreateInfo::builder() + .dynamic_states(&dynamic_states); +``` + +`ash`의 빌더는 슬라이스(`&[T]`)를 받아 내부적으로 개수(`dynamicStateCount`)와 포인터(`pDynamicStates`)를 자동으로 설정해줍니다. 이렇게 하면 이 값들의 구성이 파이프라인 생성 시에는 무시되며, 드로잉 시점에 이 데이터를 지정해야만 합니다. 이는 더 유연한 설정을 가능하게 하며, 뷰포트나 시저 상태처럼 파이프라인 상태에 고정시킬 경우 설정이 더 복잡해질 수 있는 항목들에 대해 매우 일반적인 방식입니다. + +## 정점 입력 (Vertex input) + +`vk::PipelineVertexInputStateCreateInfo` 구조체는 정점 셰이더로 전달될 정점 데이터의 형식을 설명합니다. 이 설명은 크게 두 가지 방식으로 이루어집니다. + +* **바인딩(Bindings)**: 데이터 간의 간격 및 데이터가 정점별(per-vertex)인지 인스턴스별(per-instance)인지 여부 (자세한 내용은 [인스턴싱](https://ko.wikipedia.org/wiki/인스턴싱) 참조) +* **속성 서술(Attribute descriptions)**: 정점 셰이더로 전달되는 속성의 유형, 어떤 바인딩에서 로드할지, 그리고 어떤 오프셋에 있는지 + +지금은 정점 데이터를 정점 셰이더에 직접 하드코딩하고 있으므로, 이 구조체를 채워서 로드할 정점 데이터가 없음을 명시할 것입니다. `ash` 빌더의 기본값은 비어있는 상태이므로 코드가 매우 간결해집니다. + +```rust +let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::builder() + .vertex_binding_descriptions(&[]) + .vertex_attribute_descriptions(&[]); +``` + +`vertex_binding_descriptions`와 `vertex_attribute_descriptions` 메서드에 빈 슬라이스(`&[]`)를 전달하면, `ash`가 자동으로 카운트를 0으로, 포인터는 널(null)로 설정합니다. 이 구조체는 나중에 정점 버퍼 장에서 다시 다룰 것입니다. `create_graphics_pipeline` 함수에서 셰이더 단계 정의 바로 다음에 이 코드를 추가하세요. + +## 입력 조립 (Input assembly) + +`vk::PipelineInputAssemblyStateCreateInfo` 구조체는 정점들로부터 어떤 종류의 지오메트리를 그릴 것인지와 프리미티브 재시작(primitive restart) 활성화 여부를 설명합니다. + +사용 가능한 `topology`의 종류는 다음과 같습니다: + +* `vk::PrimitiveTopology::POINT_LIST`: 점 +* `vk::PrimitiveTopology::LINE_LIST`: 선 (재사용 없음) +* `vk::PrimitiveTopology::LINE_STRIP`: 연결된 선 +* `vk::PrimitiveTopology::TRIANGLE_LIST`: 삼각형 (재사용 없음) +* `vk::PrimitiveTopology::TRIANGLE_STRIP `: 연결된 삼각형 + +이 튜토리얼에서는 삼각형을 그릴 것이므로 다음과 같이 구조체를 설정합니다. + +```rust +let input_assembly = vk::PipelineInputAssemblyStateCreateInfo::builder() + .topology(vk::PrimitiveTopology::TRIANGLE_LIST) + .primitive_restart_enable(false); +``` + +`primitive_restart_enable`을 `true`로 설정하면, `_STRIP` 토폴로지 모드에서 특수 인덱스(`0xFFFF` 또는 `0xFFFFFFFF`)를 사용하여 프리미티브를 끊을 수 있습니다. + +## 뷰포트와 시저 (Viewports and scissors) + +뷰포트(viewport)는 출력이 렌더링될 프레임버퍼의 영역을 설명합니다. 시저 사각형(scissor rectangle)은 픽셀이 실제로 저장될 영역을 정의하며, 이 영역 밖의 픽셀은 버려집니다. + +```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, +}; + +let scissor = vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: self.swapchain_extent, +}; +``` + +뷰포트와 시저는 정적 또는 동적 상태로 설정할 수 있습니다. 동적 상태를 사용하는 것이 일반적이며 성능 저하도 없습니다. + +동적 상태를 사용할 경우, 앞서 정의한 `dynamic_state_info`가 이 역할을 합니다. 파이프라인 생성 시에는 뷰포트와 시저의 개수만 지정하면 됩니다. + +```rust +let viewport_state = vk::PipelineViewportStateCreateInfo::builder() + .viewport_count(1) + .scissor_count(1); +``` + +이 경우 실제 `viewport`와 `scissor` 데이터는 나중에 커맨드 버퍼에 직접 기록해야 합니다. + +만약 동적 상태를 사용하지 않고 파이프라인에 이 값들을 고정시키려면, 빌더에 실제 데이터를 전달해야 합니다. + +```rust +// 동적 상태를 사용하지 않을 경우 +let viewport_state = vk::PipelineViewportStateCreateInfo::builder() + .viewports(&[viewport]) + .scissors(&[scissor]); +``` +`ash` 빌더의 `.viewports()`와 `.scissors()` 메서드는 슬라이스를 인자로 받습니다. 우리는 동적 상태를 사용할 것이므로 위의 첫 번째 방법을 따릅니다. + +## 래스터라이저 (Rasterizer) + +래스터라이저는 정점 셰이더가 만든 지오메트리를 프래그먼트로 변환합니다. `vk::PipelineRasterizationStateCreateInfo`로 이 단계를 설정합니다. + +```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...`: 섀도우 매핑 등에서 깊이 값에 편향을 줄 때 사용합니다. + +## 멀티샘플링 (Multisampling) + +`vk::PipelineMultisampleStateCreateInfo`는 안티 앨리어싱 기법 중 하나인 멀티샘플링을 설정합니다. 지금은 비활성화합니다 (GPU 기능 필요). + +```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회만 수행하도록 설정합니다. + +## 깊이 및 스텐실 테스팅 (Depth and stencil testing) + +깊이/스텐실 버퍼를 사용한다면 `vk::PipelineDepthStencilStateCreateInfo`로 관련 테스트를 설정해야 합니다. 지금은 사용하지 않으므로, 최종 파이프라인 생성 정보에 이 부분은 널 포인터(`std::ptr::null()`)를 전달하여 비활성화할 것입니다. + +## 색상 혼합 (Color blending) + +프래그먼트 셰이더가 반환한 색상을 프레임버퍼의 기존 색상과 결합하는 단계입니다. 프레임버퍼별로 `vk::PipelineColorBlendAttachmentState`를, 전역적으로 `vk::PipelineColorBlendStateCreateInfo`를 설정합니다. + +먼저, 단일 프레임버퍼에 대한 혼합 상태입니다. 혼합을 비활성화하면 프래그먼트 셰이더의 출력이 그대로 프레임버퍼에 쓰입니다. + +```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`라는 편리한 상수를 제공합니다. + +알파 블렌딩(반투명 효과)을 구현하려면 `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); +``` + +이제 전역 색상 혼합 상태를 설정합니다. 위에서 만든 첨부 상태(attachment state)를 참조합니다. + +```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, + // ... +} +``` + +`create_graphics_pipeline` 함수 내에서 객체를 생성합니다. +```rust +let pipeline_layout_info = vk::PipelineLayoutCreateInfo::builder() + .set_layouts(&[]) + .push_constant_ranges(&[]); + +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 ... + } + } +} +``` + +## 결론 + +이것으로 모든 고정 함수 상태 설정이 끝났습니다! 처음부터 모든 것을 설정하는 것은 많은 작업이지만, 그 장점은 이제 그래픽 파이프라인에서 일어나는 거의 모든 일을 완전히 인지하게 되었다는 것입니다. 이는 특정 컴포넌트의 기본 상태가 예상과 달라 예기치 않은 동작에 부딪힐 가능성을 줄여줍니다. + +하지만 그래픽 파이프라인을 최종적으로 생성하기 전에 만들어야 할 객체가 하나 더 있으며, 그것은 바로 [렌더 패스](!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 new file mode 100644 index 00000000..8c9bdaba --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md @@ -0,0 +1,137 @@ +## 설정 + +파이프라인 생성을 완료하기 전에, Vulkan에게 렌더링 중에 사용될 프레임버퍼 어태치먼트(attachment)에 대해 알려주어야 합니다. 우리는 몇 개의 색상 및 깊이 버퍼가 있을지, 각각에 몇 개의 샘플을 사용할지, 그리고 렌더링 작업 전반에 걸쳐 해당 콘텐츠를 어떻게 처리해야 하는지 지정해야 합니다. 이 모든 정보는 *렌더 패스(render pass)* 객체에 담기게 되며, 이를 위해 새로운 `create_render_pass` 함수를 만들 것입니다. 이 함수를 주 애플리케이션 초기화 로직에서 `create_graphics_pipeline` 앞에 호출하세요. + +```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) + +우리의 경우, 스왑체인의 이미지 중 하나로 표현되는 단일 색상 버퍼 어태치먼트만 갖게 될 것입니다. 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(); +``` + +색상 어태치먼트의 `format`은 스왑체인 이미지의 포맷과 일치해야 하며, 아직 멀티샘플링은 다루지 않으므로 `vk::SampleCountFlags::TYPE_1` 샘플을 사용하겠습니다. + +`load_op`와 `store_op`는 렌더링 전후에 어태치먼트의 데이터를 어떻게 처리할지를 결정합니다. `load_op`에 사용할 수 있는 선택지는 다음과 같습니다: + +* `vk::AttachmentLoadOp::LOAD`: 어태치먼트의 기존 내용을 보존합니다. +* `vk::AttachmentLoadOp::CLEAR`: 시작 시 값을 특정 상수로 지웁니다. +* `vk::AttachmentLoadOp::DONT_CARE`: 기존 내용이 정의되지 않음(undefined); 신경 쓰지 않습니다. + +우리의 경우, 새 프레임을 그리기 전에 프레임버퍼를 검은색으로 지우기 위해 clear 작업을 사용할 것입니다. `store_op`에는 두 가지 가능성만 있습니다: + +* `vk::AttachmentStoreOp::STORE`: 렌더링된 내용은 메모리에 저장되어 나중에 읽을 수 있습니다. +* `vk::AttachmentStoreOp::DONT_CARE`: 렌더링 작업 후 프레임버퍼의 내용은 정의되지 않습니다. + +우리는 렌더링된 삼각형을 화면에서 보고 싶으므로, store 작업을 사용할 것입니다. + +`load_op`와 `store_op`는 색상 및 깊이 데이터에 적용되며, `stencil_load_op` / `stencil_store_op`는 스텐실 데이터에 적용됩니다. 우리 애플리케이션은 스텐실 버퍼를 사용하지 않으므로, 로딩과 저장 결과는 중요하지 않습니다. + +Vulkan에서 텍스처와 프레임버퍼는 특정 픽셀 포맷을 가진 `VkImage` 객체로 표현됩니다. 하지만 이미지로 무엇을 하려는지에 따라 메모리 내 픽셀의 레이아웃이 변경될 수 있습니다. + +가장 일반적인 레이아웃 중 일부는 다음과 같습니다: + +* `vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL`: 색상 어태치먼트로 사용되는 이미지 +* `vk::ImageLayout::PRESENT_SRC_KHR`: 스왑체인에 표시(present)될 이미지 +* `vk::ImageLayout::TRANSFER_DST_OPTIMAL`: 메모리 복사 작업의 대상으로 사용될 이미지 + +`initial_layout`은 렌더 패스가 시작되기 전에 이미지가 어떤 레이아웃을 가질지 지정합니다. `final_layout`은 렌더 패스가 끝날 때 자동으로 전환될 레이아웃을 지정합니다. `initial_layout`에 `vk::ImageLayout::UNDEFINED`를 사용하면 이미지의 이전 레이아웃이 무엇이었는지 신경 쓰지 않겠다는 의미입니다. 우리는 렌더링 후에 이미지가 스왑체인을 통해 화면에 표시될 준비가 되기를 원하므로, `final_layout`으로 `vk::ImageLayout::PRESENT_SRC_KHR`을 사용합니다. + +## 서브패스와 어태치먼트 참조 + +하나의 렌더 패스는 여러 개의 서브패스(subpass)로 구성될 수 있습니다. 서브패스는 이전 패스의 프레임버퍼 내용에 의존하는 후속 렌더링 작업입니다. 우리의 첫 번째 삼각형에서는 단일 서브패스만 사용할 것입니다. + +모든 서브패스는 어태치먼트 명세 중 하나 이상을 참조합니다. 이러한 참조는 `vk::AttachmentReference` 구조체를 통해 이루어집니다. + +```rust +let color_attachment_ref = vk::AttachmentReference::builder() + .attachment(0) + .layout(vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .build(); +``` + +`attachment` 파라미터는 어태치먼트 명세 배열의 인덱스(`0`)를 통해 참조할 어태치먼트를 지정합니다. `layout`은 이 참조를 사용하는 서브패스 동안 어태치먼트가 가지길 원하는 레이아웃을 지정합니다. 우리는 이 어태치먼트를 색상 버퍼로 사용할 것이며, `vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL` 레이아웃이 최상의 성능을 제공할 것입니다. + +서브패스는 `vk::SubpassDescription` 구조체를 사용하여 설명됩니다. Rust에서는 C++의 포인터와 개수 대신 슬라이스(`&[...]`)를 사용합니다. + +```rust +let subpass = vk::SubpassDescription::builder() + .pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS) + .color_attachments(&[color_attachment_ref]) // 참조를 슬라이스로 전달 + .build(); +``` + +Vulkan은 미래에 컴퓨트 서브패스도 지원할 수 있으므로, `pipeline_bind_point`를 통해 이것이 그래픽스 서브패스임을 명시적으로 지정해야 합니다. `color_attachments` 필드는 색상 어태치먼트 참조의 슬라이스를 받습니다. + +이 배열에서 어태치먼트의 인덱스는 프래그먼트 셰이더에서 `layout(location = 0) out vec4 outColor` 지시문을 통해 직접 참조됩니다! + +## 렌더 패스 + +이제 어태치먼트와 이를 참조하는 서브패스가 설명되었으므로, 렌더 패스 자체를 생성할 수 있습니다. 애플리케이션 구조체에 `render_pass` 필드를 추가하세요. + +```rust +struct HelloTriangleApplication { + // ... + render_pass: vk::RenderPass, + pipeline_layout: vk::PipelineLayout, + // ... +} +``` + +이제 `vk::RenderPassCreateInfo` 구조체를 채워 렌더 패스를 생성합니다. 이 구조체는 어태치먼트와 서브패스의 슬라이스를 받습니다. + +```rust +let attachments = [color_attachment]; +let subpasses = [subpass]; + +let render_pass_info = vk::RenderPassCreateInfo::builder() + .attachments(&attachments) + .subpasses(&subpasses) + .build(); + +self.render_pass = unsafe { + self.device + .create_render_pass(&render_pass_info, None) +}?; +``` + +Ash 라이브러리에서 Vulkan 객체를 생성하거나 파괴하는 대부분의 함수는 `unsafe`로 표시되어 있습니다. 이는 개발자가 유효한 파라미터와 올바른 소멸 순서를 보장해야 함을 상기시켜 줍니다. Rust의 `?` 연산자를 사용하면 `Result`를 반환하는 함수에서 에러를 간결하게 처리할 수 있습니다. + +파이프라인 레이아웃과 마찬가지로 렌더 패스는 프로그램 전반에서 참조되므로, 프로그램이 끝날 때 정리해야 합니다. 일반적으로 Rust에서는 `Drop` 트레이트 구현을 통해 이 작업을 수행합니다. + +```rust +// Drop 트레이트 구현 내에서 +unsafe { + self.device.destroy_pipeline_layout(self.pipeline_layout, None); + self.device.destroy_render_pass(self.render_pass, None); + // ... +} +``` + +상당히 많은 작업이었지만, 다음 장에서는 이 모든 것이 합쳐져 마침내 그래픽스 파이프라인 객체를 생성하게 될 것입니다 \ 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 new file mode 100644 index 00000000..3ce6187f --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md @@ -0,0 +1,89 @@ +이제 이전 챕터들에서 다룬 모든 구조체와 객체를 조합하여 그래픽스 파이프라인을 만들 수 있습니다! 우리가 지금까지 다룬 객체 유형을 간단히 요약하면 다음과 같습니다: + +* **셰이더 스테이지(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) +``` + +먼저 `.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); +``` + +그다음, 고정 함수 스테이지를 설명하는 모든 구조체를 참조합니다. `depth_stencil_state`와 같은 선택적 필드는 빌더에서 해당 메서드를 호출하지 않으면 자동으로 null 포인터로 설정되므로 코드가 더 간결해집니다. + +```rust + .layout(pipeline_layout) +``` + +그다음은 파이프라인 레이아웃인데, 이것은 구조체 포인터가 아닌 Vulkan 핸들입니다. + +```rust + .render_pass(render_pass) + .subpass(0) +``` + +마지막으로 렌더 패스와, 이 그래픽스 파이프라인이 사용될 서브패스의 인덱스에 대한 참조가 있습니다. 이 파이프라인을 이 특정 인스턴스 대신 다른 렌더 패스와 함께 사용하는 것도 가능하지만, 그 렌더 패스들은 `render_pass`와 *호환 가능(compatible)*해야 합니다. 호환성 요구 사항은 [여기](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap8.html#renderpass-compatibility)에 설명되어 있지만, 이 튜토리얼에서는 해당 기능을 사용하지 않을 것입니다. + +```rust + .base_pipeline_handle(vk::Pipeline::null()) // 선택 사항 + .base_pipeline_index(-1); // 선택 사항 +``` + +실제로는 두 개의 필드가 더 있습니다: `base_pipeline_handle`과 `base_pipeline_index`. Vulkan에서는 기존 파이프라인에서 파생하여 새로운 그래픽스 파이프라인을 생성할 수 있습니다. 파이프라인 파생(derivatives)의 개념은, 기존 파이프라인과 많은 기능이 공통될 때 파이프라인을 설정하는 비용이 저렴해지고, 동일한 부모에서 파생된 파이프라인 간의 전환도 더 빠르게 수행할 수 있다는 것입니다. `base_pipeline_handle`로 기존 파이프라인의 핸들을 지정하거나, `base_pipeline_index`로 지금 생성하려는 다른 파이프라인을 인덱스로 참조할 수 있습니다. 지금은 파이프라인이 하나뿐이므로, null 핸들(`vk::Pipeline::null()`)과 유효하지 않은 인덱스(-1)를 지정하겠습니다. 이 값들은 `vk::PipelineCreateFlags::DERIVATIVE` 플래그가 지정된 경우에만 사용됩니다. 빌더 패턴에서는 이들을 명시적으로 설정하지 않으면 기본값으로 설정됩니다. + +이제 마지막 단계를 위해 `VkPipeline` 객체를 담을 구조체 필드를 준비합니다. + +```rust +struct HelloTriangleApplication { + // ... + graphics_pipeline: vk::Pipeline, + // ... +} +``` + +그리고 마침내 그래픽스 파이프라인을 생성합니다: + +```rust +self.graphics_pipeline = unsafe { + device + .create_graphics_pipelines(vk::PipelineCache::null(), &[pipeline_info.build()], None) + .expect("Failed to create graphics pipeline!") +}[0]; +``` + +`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); + // ... +} +``` + +이제 프로그램을 실행하여 이 모든 노력이 성공적인 파이프라인 생성으로 이어졌는지 확인하세요! 이제 화면에 무언가 나타나는 것에 꽤 가까워졌습니다. 다음 몇 개의 챕터에서는 스왑 체인 이미지로부터 실제 프레임버퍼를 설정하고 드로잉 커맨드를 준비할 것입니다. \ 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 new file mode 100644 index 00000000..91249215 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md @@ -0,0 +1,116 @@ +### 프레임버퍼 (Rust / Ash) + +지난 몇 장에 걸쳐 프레임버퍼에 대해 많이 이야기했고, 스왑 체인 이미지와 동일한 포맷을 가진 단일 프레임버퍼를 사용하도록 렌더 패스를 설정했지만, 아직 실제로 생성하지는 않았습니다. + +렌더 패스를 생성할 때 지정한 첨부(attachment)들은 `vk::Framebuffer` 객체로 감싸서 바인딩됩니다. 프레임버퍼 객체는 첨부를 나타내는 모든 `vk::ImageView` 객체를 참조합니다. 우리의 경우에는 단 하나, 바로 색상 첨부(color attachment)입니다. 하지만 첨부에 사용해야 할 이미지는 우리가 프레젠테이션을 위해 스왑 체인에서 이미지를 가져올 때 어떤 이미지를 반환하는지에 따라 달라집니다. 이는 스왑 체인의 모든 이미지에 대해 프레임버퍼를 생성하고, 드로잉 시점에는 가져온 이미지에 해당하는 것을 사용해야 한다는 의미입니다. + +이를 위해, 프레임버퍼를 담을 `Vec` 구조체 필드를 추가합니다: + +```rust +struct HelloTriangleApplication { + // ... + render_pass: vk::RenderPass, + pipeline_layout: vk::PipelineLayout, + graphics_pipeline: vk::Pipeline, + swapchain_framebuffers: Vec, + // ... +} +``` + +이 벡터를 채우기 위한 객체들은 애플리케이션 생성자(`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); + // ... + } + + // ... + + fn create_framebuffers( + device: &ash::Device, + render_pass: vk::RenderPass, + image_views: &[vk::ImageView], + swapchain_extent: vk::Extent2D, + ) -> Vec { + // ... 구현 ... + } +} +``` + +`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 +} +``` + +보시다시피, Ash의 빌더 패턴 덕분에 프레임버퍼 생성이 매우 간단합니다. +먼저 프레임버퍼가 어떤 `render_pass`와 호환되어야 하는지 지정해야 합니다. 프레임버퍼는 호환되는 렌더 패스와만 사용할 수 있는데, 이는 대략적으로 말해 동일한 수와 유형의 첨부를 사용한다는 것을 의미합니다. + +- `render_pass()`: 프레임버퍼가 호환되어야 할 렌더 패스를 지정합니다. +- `attachments()`: 렌더 패스의 `pAttachments` 배열에 있는 각 첨부 설명에 바인딩될 `vk::ImageView` 객체의 슬라이스를 지정합니다. +- `width()`와 `height()`: 이름에서 알 수 있듯이 명확하며, 스왑 체인의 크기(`extent`)에서 가져옵니다. +- `layers()`: 이미지 배열의 레이어 수를 나타냅니다. 우리의 스왑 체인 이미지는 단일 이미지이므로 레이어 수는 `1`입니다. + +`device.create_framebuffer` 호출은 `unsafe` 블록 안에 있습니다. 이는 유효하지 않은 핸들이나 매개변수를 전달할 경우 Vulkan 드라이버가 정의되지 않은 동작을 일으킬 수 있기 때문이며, Rust 컴파일러는 이를 보장할 수 없습니다. + +생성된 프레임버퍼는 애플리케이션이 종료될 때 정리해야 합니다. Rust에서는 `Drop` 트레잇을 구현하여 이 작업을 자동으로 처리하는 것이 일반적입니다. 프레임버퍼는 그것들이 참조하는 이미지 뷰나 렌더 패스보다 먼저 파괴되어야 합니다. `Drop` 구현에서 파괴 순서를 명시적으로 제어하는 것이 좋습니다. + +```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); + } + + // ... 나머지 리소스 정리 ... + } + } +} +``` + +이제 우리는 렌더링에 필요한 모든 객체를 갖추는 중요한 단계에 도달했습니다. 다음 장에서는 첫 실제 드로잉 명령을 작성할 것입니다. \ 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 new file mode 100644 index 00000000..42784aee --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md @@ -0,0 +1,274 @@ +Vulkan에서 그리기 연산이나 메모리 전송과 같은 커맨드(command)는 함수 호출을 통해 직접 실행되지 않습니다. 대신, 수행하려는 모든 작업을 커맨드 버퍼(command buffer) 객체에 기록(record)해야 합니다. 이 방식의 장점은 Vulkan에게 무엇을 할지 알려줄 준비가 되었을 때 모든 커맨드가 함께 제출된다는 것입니다. 그러면 Vulkan은 모든 커맨드를 한 번에 사용할 수 있으므로 더 효율적으로 처리할 수 있습니다. 또한, 원한다면 여러 스레드에서 커맨드 기록을 수행할 수도 있습니다. + +## 커맨드 풀 (Command pools) + +커맨드 버퍼를 생성하기 전에 먼저 커맨드 풀(command pool)을 생성해야 합니다. 커맨드 풀은 버퍼를 저장하는 데 사용되는 메모리를 관리하며, 커맨드 버퍼는 이 풀에서 할당됩니다. 메인 애플리케이션 `struct`에 `vk::CommandPool`을 저장할 새 필드를 추가합니다. + +```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) { + // ... + } +} +``` + +커맨드 풀 생성에는 `ash`의 빌더(builder) 패턴을 사용하는 것이 편리하고 안전합니다. + +```rust +let queue_family_indices = self.find_queue_families(self.physical_device); + +let pool_info = vk::CommandPoolCreateInfo::builder() + .flags(vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER) + .queue_family_index(queue_family_indices.graphics_family.unwrap()); +``` + +`ash`의 빌더를 사용하면 `sType` 필드가 자동으로 채워져 코드가 더 깔끔해집니다. 플래그는 타입-세이프(type-safe) 열거형으로 제공됩니다. + +* `vk::CommandPoolCreateFlags::TRANSIENT`: 커맨드 버퍼가 새로운 커맨드로 매우 자주 다시 기록될 것임을 암시합니다 (메모리 할당 동작이 변경될 수 있음). +* `vk::CommandPoolCreateFlags::RESET_COMMAND_BUFFER`: 커맨드 버퍼를 개별적으로 다시 기록할 수 있도록 허용합니다. 이 플래그가 없으면 모든 커맨드 버퍼를 함께 리셋해야 합니다. + +우리는 매 프레임마다 커맨드 버퍼를 기록할 것이므로, 이를 리셋하고 다시 기록할 수 있어야 합니다. 따라서 커맨드 풀에 `RESET_COMMAND_BUFFER` 플래그를 설정해야 합니다. + +커맨드 버퍼는 우리가 가져온 그래픽스 및 프레젠테이션 큐와 같은 장치 큐 중 하나에 제출하여 실행됩니다. 각 커맨드 풀은 단일 유형의 큐에 제출되는 커맨드 버퍼만 할당할 수 있습니다. 우리는 그리기를 위한 커맨드를 기록할 것이므로 그래픽스 큐 패밀리를 선택했습니다. `Option` 타입의 큐 패밀리 인덱스는 `unwrap()`을 통해 값을 가져옵니다. + +```rust +self.command_pool = unsafe { + self.device + .create_command_pool(&pool_info, None) + .expect("Failed to create command pool!") +}; +``` + +`ash`에서 생성 함수는 `Device`나 `Instance`의 메서드로 제공됩니다. C++의 `nullptr`는 Rust의 `None`에 해당합니다. Vulkan API 호출은 드라이버와의 상호작용과 유효한 상태 유지를 프로그래머에게 위임하므로 `unsafe` 블록 안에서 호출해야 합니다. `ash` 함수는 `Result`를 반환하므로 `?` 연산자나 `expect`를 사용해 에러를 처리할 수 있습니다. + +커맨드 풀은 프로그램이 끝날 때 파괴되어야 합니다. Rust에서는 `Drop` 트레잇을 구현하여 리소스 정리를 자동화하는 것이 일반적입니다. + +```rust +impl Drop for VulkanApp { + fn drop(&mut self) { + unsafe { + self.device.destroy_command_pool(self.command_pool, None); + // ... + } + } +} +``` + +## 커맨드 버퍼 할당 + +이제 커맨드 버퍼 할당을 시작할 수 있습니다. `vk::CommandBuffer` 객체를 `struct`의 필드로 추가합니다. 커맨드 버퍼는 커맨드 풀이 파괴될 때 자동으로 해제되므로, 명시적인 정리 코드가 필요하지 않습니다. + +```rust +struct VulkanApp { + // ... + command_pool: vk::CommandPool, + command_buffer: vk::CommandBuffer, + // ... +} +``` + +이제 커맨드 풀에서 단일 커맨드 버퍼를 할당하는 `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] + }; + } +} +``` + +커맨드 버퍼는 `allocate_command_buffers` 메서드로 할당됩니다. 이 메서드는 `Vec`를 반환하므로, 하나만 할당했더라도 첫 번째 요소(`[0]`)를 가져와야 합니다. + +`level` 매개변수는 할당된 커맨드 버퍼가 주(primary) 커맨드 버퍼인지 보조(secondary) 커맨드 버퍼인지를 지정합니다. + +* `vk::CommandBufferLevel::PRIMARY`: 큐에 제출하여 실행할 수 있지만, 다른 커맨드 버퍼에서 호출될 수는 없습니다. +* `vk::CommandBufferLevel::SECONDARY`: 직접 제출할 수는 없지만, 주 커맨드 버퍼에서 호출될 수 있습니다. + +여기서는 보조 커맨드 버퍼 기능을 사용하지 않겠지만, 주 커맨드 버퍼에서 공통 작업을 재사용하는 데 유용하다는 것을 상상할 수 있습니다. + +## 커맨드 버퍼 기록 + +이제 실행하려는 커맨드를 커맨드 버퍼에 작성하는 `record_command_buffer` 메서드 작업을 시작하겠습니다. 현재 스왑체인 이미지의 인덱스를 매개변수로 받습니다. + +```rust +impl VulkanApp { + fn record_command_buffer(&self, image_index: u32) { + // ... + } +} +``` + +커맨드 버퍼 기록은 항상 `begin_command_buffer`를 호출하는 것으로 시작합니다. + +```rust +let begin_info = vk::CommandBufferBeginInfo::builder(); + +unsafe { + self.device + .begin_command_buffer(self.command_buffer, &begin_info) + .expect("Failed to begin recording command buffer!"); +} +``` + +`flags` 매개변수는 커맨드 버퍼 사용 방식을 지정합니다. `ash`의 빌더는 기본적으로 플래그를 0으로 설정합니다. + +* `vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT`: 커맨드 버퍼는 한 번 실행된 직후 다시 기록될 것입니다. +* `vk::CommandBufferUsageFlags::RENDER_PASS_CONTINUE`: 이것은 단일 렌더 패스 내에서만 사용될 보조 커맨드 버퍼입니다. +* `vk::CommandBufferUsageFlags::SIMULTANEOUS_USE`: 커맨드 버퍼가 이미 실행 대기 중인 상태에서도 다시 제출될 수 있습니다. + +지금 우리에게는 이 플래그들 중 어느 것도 해당되지 않습니다. 커맨드 버퍼가 이미 기록되었다면 `begin_command_buffer` 호출은 암시적으로 리셋합니다. + +## 렌더 패스 시작하기 + +그리기는 `cmd_begin_render_pass`로 렌더 패스를 시작하는 것으로 시작됩니다. + +```rust +let render_pass_info = { + let clear_color = vk::ClearValue { + color: vk::ClearColorValue { + float32: [0.0, 0.0, 0.0, 1.0], + }, + }; + + let render_area = vk::Rect2D { + offset: vk::Offset2D { x: 0, y: 0 }, + extent: self.swapchain_extent, + }; + + vk::RenderPassBeginInfo::builder() + .render_pass(self.render_pass) + .framebuffer(self.swapchain_framebuffers[image_index as usize]) + .render_area(render_area) + .clear_values(&[clear_color]) +}; +``` + +`ash`에서는 배열을 전달해야 하는 곳에 Rust의 슬라이스(`&[...]`)를 사용합니다. `image_index`를 사용하여 현재 스왑체인 이미지에 맞는 프레임버퍼를 선택합니다. 렌더 영역은 셰이더 로드 및 저장이 일어날 위치를 정의하며, 최상의 성능을 위해 어태치먼트 크기와 일치시키는 것이 좋습니다. 마지막으로, `VK_ATTACHMENT_LOAD_OP_CLEAR`에 사용할 소거 값을 검은색으로 설정했습니다. + +```rust +unsafe { + self.device.cmd_begin_render_pass( + self.command_buffer, + &render_pass_info, + vk::SubpassContents::INLINE, + ); +} +``` + +커맨드를 기록하는 모든 함수는 `cmd_` 접두사를 가지며 `Device`의 메서드입니다. 반환값이 없으므로 오류 처리가 필요 없습니다. 마지막 매개변수는 다음 두 값 중 하나를 가집니다: + +* `vk::SubpassContents::INLINE`: 렌더 패스 커맨드가 주 커맨드 버퍼 자체에 포함됩니다. +* `vk::SubpassContents::SECONDARY_COMMAND_BUFFERS`: 렌더 패스 커맨드가 보조 커맨드 버퍼에서 실행됩니다. + +우리는 보조 커맨드 버퍼를 사용하지 않으므로 `INLINE`을 선택합니다. + +## 기본 드로잉 커맨드 + +이제 그래픽스 파이프라인을 바인딩합니다. + +```rust +unsafe { + self.device.cmd_bind_pipeline( + self.command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.graphics_pipeline, + ); +} +``` + +파이프라인의 뷰포트와 시저 상태를 동적으로 지정했으므로, 드로우 커맨드 전에 이를 설정해야 합니다. + +```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`에서는 뷰포트와 시저 같은 단일 항목도 슬라이스(`&[...]`)로 전달해야 합니다. + +이제 삼각형을 그리기 위한 드로우 커맨드를 실행합니다. + +```rust +unsafe { + self.device.cmd_draw(self.command_buffer, 3, 1, 0, 0); +} +``` + +`cmd_draw` 함수는 사전에 많은 정보를 지정했기 때문에 매우 간단합니다. + +* `vertex_count`: 정점 버퍼가 없지만, 3개의 정점을 그립니다. +* `instance_count`: 인스턴스 렌더링에 사용됩니다. (`1`은 미사용) +* `first_vertex`: `gl_VertexIndex`의 최솟값을 정의하는 오프셋입니다. +* `first_instance`: `gl_InstanceIndex`의 최솟값을 정의하는 오프셋입니다. + +## 마무리 + +렌더 패스를 종료하고 커맨드 버퍼 기록을 마칩니다. + +```rust +unsafe { + self.device.cmd_end_render_pass(self.command_buffer); + + self.device + .end_command_buffer(self.command_buffer) + .expect("Failed to record command buffer!"); +} +``` + +다음 챕터에서는 메인 루프 코드를 작성할 것입니다. 이 루프는 스왑 체인에서 이미지를 가져오고, 커맨드 버퍼를 기록 및 실행한 다음, 완성된 이미지를 스왑 체인으로 반환하는 작업을 수행합니다. \ 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 new file mode 100644 index 00000000..df3e2704 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md @@ -0,0 +1,334 @@ +이번 장에서는 모든 것을 하나로 합칠 시간입니다. 메인 루프에서 호출되어 삼각형을 화면에 그리는 `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(); + } + // ... + } + }) +} + +// ... + +impl HelloTriangleApplication { + fn draw_frame(&mut self) { + // 여기에 렌더링 로직을 작성합니다. + } +} +``` +*참고: Rust에서는 이벤트 루프 기반으로 동작하므로, C++의 `while` 루프 대신 `winit`의 `RedrawRequested` 이벤트 핸들러 내에서 `draw_frame`을 호출하는 것이 일반적입니다.* + +## 프레임의 개요 + +높은 수준에서 Vulkan으로 프레임을 렌더링하는 것은 다음과 같은 공통된 단계로 구성됩니다. + +* 이전 프레임이 끝나기를 기다립니다. +* 스왑 체인에서 이미지를 가져옵니다. +* 가져온 이미지에 장면을 그리는 커맨드 버퍼를 기록합니다. +* 기록된 커맨드 버퍼를 제출합니다. +* 스왑 체인 이미지를 제시(present)합니다. + +이후 장에서 드로잉 함수를 더 확장하겠지만, 지금으로서는 이것이 우리 렌더링 루프의 핵심입니다. + + + +## 동기화 + + + +Vulkan의 핵심 설계 철학 중 하나는 GPU에서의 실행 동기화가 명시적이라는 것입니다. (이하 동기화에 대한 개념 설명은 C++ 버전과 동일하므로 생략하고, Rust/Ash 구현에 초점을 맞춥니다.) + +... (세마포어와 펜스에 대한 개념 설명) ... + +### 무엇을 선택해야 할까? + +우리에게는 두 가지 동기화 프리미티브가 있고, 마침 동기화를 적용할 두 곳이 있습니다. 스왑 체인 작업과 이전 프레임이 끝나기를 기다리는 것입니다. 스왑 체인 작업은 GPU에서 발생하므로 세마포어를 사용하고, 이전 프레임이 끝나기를 기다리는 작업은 CPU(호스트)가 기다려야 하므로 펜스를 사용합니다. 이는 GPU가 커맨드 버퍼를 사용하는 동안 CPU가 해당 커맨드 버퍼를 덮어쓰지 않도록 보장하기 위함입니다. + +## 동기화 객체 생성하기 + +단일 프레임만 처리하는 대신, 여러 프레임이 동시에 처리 중(in-flight)일 수 있는 보다 일반적인 접근 방식을 사용하겠습니다. 이를 통해 GPU가 하나의 프레임을 렌더링하는 동안 CPU는 다음 프레임을 준비할 수 있어 성능이 향상됩니다. `MAX_FRAMES_IN_FLIGHT` 상수를 정의하고, 각 프레임에 대한 동기화 객체 세트를 생성합니다. + +```rust +const MAX_FRAMES_IN_FLIGHT: usize = 2; + +// AppData 구조체에 추가 +struct AppData { + // ... + image_available_semaphores: Vec, + render_finished_semaphores: Vec, + in_flight_fences: Vec, + // ... +} + +// HelloTriangleApplication 구조체에 추가 +struct HelloTriangleApplication { + // ... + current_frame: usize, + // ... +} +``` + +이제 동기화 객체를 생성하는 `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, + } + } +} + +// 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); + } + } + + ( + image_available_semaphores, + render_finished_semaphores, + in_flight_fences, + ) +} +``` +`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); + } + // ... 다른 리소스 정리 ... + } + } +} +``` + +## `draw_frame` 구현하기 + +### 이전 프레임 기다리기 + +프레임 시작 시, 현재 프레임에 할당된 펜스를 기다려 이전 작업이 완료되었는지 확인합니다. + +```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!"); + } +} +``` +`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 병렬 실행을 극대화할 수 있습니다. + +### 커맨드 버퍼 기록 및 제출 + +이제 이미지를 사용할 수 있으므로, 해당 이미지에 그리는 커맨드 버퍼를 다시 기록하고 제출합니다. + +```rust +// draw_frame 함수 내 +let command_buffer = self.command_buffers[image_index as usize]; + +unsafe { + self.device + .reset_command_buffer(command_buffer, vk::CommandBufferResetFlags::empty()) + .expect("Failed to reset Command Buffer!"); +} + +// 이전 장에서 만든 함수를 호출 +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`를 전달하여, 이 작업이 끝나면 해당 펜스에 신호를 보내도록 합니다. + +### 서브패스 종속성 + +이미지 레이아웃 전환을 올바른 시점에 수행하기 위해 서브패스 종속성을 설정해야 합니다. `acquire_next_image`가 완료된 후, 그리고 우리가 이미지에 색상을 쓰기 시작하기 전에 전환이 일어나야 합니다. `create_render_pass` 함수를 수정합니다. + +```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(); + +let dependencies = [dependency]; + +let render_pass_info = vk::RenderPassCreateInfo::builder() + .attachments(&attachments) + .subpasses(&subpasses) + .dependencies(&dependencies); // 종속성 추가 +``` +이 종속성은 외부(스왑체인 이미지 획득)에서 우리의 첫 번째 서브패스(인덱스 0)로의 전환을 제어합니다. `COLOR_ATTACHMENT_OUTPUT` 단계에서 쓰기 작업(`COLOR_ATTACHMENT_WRITE`)을 시작하기 직전에 전환이 발생하도록 보장합니다. + +### 프레젠테이션 + +마지막으로 렌더링된 이미지를 화면에 표시하기 위해 프레젠테이션 큐에 제출합니다. + +```rust +// draw_frame 함수 내 +let swapchains = [self.app_data.swapchain]; +let image_indices = [image_index]; + +let present_info = vk::PresentInfoKHR::builder() + .wait_semaphores(&signal_semaphores) // 렌더링이 끝나면 신호를 받는 세마포어를 기다림 + .swapchains(&swapchains) + .image_indices(&image_indices); + +unsafe { + self.swapchain_loader + .queue_present(self.present_queue, &present_info) + .expect("Failed to present queue."); +} + +// 다음 프레임을 위해 프레임 인덱스를 업데이트 +self.current_frame = (self.current_frame + 1) % MAX_FRAMES_IN_FLIGHT; +``` +`queue_present`는 렌더링이 완료되었음을 알리는 `render_finished_semaphore`를 기다린 후 실행됩니다. 마지막으로 `current_frame` 인덱스를 순환시켜 다음 `draw_frame` 호출에서 다음 프레임의 동기화 객체 세트를 사용하도록 합니다. + +### 유휴 상태 대기 + +프로그램이 종료될 때, 모든 비동기 작업이 완료될 때까지 기다려야 리소스를 안전하게 해제할 수 있습니다. `main_loop`가 끝난 후와 `Drop` 구현의 시작 부분에서 `device_wait_idle`을 호출합니다. + +```rust +// main_loop에서 이벤트 루프가 끝난 후 (실제로는 winit의 클로저 밖) +// 또는 Drop 구현에서 +unsafe { + self.device.device_wait_idle().unwrap(); +} +``` +이제 프로그램을 실행하면 화면에 삼각형이 나타나고, 창을 닫아도 유효성 검사 오류 없이 깔끔하게 종료될 것입니다. + +## 결론 + +상당한 양의 코드를 통해 드디어 화면에 무언가를 띄웠습니다! Vulkan의 부트스트래핑 과정은 복잡하지만, 그만큼 명시성을 통해 강력한 제어권을 얻을 수 있습니다. 지금까지 작성한 코드를 다시 살펴보며 각 Vulkan 객체의 역할과 상호 관계를 이해하는 시간을 갖는 것이 좋습니다. + +다음 장에서는 여러 프레임을 동시에 처리하는 렌더링 루프를 더욱 견고하게 만들고 창 크기 변경과 같은 예외 상황을 처리하는 방법을 알아보겠습니다. \ 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 new file mode 100644 index 00000000..c72e7c65 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md @@ -0,0 +1,182 @@ +## 동시에 여러 프레임 렌더링하기 (Frames in flight) + +현재 우리의 렌더 루프에는 한 가지 명백한 결함이 있습니다. 이전 프레임의 렌더링이 끝나기를 기다려야만 다음 프레임의 렌더링을 시작할 수 있다는 점인데, 이는 호스트(CPU)의 불필요한 유휴 상태를 유발합니다. + + + +이 문제를 해결하는 방법은 여러 프레임을 동시에 *작업 중(in-flight)* 상태로 두는 것입니다. 즉, 한 프레임의 렌더링이 다음 프레임의 기록을 방해하지 않도록 하는 것입니다. 어떻게 이렇게 할 수 있을까요? 렌더링 중에 접근하고 수정하는 모든 리소스를 복제해야 합니다. 따라서 여러 개의 커맨드 버퍼, 세마포어, 펜스가 필요합니다. 이후 챕터에서는 다른 리소스들의 여러 인스턴스도 추가할 것이므로, 이 개념은 다시 등장하게 될 것입니다. + +먼저 프로그램 상단에 동시에 처리할 프레임 수를 정의하는 상수를 추가합니다. Rust에서는 `usize` 타입을 사용하는 것이 일반적입니다. + +```rust +const MAX_FRAMES_IN_FLIGHT: usize = 2; +``` + +2를 선택한 이유는 CPU가 GPU보다 *너무* 앞서 나가는 것을 원치 않기 때문입니다. 2개의 프레임이 동시 실행되면, CPU와 GPU가 동시에 각자의 작업을 처리할 수 있습니다. 만약 CPU가 먼저 작업을 마치면, GPU가 렌더링을 마칠 때까지 기다렸다가 다음 작업을 제출합니다. 3개 이상의 프레임을 사용하면 CPU가 GPU를 앞질러 지연 시간(latency)을 추가할 수 있습니다. 일반적으로 추가적인 지연 시간은 바람직하지 않습니다. 하지만 애플리케이션에 동시 실행 프레임 수를 제어할 수 있는 권한을 주는 것은 Vulkan의 명시적인(explicit) 특성을 보여주는 또 다른 예시입니다. + +각 프레임은 자체적인 커맨드 버퍼, 세마포어 집합, 펜스를 가져야 합니다. 기존 객체들을 `Vec`으로 변경합니다. `App` 구조체 내의 필드들을 다음과 같이 수정합니다. + +```rust +struct App { + // ... + command_buffers: Vec, + image_available_semaphores: Vec, + render_finished_semaphores: Vec, + in_flight_fences: Vec, + // ... +} +``` + +다음으로 여러 개의 커맨드 버퍼를 생성해야 합니다. `create_command_buffers` 함수를 수정합니다. `ash`의 `allocate_command_buffers` 함수는 `Vec`를 반환하므로, 반환된 벡터를 바로 필드에 할당하면 됩니다. + +```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); + + self.command_buffers = unsafe { + self.device + .allocate_command_buffers(&command_buffer_allocate_info) + .expect("Failed to allocate Command Buffers!") + }; +} +``` + +`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); + } + } +} +``` + +마찬가지로, 이 객체들도 `cleanup`에서 모두 정리되어야 합니다. Rust의 `unsafe` 블록 안에서 파괴 함수를 호출합니다. + +```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); + } + // ... +} +``` + +커맨드 버퍼는 커맨드 풀이 해제될 때 자동으로 해제되므로, 커맨드 버퍼 정리를 위해 추가로 할 일은 없습니다. + +매 프레임마다 올바른 객체를 사용하기 위해, 현재 프레임을 추적해야 합니다. 이를 위해 `App` 구조체에 프레임 인덱스를 추가합니다. + +```rust +struct App { + // ... + current_frame: usize, + // ... +} +// App::new()에서 초기화... +current_frame: 0, +``` + +이제 `draw_frame` 함수를 올바른 객체들을 사용하도록 수정할 수 있습니다. `self.current_frame`을 사용하여 각 프레임에 맞는 동기화 객체와 커맨드 버퍼에 접근합니다. + +```rust +fn draw_frame(&mut self) { + let in_flight_fence = self.in_flight_fences[self.current_frame]; + + unsafe { + self.device + .wait_for_fences(&[in_flight_fence], true, u64::MAX) + .expect("Failed to wait for Fence!"); + + 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!"); + } + + 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 로직 ... +} +``` + +물론, 매번 다음 프레임으로 넘어가는 것을 잊지 말아야 합니다. `draw_frame` 함수의 마지막 부분에 추가합니다. + +```rust +fn draw_frame(&mut self) { + // ... + + self.current_frame = (self.current_frame + 1) % MAX_FRAMES_IN_FLIGHT; +} +``` + +나머지(%) 연산자를 사용함으로써, 프레임 인덱스는 `MAX_FRAMES_IN_FLIGHT` 만큼의 프레임이 큐에 쌓인 후 다시 순환하게 됩니다. + +이제 우리는 최대 `MAX_FRAMES_IN_FLIGHT`개의 프레임만 작업 큐에 쌓이도록 하고, 이 프레임들이 서로를 침범하지 않도록 하는 데 필요한 모든 동기화를 구현했습니다. 최종 정리(cleanup)와 같은 코드의 다른 부분에서는 `device.device_wait_idle()`처럼 더 단순한 동기화에 의존해도 괜찮다는 점에 유의하세요. 성능 요구사항에 따라 어떤 접근 방식을 사용할지 결정해야 합니다. + +동기화에 대해 예제를 통해 더 배우고 싶다면, Khronos의 [이 광범위한 개요](https://github.com/KhronosGroup/Vulkan-Docs/wiki/Synchronization-Examples#swapchain-image-acquire-and-present)를 살펴보세요. + +다음 챕터에서는 잘 동작하는 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 new file mode 100644 index 00000000..8042c7e6 --- /dev/null +++ b/ko-rust/03_Drawing_a_triangle/04_Swap_chain_recreation.md @@ -0,0 +1,282 @@ +## 소개 + +지금까지 우리가 만든 애플리케이션은 성공적으로 삼각형을 그리지만, 아직 제대로 처리하지 못하는 몇 가지 상황이 있습니다. 윈도우 서피스가 변경되어 스왑 체인이 더 이상 호환되지 않는 경우가 발생할 수 있습니다. 이런 상황이 발생하는 원인 중 하나는 윈도우 크기가 변경되는 것입니다. 우리는 이러한 이벤트를 감지하고 스왑 체인을 다시 만들어야 합니다. + +## 스왑 체인 재구성 + +`recreate_swapchain`이라는 새로운 함수를 만들어, `create_swapchain`과 스왑 체인 또는 윈도우 크기에 의존하는 모든 객체들의 생성 함수를 호출하도록 합시다. + +```rust +fn recreate_swapchain(&mut self) -> Result<(), Box> { + unsafe { + self.device.device_wait_idle()?; + } + + self.create_swapchain()?; + self.create_image_views()?; + self.create_framebuffers()?; + + Ok(()) +} +``` + +먼저 `device_wait_idle`을 호출하는데, 이는 이전 장에서와 마찬가지로 아직 사용 중일 수 있는 리소스에 접근해서는 안 되기 때문입니다. 이 함수는 `unsafe` 블록 안에서 호출해야 합니다. 당연히 스왑 체인 자체를 다시 만들어야 합니다. 이미지 뷰는 스왑 체인 이미지에 직접 기반하므로 다시 만들어야 합니다. 마지막으로, 프레임버퍼는 스왑 체인 이미지에 직접 의존하므로 다시 만들어야 합니다. + +이러한 객체들의 이전 버전이 재생성되기 전에 확실히 정리되도록, 일부 정리 코드를 별도의 함수로 옮겨 `recreate_swapchain` 함수에서 호출하도록 합시다. 이 함수를 `cleanup_swapchain`이라고 부르겠습니다. + +```rust +fn cleanup_swapchain(&mut self) { + unsafe { + self.framebuffers + .iter() + .for_each(|&framebuffer| self.device.destroy_framebuffer(framebuffer, None)); + + self.swapchain_image_views + .iter() + .for_each(|&view| self.device.destroy_image_view(view, None)); + + self.swapchain_loader + .destroy_swapchain(self.swapchain, None); + } +} + +fn recreate_swapchain(&mut self) -> Result<(), Box> { + unsafe { + self.device.device_wait_idle()?; + } + + self.cleanup_swapchain(); + + self.create_swapchain()?; + self.create_image_views()?; + self.create_framebuffers()?; + + Ok(()) +} +``` + +여기서는 간단하게 하기 위해 렌더 패스를 다시 만들지 않는다는 점에 유의하세요. 이론적으로는 애플리케이션 실행 중에 스왑 체인 이미지 포맷이 변경될 수 있습니다. 예를 들어, 표준 다이나믹 레인지(SDR) 모니터에서 하이 다이나믹 레인지(HDR) 모니터로 창을 옮기는 경우가 그렇습니다. 이 경우 다이나믹 레인지 간의 변경이 올바르게 반영되도록 애플리케이션이 렌더 패스를 다시 만들어야 할 수도 있습니다. + +스왑 체인 갱신의 일부로 재생성되는 모든 객체들의 정리 코드를 `cleanup` (Rust에서는 `Drop` 트레이트 구현)에서 `cleanup_swapchain`으로 옮기겠습니다. Rust에서는 리소스 해제를 위해 `Drop` 트레이트를 구현하는 것이 일반적입니다. + +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + self.device.device_wait_idle().unwrap(); + + self.cleanup_swapchain(); + + 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 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); + } + + self.device.destroy_command_pool(self.command_pool, None); + + self.device.destroy_device(None); + + if VALIDATION.is_enable { + self.debug_utils_loader + .destroy_debug_utils_messenger(self.debug_messenger, None); + } + + self.surface_loader.destroy_surface(self.surface, None); + self.instance.destroy_instance(None); + } + } +} +``` +`choose_swap_extent`에서는 이미 새로운 윈도우 해상도를 조회하여 스왑 체인 이미지가 (새로운) 올바른 크기를 갖도록 하고 있으므로, `choose_swap_extent`를 수정할 필요는 없습니다 (스왑 체인을 만들 때 이미 서피스의 해상도를 픽셀 단위로 얻기 위해 `window.get_framebuffer_size()`를 사용해야 했던 것을 기억하세요). + +이것만으로도 스왑 체인을 재구성할 수 있습니다! 하지만 이 방법의 단점은 새로운 스왑 체인을 만들기 전에 모든 렌더링을 중단해야 한다는 것입니다. 이전 스왑 체인의 이미지에 대한 그리기 명령이 아직 실행 중인 상태에서 새로운 스왑 체인을 만드는 것도 가능합니다. 그러려면 `vk::SwapchainCreateInfoKHR` 구조체의 `old_swapchain` 필드에 이전 스왑 체인을 전달하고, 이전 스왑 체인 사용이 끝나는 즉시 파괴해야 합니다. + +## 준최적(Suboptimal) 또는 오래된(out-of-date) 스왑 체인 + +이제 스왑 체인 재구성이 언제 필요한지 파악하고 새로운 `recreate_swapchain` 함수를 호출하기만 하면 됩니다. 다행히도 Vulkan은 보통 프레젠테이션 중에 스왑 체인이 더 이상 적합하지 않다고 알려줍니다. Ash 라이브러리는 `acquire_next_image`와 `queue_present` 함수에서 `Result` 타입을 반환하여 이를 명확하게 처리합니다. + +* `Err(vk::Result::ERROR_OUT_OF_DATE_KHR)`: 스왑 체인이 서피스와 호환되지 않게 되어 더 이상 렌더링에 사용할 수 없습니다. 보통 윈도우 리사이즈 후에 발생합니다. +* `Ok((_image_index, true))`: `acquire_next_image`에서 반환되는 튜플의 두 번째 값이 `true`이면 스왑 체인이 준최적(suboptimal) 상태임을 의미합니다. `queue_present`에서는 `Ok(true)`가 준최적 상태를 나타냅니다. + +```rust +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()), +}; +``` + +`acquire_next_image`가 `ERROR_OUT_OF_DATE_KHR` 오류를 반환하면 더 이상 현재 스왑 체인으로 프레젠테이션을 할 수 없습니다. 따라서 즉시 스왑 체인을 재구성하고 `draw_frame`을 종료하여 다음 루프에서 다시 시도해야 합니다. Ash의 `acquire_next_image`는 성공 시 `(u32, bool)` 튜플을 반환하는데, 두 번째 `bool` 값은 준최적 여부를 나타냅니다. 일단은 이미지를 성공적으로 획득했으므로 준최적 상태는 무시하고 진행합니다. + +```rust +let present_info = vk::PresentInfoKHR::builder() + // ... + ; + +let result = unsafe { self.swapchain_loader.queue_present(self.present_queue, &present_info) }; + +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()), + _ => {} +}; + +self.current_frame = (self.current_frame + 1) % MAX_FRAMES_IN_FLIGHT; +``` + +`queue_present` 함수도 `Result`를 반환합니다. `Ok(true)`는 준최적 상태를, `Err(vk::Result::ERROR_OUT_OF_DATE_KHR)`는 오래된 상태를 의미합니다. 두 경우 모두 최상의 결과를 위해 스왑 체인을 재구성합니다. + +## 데드락 해결하기 + +지금 코드를 실행하면 데드락이 발생할 수 있습니다. 코드를 디버깅해보면, 애플리케이션이 `wait_for_fences`에 도달한 후 더 이상 진행하지 못하고 멈추는 것을 발견할 수 있습니다. 이는 `acquire_next_image`가 `ERROR_OUT_OF_DATE_KHR`를 반환할 때, 우리가 스왑 체인을 재구성한 후 `draw_frame`에서 즉시 반환하기 때문입니다. 하지만 그 전에, 현재 프레임의 펜스는 대기 상태에 들어간 후 리셋되었습니다. 우리가 즉시 반환하므로 아무 작업도 제출되지 않고, 따라서 펜스는 절대 신호를 받지 못하게 되어 `wait_for_fences`가 영원히 멈추게 됩니다. + +다행히 간단한 해결책이 있습니다. 펜스를 리셋하는 것을, 우리가 확실히 작업을 제출할 것이라는 것을 안 이후로 미루는 것입니다. 이렇게 하면, 우리가 일찍 반환하더라도 펜스는 여전히 신호를 받은 상태(signaled)로 남아있어, 다음에 같은 펜스 객체를 사용할 때 `wait_for_fences`가 데드락을 일으키지 않을 것입니다. + +이제 `draw_frame` 함수의 시작 부분은 다음과 같아야 합니다: +```rust +unsafe { + self.device.wait_for_fences( + &[self.in_flight_fences[self.current_frame]], + true, + u64::MAX, + )?; +} + +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()), +}; + +// 작업을 제출할 때만 펜스를 리셋합니다. +unsafe { + self.device + .reset_fences(&[self.in_flight_fences[self.current_frame]])?; +} +``` +## 명시적으로 리사이즈 처리하기 + +많은 드라이버와 플랫폼이 윈도우 리사이즈 후 자동으로 `ERROR_OUT_OF_DATE_KHR`를 발생시키지만, 이것이 보장되지는 않습니다. 그래서 우리는 리사이즈를 명시적으로 처리하는 코드를 추가할 것입니다. 먼저 구조체에 리사이즈가 발생했음을 알리는 플래그 멤버 변수를 추가합니다: + +```rust +struct HelloTriangleApplication { + // ... + in_flight_fences: Vec, + framebuffer_resized: bool, + // ... +} +``` + +`draw_frame` 함수를 이 플래그도 확인하도록 수정해야 합니다. `queue_present` 후의 로직에 이 플래그를 함께 검사합니다. + +```rust +let present_result = unsafe { self.swapchain_loader.queue_present(self.present_queue, &present_info) }; + +let suboptimal = match present_result { + Ok(suboptimal) => suboptimal, + Err(vk::Result::ERROR_OUT_OF_DATE_KHR) => true, // 오래된 경우도 리사이즈로 취급 + Err(e) => return Err(e.into()), +}; + +if suboptimal || self.framebuffer_resized { + self.framebuffer_resized = false; + self.recreate_swapchain()?; +} +``` + +이제 실제로 리사이즈를 감지하기 위해 `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; + } + } +} +``` + +C++ 예제와 마찬가지로, `glfw`는 Rust의 멤버 함수를 직접 호출하는 방법을 모르기 때문에, `static` 함수(Rust에서는 `extern "C" fn`)를 사용합니다. `set_user_data`를 통해 `self`의 포인터를 윈도우에 저장하고, 콜백 함수 내에서 `glfwGetWindowUserPointer`로 다시 가져와 애플리케이션의 `framebuffer_resized` 플래그를 설정합니다. 이 과정은 `unsafe` 블록 안에서 수행되어야 합니다. + +이제 프로그램을 실행하고 윈도우 크기를 조절하여 프레임버퍼가 윈도우에 맞게 올바르게 리사이즈되는지 확인해 보세요. + +## 창 최소화 처리하기 + +스왑 체인이 오래될 수 있는 또 다른 경우는 특별한 종류의 윈도우 리사이즈인 창 최소화입니다. 이 경우는 프레임버퍼 크기가 `(0, 0)`이 되기 때문에 특별합니다. 이 튜토리얼에서는 윈도우가 다시 전경에 올 때까지 일시 중지하는 방식으로 이 문제를 처리할 것입니다. `recreate_swapchain` 함수를 다음과 같이 확장합니다: + +```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(); + } + + 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`가 기다릴 것이 없는 경우를 처리합니다. + +축하합니다, 여러분은 이제 최초의 잘 동작하는(well-behaved) Rust-Vulkan 프로그램을 완성했습니다! 다음 장에서는 버텍스 셰이더에 하드코딩된 정점들을 제거하고 실제로 정점 버퍼(vertex buffer)를 사용할 것입니다. + +[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-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..c67f026d --- /dev/null +++ b/ko-rust/04_Vertex_buffers/00_Vertex_input_description.md @@ -0,0 +1,161 @@ +## 소개 + +다음 몇 개의 챕터에서는 버텍스 셰이더에 하드코딩된 정점 데이터를 메모리의 버텍스 버퍼로 교체할 것입니다. 가장 쉬운 접근법으로 시작하여, CPU에서 볼 수 있는(visible) 버퍼를 만들고 메모리 복사를 통해 정점 데이터를 직접 GPU로 전달하는 방법을 알아볼 것입니다. 그 후에는 스테이징 버퍼(staging buffer)를 사용해 정점 데이터를 고성능 메모리로 복사하는 방법도 살펴볼 것입니다. + +## 버텍스 셰이더 + +먼저 버텍스 셰이더를 변경하여, 셰이더 코드 자체에 더 이상 정점 데이터를 포함하지 않도록 합니다. 버텍스 셰이더는 `in` 키워드를 사용하여 버텍스 버퍼로부터 입력을 받습니다. + +```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; +} +``` + +`inPosition`과 `inColor` 변수는 **정점 속성(vertex attributes)**입니다. 이들은 우리가 이전에 수동으로 위치와 색상을 지정했던 것처럼, 버텍스 버퍼에서 정점 단위로 지정되는 속성입니다. 버텍스 셰이더를 다시 컴파일하는 것을 잊지 마세요! + +`fragColor`와 마찬가지로, `layout(location = x)` 어노테이션은 나중에 참조할 수 있도록 입력에 인덱스를 할당합니다. 64비트 벡터인 `dvec3` 같은 일부 타입은 여러 개의 **슬롯(slot)**을 사용한다는 점을 아는 것이 중요합니다. 즉, 그 다음의 인덱스는 최소 2 이상 커야 합니다. + +```glsl +layout(location = 0) in dvec3 inPosition; +layout(location = 2) in vec3 inColor; +``` + +레이아웃 한정자(layout qualifier)에 대한 더 많은 정보는 [OpenGL 위키](https://www.khronos.org/opengl/wiki/Layout_Qualifier_(GLSL))에서 찾을 수 있습니다. + +## 정점 데이터 + +이제 정점 데이터를 셰이더 코드에서 우리 Rust 애플리케이션의 코드로 옮길 것입니다. 먼저 Rust에서 가장 널리 사용되는 선형대수 라이브러리인 `glam`을 `Cargo.toml`에 추가합니다. `glam`은 벡터나 행렬 같은 타입을 제공합니다. + +```toml +[dependencies] +glam = "0.24" +``` + +이제 버텍스 셰이더에서 사용할 두 속성을 포함하는 `Vertex` 구조체를 새로 정의합니다. `#[repr(C)]` 어트리뷰트는 Rust 컴파일러가 C와 호환되도록 필드 순서를 보장하게 만들어, 메모리 레이아웃을 예측 가능하게 합니다. 이는 Vulkan API와 상호작용할 때 필수적입니다. + +```rust +use glam::{Vec2, Vec3}; + +#[repr(C)] +#[derive(Clone, Debug, Copy)] +struct Vertex { + pos: Vec2, + color: Vec3, +} +``` + +`glam`은 셰이더 언어에서 사용되는 벡터 타입과 정확히 일치하는 Rust 타입을 편리하게 제공합니다. + +이제 `Vertex` 구조체를 사용하여 정점 데이터 배열을 정의합니다. 이전과 정확히 같은 위치와 색상 값을 사용하지만, 이제는 하나의 정점 배열로 결합되었습니다. 이를 **인터리빙(interleaving)** 정점 속성이라고 합니다. + +```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) }, +]; +``` + +## 바인딩 서술 (Binding descriptions) + +다음 단계는 이 데이터 포맷이 GPU 메모리에 업로드된 후, 버텍스 셰이더로 어떻게 전달될지를 Vulkan에게 알려주는 것입니다. 이를 위해 두 종류의 구조체를 설정해야 합니다. + +첫 번째 구조체는 `ash::vk::VertexInputBindingDescription`입니다. `Vertex` 구조체에 대한 `impl` 블록 내에 연관 함수(associated function)를 추가하여 이 구조체를 생성하도록 하겠습니다. + +```rust +use ash::vk; + +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, + } + } +} +``` + +정점 바인딩(vertex binding)은 정점들 전체에서 메모리로부터 데이터를 어떤 속도(rate)로 로드할지 서술합니다. 이는 데이터 항목 사이의 바이트 수와 각 정점 또는 각 인스턴스 이후에 다음 데이터 항목으로 이동할지 여부를 지정합니다. + +* `binding`: 바인딩 배열에서의 인덱스를 지정합니다. 우리는 하나의 바인딩만 사용하므로 `0`입니다. +* `stride`: 한 정점 데이터에서 다음 정점 데이터까지의 바이트 거리입니다. `std::mem::size_of`를 사용하여 `Vertex` 구조체의 크기를 가져옵니다. +* `input_rate`: 다음 값 중 하나를 가집니다. + * `vk::VertexInputRate::VERTEX`: 각 정점마다 다음 데이터 항목으로 이동합니다. + * `vk::VertexInputRate::INSTANCE`: 각 인스턴스마다 다음 데이터 항목으로 이동합니다. + +우리는 인스턴스 렌더링을 사용하지 않으므로, 정점별 데이터(`VERTEX`)를 사용합니다. + +## 속성 서술 (Attribute descriptions) + +정점 입력을 처리하는 방법을 서술하는 두 번째 구조체는 `ash::vk::VertexInputAttributeDescription`입니다. 이 구조체들을 채우기 위해 `Vertex`에 또 다른 연관 함수를 추가할 것입니다. + +필드 오프셋을 안전하게 계산하기 위해 `memoffset` 크레이트가 필요합니다. `Cargo.toml`에 추가해 주세요. + +```toml +[dependencies] +memoffset = "0.9" +``` + +```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, + }, + ] + } +} +``` + +속성 서술(attribute description)은 바인딩에서 제공된 정점 데이터 덩어리로부터 특정 속성을 어떻게 추출할지 설명합니다. 우리는 위치와 색상, 두 가지 속성을 가지고 있으므로 두 개의 속성 서술이 필요합니다. + +* `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!` 매크로를 사용하여 컴파일 타임에 안전하게 계산합니다. + +## 파이프라인 정점 입력 + +이제 `create_graphics_pipeline` 함수에서 이 정보들을 참조하여, 그래픽 파이프라인이 해당 포맷의 정점 데이터를 받도록 설정해야 합니다. `ash`가 제공하는 빌더(builder) 패턴을 사용하면 코드를 더 안전하고 간결하게 작성할 수 있습니다. + +```rust +let binding_descriptions = [Vertex::get_binding_description()]; +let attribute_descriptions = Vertex::get_attribute_descriptions(); + +let vertex_input_info = vk::PipelineVertexInputStateCreateInfo::builder() + .vertex_binding_descriptions(&binding_descriptions) + .vertex_attribute_descriptions(&attribute_descriptions); +``` +`ash`의 빌더는 슬라이스(`&[...]`)를 인자로 받으므로, C++ 버전처럼 개수(count)와 포인터(pointer)를 수동으로 설정할 필요가 없습니다. 빌더가 내부적으로 처리해주기 때문에 메모리 안전성이 높고 코드가 깔끔해집니다. 이 `vertex_input_info` 빌더를 파이프라인 생성 정보에 전달하면 됩니다. + +이제 파이프라인은 `VERTICES` 배열과 같은 포맷의 정점 데이터를 받아들여 우리 버텍스 셰이더로 전달할 준비가 되었습니다. 만약 지금 검증 레이어를 활성화한 상태로 프로그램을 실행하면, 바인딩에 연결된 버텍스 버퍼가 없다고 경고하는 것을 볼 수 있을 것입니다. 다음 단계는 버텍스 버퍼를 생성하고 정점 데이터를 그곳으로 옮겨 GPU가 접근할 수 있도록 하는 것입니다. + +[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 new file mode 100644 index 00000000..f31264d4 --- /dev/null +++ b/ko-rust/04_Vertex_buffers/01_Vertex_buffer_creation.md @@ -0,0 +1,269 @@ +## 소개 + +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(()) +} + +// ... + +fn create_vertex_buffer(&mut self) -> Result<(), Box> { + // 여기에 구현 + Ok(()) +} +``` + +버퍼를 생성하려면 `vk::BufferCreateInfo` 구조체를 채워야 합니다. Rust와 `ash`에서는 빌더(builder) 패턴을 사용하는 것이 일반적이며 훨씬 깔끔합니다. + +```rust +// in create_vertex_buffer +let buffer_size = (std::mem::size_of::() * self.vertices.len()) as vk::DeviceSize; + +let buffer_info = vk::BufferCreateInfo::builder() + .size(buffer_size) + .usage(vk::BufferUsageFlags::VERTEX_BUFFER) + .sharing_mode(vk::SharingMode::EXCLUSIVE); +``` + +`size` 필드는 버퍼의 크기를 바이트 단위로 지정합니다. Rust에서는 `std::mem::size_of`를 사용해 정점 구조체의 크기를 얻고, 이를 벡터의 길이와 곱하여 전체 크기를 계산할 수 있습니다. + +`usage` 필드는 버퍼의 데이터가 어떤 목적으로 사용될지를 나타냅니다. 우리의 사용 사례는 정점 버퍼이므로 `vk::BufferUsageFlags::VERTEX_BUFFER`를 사용합니다. + +`sharing_mode`는 스왑 체인의 이미지처럼 버퍼가 특정 큐 패밀리에 의해 배타적으로 소유될지, 아니면 여러 큐 간에 공유될지를 결정합니다. 우리는 그래픽스 큐에서만 사용할 것이므로 `vk::SharingMode::EXCLUSIVE`으로 설정합니다. + +이제 `device.create_buffer`를 사용해 버퍼를 생성할 수 있습니다. 버퍼 핸들과 할당된 메모리를 저장할 구조체 필드를 정의합니다. + +```rust +// in `HelloTriangleApplication` struct +struct HelloTriangleApplication { + // ... + vertex_buffer: vk::Buffer, + vertex_buffer_memory: vk::DeviceMemory, + // ... +} + +// in create_vertex_buffer +let vertex_buffer = unsafe { + self.device.create_buffer(&buffer_info, None)? +}; +self.vertex_buffer = vertex_buffer; +``` + +`ash`의 생성 및 파괴 함수들은 대부분 `unsafe`로 표시되어 있습니다. 이는 개발자가 Vulkan 객체의 생명주기를 올바르게 관리할 책임이 있다는 것을 명시하기 위함입니다. 예를 들어, `device`가 파괴되기 전에 이 버퍼를 반드시 파괴해야 합니다. + +버퍼는 프로그램이 끝날 때까지 사용되므로, Rust의 `Drop` 트레잇을 구현하여 정리하는 것이 가장 관용적입니다. + +```rust +// in `impl Drop for HelloTriangleApplication` +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + // ... 다른 객체들 정리 + self.device.destroy_buffer(self.vertex_buffer, None); + // ... + } + } +} +``` + +## 메모리 요구사항 + +버퍼는 생성되었지만 아직 실제 메모리가 할당되지 않았습니다. 버퍼에 메모리를 할당하는 첫 단계는 `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) +}; +``` + +`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; + } + } + + panic!("failed to find suitable memory type!"); +} +``` + +이 함수는 C++ 버전과 매우 유사하게 동작합니다. 먼저 `instance.get_physical_device_memory_properties`를 통해 물리 디바이스의 메모리 속성을 가져옵니다. + +루프 안에서는 `type_filter`를 통해 버퍼가 요구하는 메모리 타입인지 확인하고, `ash`의 비트플래그가 제공하는 `contains` 메서드를 사용해 우리가 필요로 하는 `properties`(예: CPU에서 접근 가능하고 일관성을 유지하는 속성)를 모두 포함하는지 검사합니다. + +## 메모리 할당 + +이제 올바른 메모리 타입을 결정할 수 있으므로, `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, +); + +let alloc_info = vk::MemoryAllocateInfo::builder() + .allocation_size(mem_requirements.size) + .memory_type_index(memory_type_index); + +let vertex_buffer_memory = unsafe { + self.device.allocate_memory(&alloc_info, None)? +}; +self.vertex_buffer_memory = vertex_buffer_memory; +``` + +`find_memory_type`을 호출하여 적절한 메모리 타입의 인덱스를 찾고, `allocation_size`와 함께 `MemoryAllocateInfo`를 설정합니다. `HOST_VISIBLE` 속성은 CPU가 이 메모리에 접근(map)할 수 있게 하고, `HOST_COHERENT` 속성은 CPU의 쓰기가 별도의 명시적 플러시(flush) 작업 없이 GPU에 보이도록 보장합니다. + +메모리 할당에 성공했다면, `bind_buffer_memory`를 호출하여 이 메모리를 버퍼와 연결합니다. + +```rust +// in create_vertex_buffer, after allocate_memory +unsafe { + self.device.bind_buffer_memory(self.vertex_buffer, self.vertex_buffer_memory, 0)?; +} +``` + +오프셋은 `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); + // ... + } + } +} +``` + +## 정점 버퍼 채우기 + +이제 정점 데이터를 버퍼에 복사할 시간입니다. `device.map_memory`를 사용하여 버퍼 메모리를 CPU가 접근할 수 있는 메모리 공간에 맵핑합니다. + +```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(), + )? +}; +``` + +이 함수는 맵핑된 메모리 영역을 가리키는 원시 포인터(`*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); +} +``` + +**참고**: `ash` 0.37.0부터는 `ash::util::Align`이라는 편리한 유틸리티가 제공됩니다. 이 유틸리티는 맵핑된 메모리의 정렬(alignment)을 처리하고 슬라이스에서 데이터를 안전하게 복사하는 작업을 도와줍니다. `memcpy`와 같은 저수준의 포인터 연산을 직접 사용하는 것보다 안전하고 편리합니다. + +데이터 복사가 끝나면 `unmap_memory`를 호출하여 맵핑을 해제합니다. + +```rust +// in create_vertex_buffer, after copying data +unsafe { + self.device.unmap_memory(self.vertex_buffer_memory); +} +``` + +우리는 `HOST_COHERENT` 메모리 타입을 사용했기 때문에, 드라이버가 CPU의 쓰기를 자동으로 GPU에 반영합니다. 이는 명시적으로 `flush`를 호출하는 것보다 약간의 성능 저하가 있을 수 있지만, 이 예제에서는 더 간단하고 충분합니다. + +## 정점 버퍼 바인딩 + +이제 남은 일은 렌더링 과정에서 정점 버퍼를 바인딩하는 것입니다. `record_command_buffer` 함수를 수정합니다. + +```rust +// in record_command_buffer +unsafe { + self.device.cmd_bind_pipeline( + command_buffer, + vk::PipelineBindPoint::GRAPHICS, + self.graphics_pipeline, + ); + + let vertex_buffers = [self.vertex_buffer]; + let offsets = [0]; + self.device.cmd_bind_vertex_buffers(command_buffer, 0, &vertex_buffers, &offsets); + + self.device.cmd_draw(command_buffer, self.vertices.len() as u32, 1, 0, 0); +} +``` + +`cmd_bind_vertex_buffers` 함수는 정점 버퍼를 특정 바인딩 위치에 연결합니다. Rust에서는 배열 슬라이스(`&[...]`)를 사용하여 버퍼와 오프셋을 전달합니다. 또한, `cmd_draw` 호출에서 하드코딩된 정점 수 `3` 대신 `self.vertices.len()`을 사용하여 버퍼에 있는 실제 정점 수를 전달하도록 수정합니다. + +이제 프로그램을 실행하면 익숙한 삼각형이 다시 나타날 것입니다. + +![](/images/triangle.png) + +맨 위 정점의 색을 흰색으로 바꾸려면 `vertices` 배열을 수정해 보세요. + +```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] }, +]; +``` + +프로그램을 다시 실행하면 다음과 같은 결과를 볼 수 있습니다. + +![](/images/triangle_white.png) + +다음 장에서는 더 나은 성능을 제공하지만 약간의 추가 작업이 필요한, 정점 데이터를 정점 버퍼로 복사하는 다른 방법을 살펴보겠습니다. \ 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 new file mode 100644 index 00000000..b96617e7 --- /dev/null +++ b/ko-rust/04_Vertex_buffers/02_Staging_buffer.md @@ -0,0 +1,294 @@ +## 소개 + +지금 우리가 사용하는 정점 버퍼는 올바르게 작동하지만, 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에서는 출력 매개변수 대신 튜플을 반환하는 것이 일반적입니다. + +```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); + + let buffer = unsafe { + self.device + .create_buffer(&buffer_info, None) + .expect("failed to create buffer!") + }; + + 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!"); + } + + (buffer, buffer_memory) +} +``` + +다양한 종류의 버퍼를 생성하는 데 이 함수를 사용할 수 있도록 버퍼 크기, 메모리 속성, 사용 목적을 매개변수로 받습니다. 함수는 생성된 버퍼와 메모리 핸들을 튜플로 반환합니다. + +이제 `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); + } +} +``` + +프로그램을 실행하여 정점 버퍼가 여전히 제대로 작동하는지 확인하세요. + +## 스테이징 버퍼 사용하기 + +이제 `create_vertex_buffer` 함수를 수정하여, 호스트 가시성 버퍼는 임시 버퍼로만 사용하고, 디바이스 로컬 버퍼를 실제 정점 버퍼로 사용하도록 변경하겠습니다. + +```rust +fn create_vertex_buffer(&mut self) { + let buffer_size = (std::mem::size_of::() * self.vertices.len()) as vk::DeviceSize; + + let (staging_buffer, staging_buffer_memory) = self.create_buffer( + buffer_size, + vk::BufferUsageFlags::TRANSFER_SRC, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + ); + + unsafe { + let data_ptr = self.device.map_memory( + staging_buffer_memory, + 0, + buffer_size, + vk::MemoryMapFlags::empty(), + ).expect("failed to map staging buffer memory!"); + + 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); + } + + 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; + + // ... 다음 단계에서 복사 로직 추가 ... +} +``` + +이제 정점 데이터를 매핑하고 복사하기 위해 `staging_buffer`와 `staging_buffer_memory`를 사용합니다. 이번 장에서는 두 개의 새로운 버퍼 사용 플래그를 사용합니다: + +* `vk::BufferUsageFlags::TRANSFER_SRC`: 버퍼가 메모리 전송 연산의 원본(source)으로 사용될 수 있습니다. +* `vk::BufferUsageFlags::TRANSFER_DST`: 버퍼가 메모리 전송 연산의 대상(destination)으로 사용될 수 있습니다. + +`vertex_buffer`는 이제 디바이스 로컬 메모리 타입으로 할당됩니다. 이는 일반적으로 우리가 `map_memory`를 사용할 수 없다는 것을 의미합니다. 하지만 `staging_buffer`에서 `vertex_buffer`로 데이터를 복사할 수는 있습니다. 이를 위해 `staging_buffer`에는 전송 원본 플래그를, `vertex_buffer`에는 정점 버퍼 사용 플래그와 함께 전송 대상 플래그를 지정해야 합니다. + +이제 한 버퍼의 내용을 다른 버퍼로 복사하는 `copy_buffer` 함수를 작성하겠습니다. + +```rust +fn copy_buffer(&self, src_buffer: vk::Buffer, dst_buffer: vk::Buffer, size: vk::DeviceSize) { + // ... +} +``` + +메모리 전송 연산은 그리기 명령과 마찬가지로 커맨드 버퍼를 사용하여 실행됩니다. 따라서 먼저 임시 커맨드 버퍼를 할당해야 합니다. 이런 종류의 단기 버퍼를 위해 `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] + }; +} +``` + +그리고 즉시 커맨드 버퍼 기록을 시작합니다. `ash`에서는 빌더 패턴을 사용합니다. + +```rust +let begin_info = vk::CommandBufferBeginInfo::builder() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); + +unsafe { + self.device + .begin_command_buffer(command_buffer, &begin_info) + .expect("failed to begin recording command buffer!"); +} +``` + +우리는 이 커맨드 버퍼를 한 번만 사용할 것이므로 `vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT` 플래그를 사용하여 드라이버에 최적화 힌트를 줍니다. + +```rust +let copy_region = vk::BufferCopy::builder() + .src_offset(0) // Optional + .dst_offset(0) // Optional + .size(size) + .build(); + +unsafe { + self.device + .cmd_copy_buffer(command_buffer, src_buffer, dst_buffer, &[copy_region]); +} +``` + +버퍼 내용은 `cmd_copy_buffer` 명령을 통해 전송됩니다. 이 함수는 원본과 대상 버퍼, 그리고 복사할 영역들을 담은 슬라이스(`&[vk::BufferCopy]`)를 인자로 받습니다. + +```rust +unsafe { + self.device + .end_command_buffer(command_buffer) + .expect("failed to record command buffer!"); +} +``` + +이 커맨드 버퍼는 복사 명령만 포함하므로, 바로 기록을 중단합니다. 이제 커맨드 버퍼를 실행하여 전송을 완료합니다. + +```rust +let submit_info = vk::SubmitInfo::builder() + .command_buffers(&[command_buffer]) + .build(); + +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!"); +} +``` + +`queue_wait_idle`을 사용하여 전송이 완료될 때까지 동기적으로 기다립니다. 펜스를 사용하면 여러 전송을 동시에 스케줄링하고 한 번에 기다리는 비동기적인 방식도 가능합니다. + +```rust +unsafe { + self.device + .free_command_buffers(self.command_pool, &[command_buffer]); +} +``` + +전송 작업에 사용된 커맨드 버퍼를 정리하는 것을 잊지 마세요. + +이제 `create_vertex_buffer` 함수에서 `copy_buffer`를 호출하여 정점 데이터를 디바이스 로컬 버퍼로 옮기고, 스테이징 버퍼를 정리합니다. + +```rust +fn create_vertex_buffer(&mut self) { + let buffer_size = (std::mem::size_of::() * self.vertices.len()) as vk::DeviceSize; + + 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); + } + + + 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; + + self.copy_buffer(staging_buffer, self.vertex_buffer, buffer_size); + + unsafe { + self.device.destroy_buffer(staging_buffer, None); + self.device.free_memory(staging_buffer_memory, None); + } +} +``` + +프로그램을 실행하여 익숙한 삼각형이 다시 보이는지 확인하세요. 성능 향상이 지금 당장은 눈에 보이지 않을 수 있지만, 이제 정점 데이터는 고성능 메모리에서 로드되고 있습니다. 이는 앞으로 더 복잡한 지오메트리를 렌더링하기 시작할 때 중요해질 것입니다. + +## 결론 + +실제 애플리케이션에서는 모든 개별 버퍼에 대해 `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 new file mode 100644 index 00000000..34faf107 --- /dev/null +++ b/ko-rust/04_Vertex_buffers/03_Index_buffer.md @@ -0,0 +1,175 @@ +## 소개 + +실제 애플리케이션에서 렌더링할 3D 메시는 여러 삼각형 간에 정점을 공유하는 경우가 많습니다. 이는 사각형을 그리는 것처럼 간단한 작업에서도 이미 발생합니다. + +![](/images/vertex_vs_index.svg) + +사각형을 그리려면 두 개의 삼각형이 필요하며, 이는 6개의 정점으로 구성된 정점 버퍼가 필요하다는 것을 의미합니다. 문제는 두 정점의 데이터가 중복되어 50%의 중복이 발생한다는 것입니다. 더 복잡한 메시에서는 정점이 평균 3개의 삼각형에서 재사용되므로 이 문제는 더욱 심각해집니다. 이 문제에 대한 해결책은 *인덱스 버퍼(index buffer)*를 사용하는 것입니다. + +인덱스 버퍼는 본질적으로 정점 버퍼에 대한 포인터 배열입니다. 인덱스 버퍼를 사용하면 정점 데이터의 순서를 바꾸고, 여러 정점에 대해 기존 데이터를 재사용할 수 있습니다. 위 그림은 4개의 고유한 정점을 포함하는 정점 버퍼가 있을 때, 사각형을 위한 인덱스 버퍼가 어떻게 보일지를 보여줍니다. 처음 세 개의 인덱스는 오른쪽 위 삼각형을 정의하고, 마지막 세 개의 인덱스는 왼쪽 아래 삼각형의 정점을 정의합니다. + +## 인덱스 버퍼 생성 + +이번 장에서는 정점 데이터를 수정하고 인덱스 데이터를 추가하여 그림과 같은 사각형을 그려보겠습니다. 네 개의 모서리를 나타내도록 정점 데이터를 수정합니다. Rust에서는 상수로 정의하는 것이 일반적입니다. + +```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] }, +]; +``` + +왼쪽 아래 모서리는 빨간색, 오른쪽 아래는 녹색, 오른쪽 위는 파란색, 왼쪽 위는 흰색입니다. `INDICES`라는 새 상수를 추가하여 인덱스 버퍼의 내용을 나타냅니다. 이 배열은 그림의 인덱스와 일치시켜 오른쪽 위 삼각형과 왼쪽 아래 삼각형을 그려야 합니다. + +```rust +const INDICES: [u16; 6] = [0, 1, 2, 2, 3, 0]; +``` + +`VERTICES` 슬라이스의 길이에 따라 인덱스 버퍼에 `u16` 또는 `u32`를 사용할 수 있습니다. 65535개 미만의 고유 정점을 사용하므로 지금은 `u16`를 사용하겠습니다. + +정점 데이터와 마찬가지로, 인덱스도 GPU가 접근할 수 있도록 `ash::vk::Buffer`에 업로드해야 합니다. 애플리케이션의 주 구조체에 인덱스 버퍼 리소스를 저장할 두 개의 새로운 필드를 정의합니다. + +```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, + // ... +} +``` + +이제 추가할 `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(()) + } +} +``` + +주목할 만한 차이점은 두 가지뿐입니다. `buffer_size`는 이제 인덱스 수에 인덱스 타입(`u16` 또는 `u32`)의 크기를 곱한 값과 같습니다. `index_buffer`의 `usage`는 `ash::vk::BufferUsageFlags::VERTEX_BUFFER` 대신 `ash::vk::BufferUsageFlags::INDEX_BUFFER`이어야 하는데, 이는 타당한 설정입니다. 그 외의 과정은 `create_buffer`와 `copy_buffer` 헬퍼 함수를 재사용하여 정확히 동일하게 진행됩니다. 스테이징 버퍼를 만들어 `INDICES` 배열의 내용을 복사하고, 그 내용을 최종 장치 로컬 인덱스 버퍼로 복사합니다. + +인덱스 버퍼는 정점 버퍼와 마찬가지로 프로그램이 끝날 때 정리해야 합니다. Rust에서는 `Drop` 트레잇을 구현하여 이 작업을 자동으로 처리하는 것이 가장 이상적입니다. + +```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); + + self.device.destroy_buffer(self.vertex_buffer, None); + self.device.free_memory(self.vertex_buffer_memory, None); + // ... + } + } +} +``` + +## 인덱스 버퍼 사용하기 + +그리기에 인덱스 버퍼를 사용하는 것은 `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, + ); + // ... +} +``` + +`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 + ); + // ... +} +``` + +이 함수 호출은 `cmd_draw`와 매우 유사합니다. 첫 번째와 두 번째 매개변수는 인덱스의 수와 인스턴스의 수를 지정합니다. 인스턴싱은 사용하지 않으므로 인스턴스는 `1`로 지정합니다. 인덱스의 수(`INDICES.len() as u32`)는 정점 셰이더로 전달될 정점의 수를 나타냅니다. 다음 매개변수는 인덱스 버퍼로의 오프셋을 지정하며, `first_index` 값으로 `1`을 사용하면 그래픽 카드가 두 번째 인덱스부터 읽기 시작합니다. 그 다음 매개변수인 `vertex_offset`은 인덱스 버퍼의 인덱스에 더할 오프셋을 지정합니다. 마지막 매개변수는 인스턴싱을 위한 오프셋을 지정하는데, 우리는 사용하지 않습니다. + +이제 프로그램을 실행하면 다음과 같은 결과가 나타나야 합니다. + +![](/images/indexed_rectangle.png) + +이제 인덱스 버퍼를 사용하여 정점을 재사용함으로써 메모리를 절약하는 방법을 알게 되었습니다. 이는 나중에 복잡한 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 new file mode 100644 index 00000000..5176fe09 --- /dev/null +++ b/ko-rust/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md @@ -0,0 +1,350 @@ +## 서론 + +이제 우리는 각 정점마다 임의의 어트리뷰트를 버텍스 셰이더로 전달할 수 있게 되었습니다. 하지만 전역 변수는 어떨까요? 이번 장부터는 3D 그래픽스로 넘어가게 되는데, 여기에는 모델-뷰-프로젝션(MVP) 행렬이 필요합니다. 이 행렬을 정점 데이터에 포함시킬 수도 있겠지만, 이는 메모리 낭비이며 변환이 변경될 때마다 정점 버퍼를 업데이트해야 합니다. 변환은 매 프레임마다 쉽게 바뀔 수 있습니다. + +Vulkan에서 이 문제를 해결하는 올바른 방법은 *리소스 디스크립터(resource descriptor)*를 사용하는 것입니다. 디스크립터는 셰이더가 버퍼나 이미지 같은 리소스에 자유롭게 접근할 수 있게 해주는 방법입니다. 우리는 변환 행렬들을 담고 있는 버퍼를 설정하고, 버텍스 셰이더가 디스크립터를 통해 이들에 접근하도록 할 것입니다. 디스크립터 사용은 세 부분으로 구성됩니다: + +* 파이프라인 생성 시 디스크립터 셋 레이아웃 명시 +* 디스크립터 풀에서 디스크립터 셋 할당 +* 렌더링 시 디스크립터 셋 바인딩 + +*디스크립터 셋 레이아웃*은 렌더 패스가 접근할 어태치먼트의 타입을 명시하는 것과 유사하게, 파이프라인이 접근할 리소스의 타입을 명시합니다. *디스크립터 셋*은 프레임버퍼가 렌더 패스 어태치먼트에 바인딩할 실제 이미지 뷰를 지정하는 것과 유사하게, 디스크립터에 바인딩될 실제 버퍼나 이미지 리소스를 지정합니다. 그 후 디스크립터 셋은 정점 버퍼나 프레임버퍼처럼 드로우 커맨드를 위해 바인딩됩니다. + +많은 종류의 디스크립터가 있지만, 이번 장에서는 uniform buffer object (UBO)를 다룰 것입니다. 다른 종류의 디스크립터는 다음 장들에서 살펴보겠지만, 기본적인 과정은 동일합니다. 버텍스 셰이더가 사용하길 원하는 데이터를 다음과 같은 Rust 구조체에 담는다고 가정해 봅시다: + +```rust +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct UniformBufferObject { + model: Matrix4, + view: Matrix4, + proj: Matrix4, +} +``` + +그러면 우리는 이 데이터를 `vk::Buffer`로 복사하고, 버텍스 셰이더에서 uniform buffer object 디스크립터를 통해 다음과 같이 접근할 수 있습니다: + +```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; +} +``` + +우리는 이전 장의 사각형을 3D 공간에서 회전시키기 위해 매 프레임마다 모델, 뷰, 프로젝션 행렬을 업데이트할 것입니다. + +## 버텍스 셰이더 + +위에서 명시된 것처럼 uniform buffer object를 포함하도록 버텍스 셰이더를 수정하세요. MVP 변환에 대해서는 이미 익숙하다고 가정하겠습니다. 만약 익숙하지 않다면, 첫 장에서 언급된 [참고 자료](https://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/)를 확인하세요. + +```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; +} +``` + +`uniform`, `in`, `out` 선언 순서는 중요하지 않습니다. `binding` 지시어는 어트리뷰트의 `location` 지시어와 유사합니다. 우리는 디스크립터 셋 레이아웃에서 이 바인딩을 참조할 것입니다. `gl_Position`을 계산하는 줄은 변환 행렬들을 사용하여 최종 클립 좌표(clip coordinates)를 계산하도록 변경되었습니다. 2D 삼각형과 달리, 클립 좌표의 마지막 성분은 `1`이 아닐 수 있으며, 이는 화면의 최종 정규화된 장치 좌표(normalized device coordinates)로 변환될 때 나눗셈을 유발합니다. 이것은 원근 투영에서 *원근 분할(perspective division)*로 사용되며, 가까운 물체가 멀리 있는 물체보다 더 크게 보이게 하는 데 필수적입니다. + +## 디스크립터 셋 레이아웃 + +다음 단계는 Rust 측에서 UBO를 정의하고, 버텍스 셰이더의 이 디스크립터에 대해 Vulkan에 알려주는 것입니다. 행렬 연산을 위해 `cgmath` crate를 사용합니다. + +```rust +use cgmath::{Matrix4, Point3, Vector3}; + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct UniformBufferObject { + model: Matrix4, + view: Matrix4, + proj: Matrix4, +} +``` + +`#[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(); + // ... + } + + // ... + + fn create_descriptor_set_layout(&mut self) { + // ... + } +} +``` + +먼저, `HelloTriangleApplication` 구조체에 `descriptor_set_layout` 멤버를 추가합니다. + +```rust +struct HelloTriangleApplication { + // ... + pipeline_layout: vk::PipelineLayout, + descriptor_set_layout: vk::DescriptorSetLayout, + // ... +} +``` + +이제 `create_descriptor_set_layout` 함수를 구현합니다. 각 바인딩은 `vk::DescriptorSetLayoutBinding` 구조체로 기술됩니다. `ash`의 빌더 패턴을 사용하면 코드를 더 명확하게 만들 수 있습니다. + +```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(); + + let bindings = [ubo_layout_binding]; + let layout_info = vk::DescriptorSetLayoutCreateInfo::builder() + .bindings(&bindings); + + self.descriptor_set_layout = unsafe { + self.device + .create_descriptor_set_layout(&layout_info, None) + } + .expect("Failed to create descriptor set layout!"); +} +``` + +`binding(0)`은 셰이더의 `layout(binding = 0)`에 해당합니다. `descriptor_type`은 uniform 버퍼임을 명시합니다. `descriptor_count`는 배열의 크기이며, 우리는 단일 UBO만 사용하므로 1입니다. `stage_flags`는 이 디스크립터가 버텍스 셰이더에서 사용됨을 나타냅니다. + +그런 다음, 파이프라인 레이아웃을 생성할 때 이 디스크립터 셋 레이아웃을 참조하도록 `create_pipeline_layout` (혹은 `create_graphics_pipeline` 내의 관련 부분)을 수정해야 합니다. + +```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!"); +``` + +`set_layouts` 슬라이스를 통해 하나 이상의 디스크립터 셋 레이아웃을 파이프라인에 바인딩할 수 있습니다. + +마지막으로, 애플리케이션 종료 시 리소스를 정리합니다. + +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + // ... + self.device.destroy_descriptor_set_layout(self.descriptor_set_layout, None); + // ... + } + } +} +``` + +## Uniform 버퍼 + +다음으로, 셰이더를 위한 UBO 데이터를 담을 버퍼를 생성해야 합니다. 매 프레임 UBO를 업데이트할 것이므로 스테이징 버퍼는 불필요한 오버헤드를 유발할 수 있습니다. + +여러 프레임이 동시에 처리 중(in-flight)일 수 있으므로, 이전 프레임이 사용 중인 버퍼를 덮어쓰는 것을 방지하기 위해 각 프레임마다 별도의 uniform 버퍼를 가져야 합니다. `HelloTriangleApplication` 구조체에 새로운 멤버들을 추가합니다. + +```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>, + // ... +} +``` + +`create_uniform_buffers` 함수를 만들어 `create_index_buffer` 다음에 호출되도록 합니다. + +```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!"); + } + } +} +``` +버퍼를 생성한 직후 `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); + // ... + } + } +} +``` + +## Uniform 데이터 업데이트 + +`draw_frame` 함수 내에서 커맨드 버퍼를 제출하기 전에 uniform 데이터를 업데이트하는 `update_uniform_buffer` 함수를 호출합니다. + +```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) { + // ... + } +} +``` + +시간에 따라 회전하는 애니메이션을 구현하기 위해 `cgmath`와 `std::time`을 사용합니다. `Cargo.toml`에 `cgmath`와 `chrono` (또는 표준 라이브러리의 `std::time`)가 포함되어 있는지 확인하세요. + +`HelloTriangleApplication`에 시간을 추적할 필드를 추가합니다. + +```rust +struct HelloTriangleApplication { + // ... + start_time: std::time::Instant, + // ... +} +``` + +`new` 함수에서 `start_time`을 초기화합니다. + +```rust +impl HelloTriangleApplication { + pub fn new(window: &winit::window::Window) -> Self { + // ... + let mut app = Self { + // ... + start_time: std::time::Instant::now(), + }; + // ... + } +} +``` + +이제 `update_uniform_buffer` 함수를 구현합니다. + +```rust +use cgmath::{Deg, Matrix4, Point3, Rad, Vector3}; + +// ... + +fn update_uniform_buffer(&mut self, current_image_index: usize) { + let time = self.start_time.elapsed().as_secs_f32(); + + let model = Matrix4::from_axis_angle( + Vector3::new(0.0, 0.0, 1.0), + Deg(90.0 * time), + ); + + 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), + ); + + let mut proj = cgmath::perspective( + Deg(45.0), + self.swapchain_extent.width as f32 / self.swapchain_extent.height as f32, + 0.1, + 10.0, + ); + + // cgmath는 OpenGL의 클립 좌표계를 기준으로 설계되었습니다. + // Vulkan은 Y좌표가 반대이므로, 프로젝션 행렬의 Y 스케일링 요소의 부호를 뒤집어줍니다. + proj[1][1] *= -1.0; + + let ubo = UniformBufferObject { model, view, proj }; + + // 매핑된 메모리에 데이터 복사 + 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` 블록 내에서 수행되어야 합니다. + +자주 변경되는 작은 데이터를 셰이더에 전달하는 데 UBO를 사용하는 것이 항상 가장 효율적인 방법은 아닙니다. 더 효율적인 방법으로는 *푸시 상수(push constants)*가 있으며, 이는 추후 튜토리얼에서 다룰 수 있습니다. + +다음 장에서는 디스크립터 셋에 대해 알아보고, 우리가 생성한 `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 new file mode 100644 index 00000000..632d992b --- /dev/null +++ b/ko-rust/05_Uniform_buffers/01_Descriptor_pool_and_sets.md @@ -0,0 +1,335 @@ +## 소개 + +이전 장에서 다룬 디스크립터 셋 레이아웃은 바인딩할 수 있는 디스크립터의 유형을 설명합니다. 이번 장에서는 각 `vk::Buffer` 리소스마다 디스크립터 셋을 만들어서 유니폼 버퍼 디스크립터에 바인딩할 것입니다. + +## 디스크립터 풀 (Descriptor Pool) + +디스크립터 셋은 직접 생성할 수 없으며, 커맨드 버퍼처럼 풀(pool)에서 할당해야 합니다. 디스크립터 셋을 위한 이러한 풀은 *디스크립터 풀(descriptor pool)*이라고 불립니다. 이를 설정하기 위해 새로운 함수 `create_descriptor_pool`을 작성하겠습니다. + +```rust +impl VulkanApp { + fn init_vulkan(&mut self) -> Result<()> { + ... + self.create_uniform_buffers()?; + self.create_descriptor_pool()?; + ... + } + + ... + + fn create_descriptor_pool(&mut self) -> Result<()> { + // 이 함수 내용을 채워나갑니다. + Ok(()) + } +} +``` + +먼저 `vk::DescriptorPoolSize` 구조체를 사용해 우리 디스크립터 셋이 어떤 유형의 디스크립터를 얼마나 포함할지 기술해야 합니다. + +```rust +let pool_size = vk::DescriptorPoolSize { + ty: vk::DescriptorType::UNIFORM_BUFFER, + descriptor_count: MAX_FRAMES_IN_FLIGHT as u32, +}; +``` + +우리는 프레임마다 하나씩 이 디스크립터를 할당할 것입니다. 이 풀 크기 구조체는 메인 `vk::DescriptorPoolCreateInfo`에서 참조됩니다. `ash`의 빌더 패턴을 사용하면 코드가 더 깔끔해집니다. + +```rust +let pool_sizes = [pool_size]; +let pool_info = vk::DescriptorPoolCreateInfo::builder() + .pool_sizes(&pool_sizes) + // ... +``` + +개별 디스크립터의 최대 개수 외에도, 할당될 수 있는 디스크립터 셋의 최대 개수도 지정해야 합니다. + +```rust + .max_sets(MAX_FRAMES_IN_FLIGHT as u32); +``` + +이 구조체는 커맨드 풀과 유사한 선택적 플래그 `vk::DescriptorPoolCreateFlags::FREE_DESCRIPTOR_SET`를 가집니다. 이 플래그는 개별 디스크립터 셋을 해제할 수 있는지 여부를 결정합니다. 우리는 디스크립터 셋을 생성한 후에는 수정하지 않을 것이므로 이 플래그는 필요 없습니다. `flags`는 기본값으로 둘 수 있습니다. + +애플리케이션 구조체에 디스크립터 풀 핸들을 저장할 필드를 추가하고 `create_descriptor_pool`을 호출하여 생성합니다. + +```rust +// 구조체 정의 +struct VulkanApp { + ... + descriptor_pool: vk::DescriptorPool, + ... +} + +// create_descriptor_pool 함수 내부 +let pool_info = vk::DescriptorPoolCreateInfo::builder() + .pool_sizes(&pool_sizes) + .max_sets(MAX_FRAMES_IN_FLIGHT as u32); + +self.descriptor_pool = unsafe { + self.device + .create_descriptor_pool(&pool_info, None) +}?; +``` + +## 디스크립터 셋 (Descriptor Set) + +이제 디스크립터 셋 자체를 할당할 수 있습니다. 이를 위해 `create_descriptor_sets` 함수를 추가합시다. + +```rust +// init_vulkan 함수 내부 +... +self.create_descriptor_pool()?; +self.create_descriptor_sets()?; +... + +// VulkanApp impl 블록 내부 +fn create_descriptor_sets(&mut self) -> Result<()> { + // 이 함수 내용을 채워나갑니다. + Ok(()) +} +``` + +`vk::DescriptorSetAllocateInfo` 구조체로 디스크립터 셋 할당을 기술합니다. 할당할 디스크립터 풀, 할당할 디스크립터 셋의 개수, 그리고 기반으로 할 디스크립터 셋 레이아웃을 지정해야 합니다. + +```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); +``` + +우리의 경우, 각 프레임마다 하나의 디스크립터 셋을 생성하며, 모두 동일한 레이아웃을 가집니다. `ash`를 사용하면 `allocate_descriptor_sets` 함수가 `Vec`을 반환하므로 미리 벡터 크기를 조절할 필요가 없습니다. + +구조체에 디스크립터 셋 핸들을 담을 필드를 추가하고 `allocate_descriptor_sets`로 할당합니다. + +```rust +// 구조체 정의 +struct VulkanApp { + ... + descriptor_sets: Vec, + ... +} + +// create_descriptor_sets 함수 내부 +self.descriptor_sets = unsafe { self.device.allocate_descriptor_sets(&alloc_info) }?; +``` + +디스크립터 풀이 파괴될 때 자동으로 해제되므로, 디스크립터 셋을 명시적으로 정리할 필요는 없습니다. `allocate_descriptor_sets` 호출은 각각 하나의 유니폼 버퍼 디스크립터를 가진 디스크립터 셋들을 할당할 것입니다. `cleanup` (또는 `Drop` 구현)에서는 디스크립터 풀만 파괴하면 됩니다. + +```rust +// cleanup 함수 또는 Drop 트레이트 구현 내부 +... +unsafe { + self.device.destroy_descriptor_pool(self.descriptor_pool, None); + self.device.destroy_descriptor_set_layout(self.descriptor_set_layout, None); +} +... +``` + +이제 디스크립터 셋은 할당되었지만, 그 안의 디스크립터들은 아직 설정이 필요합니다. 이제 모든 디스크립터를 채우기 위한 루프를 추가합니다. + +우리의 유니폼 버퍼 디스크립터처럼 버퍼를 참조하는 디스크립터는 `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, + }; +``` + +우리처럼 버퍼 전체를 덮어쓰는 경우, `range`에 `vk::WHOLE_SIZE` 값을 사용하는 것도 가능합니다. 디스크립터 설정은 `vk::WriteDescriptorSet` 구조체의 배열을 파라미터로 받는 `update_descriptor_sets` 함수를 사용하여 업데이트됩니다. `ash`의 빌더 패턴을 사용합시다. + +```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); +``` + +`ash`의 `update_descriptor_sets` 함수는 쓰기(write)와 복사(copy)를 위한 슬라이스를 받습니다. 우리는 쓰기만 할 것입니다. + +```rust + unsafe { + self.device.update_descriptor_sets(&[descriptor_write.build()], &[]); + } +} +``` + +`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, + ); +} +``` + +정점 및 인덱스 버퍼와 달리, 디스크립터 셋은 그래픽스 파이프라인에만 국한되지 않습니다. 따라서 디스크립터 셋을 그래픽스 파이프라인에 바인딩할지, 컴퓨트 파이프라인에 바인딩할지 지정해야 합니다. 다음 파라미터는 디스크립터가 기반으로 하는 레이아웃입니다. 그 다음 세 파라미터는 첫 번째 디스크립터 셋의 인덱스, 바인딩할 셋의 개수, 그리고 바인딩할 셋의 슬라이스를 지정합니다. 마지막 파라미터는 동적 디스크립터에 사용되는 오프셋 슬라이스이며, 이는 다음 장에서 살펴보겠습니다. + +지금 프로그램을 실행해보면 안타깝게도 아무것도 보이지 않을 수 있습니다. 문제는 투영 행렬에서 Y축을 뒤집었기 때문에, 정점들이 시계 방향 대신 반시계 방향으로 그려진다는 것입니다. 이로 인해 후면 컬링(backface culling)이 작동하여 지오메트리가 그려지지 않게 됩니다. `create_graphics_pipeline` 함수로 가서 래스터화 상태의 `front_face`를 수정하여 이를 바로잡습니다. + +```rust +// create_graphics_pipeline 함수 내부 +let rasterizer = vk::PipelineRasterizationStateCreateInfo::builder() + ... + .cull_mode(vk::CullModeFlags::BACK) + .front_face(vk::FrontFace::COUNTER_CLOCKWISE); +``` + +프로그램을 다시 실행하면 다음과 같은 화면을 볼 수 있습니다. + +![](/images/spinning_quad.png) + +투영 행렬이 이제 화면 비율을 보정하기 때문에 직사각형이 정사각형으로 변경되었습니다. `update_uniform_buffer`가 화면 크기 조정을 처리하므로 `recreate_swapchain`에서 디스크립터 셋을 다시 만들 필요는 없습니다. + +## 정렬 요구사항 (Alignment Requirements) + +지금까지 간과한 한 가지는 Rust 구조체의 데이터가 셰이더의 유니폼 정의와 정확히 어떻게 일치해야 하는가입니다. 예를 들어, 수학 라이브러리로 `glam`을 사용한다고 가정합시다. + +```rust +// #[repr(C)]는 필드 순서를 보장합니다. +#[repr(C)] +struct UniformBufferObject { + model: glam::Mat4, + view: glam::Mat4, + proj: glam::Mat4, +} + +// 셰이더 +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; +``` + +하지만 이게 전부가 아닙니다. 예를 들어, 구조체와 셰이더를 다음과 같이 수정해보세요. + +```rust +#[repr(C)] +struct UniformBufferObject { + foo: glam::Vec2, + model: glam::Mat4, + view: glam::Mat4, + proj: glam::Mat4, +} +``` + +셰이더와 프로그램을 다시 컴파일하고 실행하면, 다채로운 사각형이 사라질 것입니다! 이는 우리가 *정렬 요구사항(alignment requirements)*을 고려하지 않았기 때문입니다. + +Vulkan은 구조체의 데이터가 메모리에서 특정 방식으로 정렬되기를 기대합니다. 예를 들면 다음과 같습니다: + +* 스칼라는 N(32비트 부동소수점의 경우 4바이트)으로 정렬되어야 합니다. +* `vec2`는 2N(8바이트)으로 정렬되어야 합니다. +* `vec3` 또는 `vec4`는 4N(16바이트)으로 정렬되어야 합니다. +* 중첩 구조체는 멤버의 기본 정렬을 16의 배수로 올림한 값으로 정렬되어야 합니다. +* `mat4` 행렬은 `vec4`와 동일한 정렬을 가져야 합니다. + +`glam::Mat4`는 이미 16바이트 정렬이 되어 있어 원래 구조체는 문제가 없었습니다. 그러나 `glam::Vec2`는 8바이트 정렬을 가지므로, `model` 필드의 오프셋이 8이 되어 16의 배수가 아니게 됩니다. 이 문제를 해결하기 위해 Rust에서는 `#[repr(C, align(N))]` 속성을 사용할 수 있습니다. + +```rust +#[repr(C)] +struct UniformBufferObject { + foo: glam::Vec2, + #[repr(align(16))] + model: glam::Mat4, + view: glam::Mat4, + proj: glam::Mat4, +} +``` + +하지만 이 방법은 필드별로 적용하기 번거롭습니다. 더 나은 방법은 구조체 전체에 정렬을 적용하는 것입니다. + +```rust +#[repr(C, align(16))] +struct UniformBufferObject { + foo: glam::Vec2, + model: glam::Mat4, + view: glam::Mat4, + proj: glam::Mat4, +} +``` + +이 코드는 작동하지 않습니다. `foo` 필드 뒤에 `model` 필드가 올바르게 정렬되도록 컴파일러가 패딩을 추가해야 하는데, `#[repr(C)]`는 이를 보장하지 않을 수 있습니다. 가장 안전하고 명확한 방법은 정렬이 필요한 각 멤버에 명시적으로 `align`을 지정하는 대신, `glam`과 같은 라이브러리가 제공하는 이미 정렬된 타입을 사용하는 것입니다. 다행히 `glam::Mat4`, `glam::Vec4` 등은 기본적으로 16바이트 정렬이 되어 있습니다. 문제가 발생한 `Vec2` 같은 타입의 경우, 수동으로 패딩을 추가하거나 구조를 변경해야 할 수 있습니다. + +이러한 함정을 피하기 위해, 항상 정렬에 대해 명시적인 것이 좋습니다. 최종적으로 우리의 UBO 구조체는 다음과 같이 명시적으로 정렬을 보장하는 것이 가장 안전합니다. + +```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 { + v: glam::Vec2, // 8바이트 정렬 +} + +#[repr(C)] +struct UniformBufferObject { + f1: Foo, + f2: Foo, // 이 필드의 오프셋은 8이 되며, 16이어야 합니다. +} +``` +이런 경우, 명시적으로 정렬을 지정해야 합니다. +```rust +#[repr(C, align(16))] +struct Foo { + v: glam::Vec2, +} + +#[repr(C)] +struct UniformBufferObject { + f1: Foo, + f2: Foo, // 이제 f1과 f2 모두 16바이트 경계에 정렬됩니다. +} +``` + +정렬 오류는 디버깅하기 매우 까다로우므로, 유니폼 버퍼로 사용할 구조체는 `#[repr(C)]`와 함께 필요에 따라 `align` 속성을 명시하는 것이 좋습니다. + +우리의 원래 `UniformBufferObject`로 돌아가 `foo` 필드를 제거하고 셰이더를 다시 컴파일하는 것을 잊지 마세요. + +## 여러 개의 디스크립터 셋 (Multiple Descriptor Sets) + +일부 구조체와 함수 호출에서 암시되었듯이, 여러 디스크립터 셋을 동시에 바인딩하는 것도 가능합니다. 파이프라인 레이아웃을 생성할 때 각 디스크립터 셋에 대한 디스크립터 셋 레이아웃을 지정해야 합니다. 그러면 셰이더는 다음과 같이 특정 디스크립터 셋을 참조할 수 있습니다. + +```glsl +layout(set = 0, binding = 0) uniform UniformBufferObject { ... } +``` + +이 기능을 사용하면 객체별로 다른 디스크립터와 공유되는 디스크립터를 별도의 디스크립터 셋에 넣을 수 있습니다. 이 경우 드로우 콜 간에 대부분의 디스크립터를 다시 바인딩하는 것을 피할 수 있어 잠재적으로 더 효율적입니다. + +[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 new file mode 100644 index 00000000..63b94115 --- /dev/null +++ b/ko-rust/06_Texture_mapping/00_Images.md @@ -0,0 +1,477 @@ +## 서론 + +지금까지는 정점별(per-vertex) 색상을 사용해 지오메트리를 색칠해왔는데, 이는 다소 제한적인 방법입니다. 이번 장에서는 텍스처 매핑을 구현하여 지오메트리가 더 흥미롭게 보이도록 만들 것입니다. 이를 통해 다음 장에서는 기본적인 3D 모델을 로드하고 그릴 수도 있게 됩니다. + +애플리케이션에 텍스처를 추가하는 작업은 다음 단계를 포함합니다. + +* 디바이스 메모리를 기반으로 하는 이미지 객체 생성 +* 이미지 파일의 픽셀로 채우기 +* 이미지 샘플러 생성 +* 텍스처에서 색상을 샘플링하기 위한 결합 이미지 샘플러 디스크립터 추가 + +이전에도 이미지 객체를 다룬 적이 있지만, 그것들은 스왑체인 확장에 의해 자동으로 생성되었습니다. 이번에는 직접 하나를 만들어야 합니다. 이미지를 생성하고 데이터를 채우는 과정은 정점 버퍼 생성과 유사합니다. 먼저 스테이징 리소스를 생성하고 픽셀 데이터로 채운 다음, 렌더링에 사용할 최종 이미지 객체로 복사합니다. 이 목적으로 스테이징 이미지를 생성할 수도 있지만, Vulkan은 `VkBuffer`에서 이미지로 픽셀을 복사하는 것도 허용하며, 이 API는 [일부 하드웨어에서 실제로 더 빠릅니다](https://developer.nvidia.com/vulkan-memory-management). 우리는 먼저 이 버퍼를 생성하고 픽셀 값으로 채운 다음, 픽셀을 복사할 이미지를 생성할 것입니다. 이미지 생성은 버퍼 생성과 크게 다르지 않습니다. 이전에 보았듯이 메모리 요구사항을 쿼리하고, 디바이스 메모리를 할당하고, 바인딩하는 과정이 포함됩니다. + +하지만 이미지 작업 시에는 추가적으로 신경 써야 할 것이 있습니다. 이미지는 메모리에서 픽셀이 구성되는 방식에 영향을 미치는 다양한 *레이아웃(layouts)*을 가질 수 있습니다. 그래픽 하드웨어의 작동 방식 때문에, 단순히 픽셀을 행 단위로 저장하는 것이 최상의 성능으로 이어지지 않을 수 있습니다. 이미지에 대한 어떠한 작업을 수행할 때든, 해당 작업에 최적화된 레이아웃을 가지고 있는지 확인해야 합니다. 렌더 패스를 지정할 때 이미 이러한 레이아웃 중 일부를 본 적이 있습니다: + +* `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`: 셰이더에서 샘플링하기에 최적화 + +이미지의 레이아웃을 전환하는 가장 일반적인 방법 중 하나는 *파이프라인 배리어(pipeline barrier)*입니다. 파이프라인 배리어는 주로 리소스 접근을 동기화하는 데 사용됩니다. 예를 들어, 이미지를 읽기 전에 쓰기가 완료되었는지 확인하는 것과 같지만, 레이아웃을 전환하는 데에도 사용할 수 있습니다. 이번 장에서는 파이프라인 배리어가 이 목적으로 어떻게 사용되는지 볼 것입니다. 배리어는 `vk::SharingMode::EXCLUSIVE`를 사용할 때 큐 패밀리 소유권을 이전하는 데에도 추가적으로 사용될 수 있습니다. + +## 이미지 라이브러리 + +이미지를 로드하기 위한 많은 라이브러리가 있으며, Rust에서는 `image` 크레이트가 가장 대중적이고 강력한 선택지입니다. `stb_image`와 마찬가지로 다양한 이미지 형식을 지원하며 사용하기 매우 쉽습니다. + +`Cargo.toml` 파일의 `[dependencies]` 섹션에 `image` 크레이트를 추가하세요. + +```toml +[dependencies] +ash = "0.37" +# ... other dependencies +image = "0.24" +``` + +## 이미지 로딩하기 + +이제 `image` 크레이트를 사용하여 텍스처를 로드할 수 있습니다. 먼저 애플리케이션 구조체에 관련 필드를 추가해야 합니다. + +```rust +// In VulkanApp struct +struct VulkanApp { + // ... + texture_image: vk::Image, + texture_image_memory: vk::DeviceMemory, + // ... +} +``` + +그리고 `init_vulkan` 함수에서 `create_texture_image` 함수를 호출하도록 순서를 조정합니다. 커맨드 버퍼를 사용하므로 커맨드 풀 생성 이후에 호출되어야 합니다. + +```rust +impl VulkanApp { + pub fn init_vulkan(&mut self) -> Result<(), Box> { + // ... + self.create_command_pool()?; + self.create_texture_image()?; + self.create_vertex_buffer()?; + // ... + Ok(()) + } + + // ... + + fn create_texture_image(&mut self) -> Result<(), Box> { + // ... + Ok(()) + } +} +``` + +이제 `create_texture_image` 함수에서 이미지를 로드합니다. `shaders` 디렉토리 옆에 `textures` 디렉토리를 만들고, 이 튜토리얼에서 사용할 512x512 픽셀 크기의 `texture.jpg` 이미지를 저장하세요. + +![](/images/texture.jpg) + +`image` 크레이트로 이미지를 로드하는 것은 매우 간단합니다. + +```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; + + // ... rest of the function + Ok(()) +} +``` + +`image::open`은 파일 경로에서 이미지를 로드합니다. `.to_rgba8()`는 이미지를 RGBA 형식으로 변환하여 알파 채널이 없는 이미지도 일관성 있게 처리합니다. `.into_raw()`는 이 이미지 데이터를 `Vec` 형태로 반환합니다. 이 벡터는 `tex_width * tex_height * 4` 바이트 크기를 가집니다. + +## 스테이징 버퍼 + +이제 호스트 가시성(host-visible) 메모리에 버퍼를 생성하여 `vkMapMemory`를 사용하고 픽셀 데이터를 복사할 수 있도록 합니다. + +```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, +)?; + +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); +} +``` + +`create_buffer` 헬퍼 함수를 사용하여 스테이징 버퍼를 생성합니다. 이 버퍼는 호스트에서 접근 가능해야 하고(`HOST_VISIBLE`), 전송 소스로 사용될 수 있어야 합니다(`TRANSFER_SRC`). `HOST_COHERENT` 플래그는 매핑된 메모리에 쓴 내용이 자동으로 디바이스에 보이도록 보장합니다. + +`map_memory`로 메모리 포인터를 얻은 후, `copy_from_nonoverlapping`을 사용하여 이미지 픽셀 데이터를 버퍼로 복사합니다. 작업이 끝나면 `unmap_memory`를 호출합니다. Rust의 `image_data`는 `Vec`이므로 범위를 벗어나면 자동으로 메모리가 해제됩니다. C++의 `stbi_image_free`처럼 수동으로 해제할 필요가 없습니다. + +## 텍스처 이미지 + +셰이더가 버퍼의 픽셀 값에 접근하도록 설정할 수도 있지만, Vulkan에서는 이미지 객체를 사용하는 것이 더 좋습니다. 이미지 객체는 2D 좌표를 사용할 수 있게 하여 색상을 더 쉽고 빠르게 가져올 수 있게 해줍니다. 이미지 객체 내의 픽셀은 텍셀(texel)이라고 하며, 지금부터 이 용어를 사용하겠습니다. + +`createTextureImage` 함수의 나머지 부분에서 텍스처 이미지를 생성합니다. + +```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, +)?; + +self.texture_image = texture_image; +self.texture_image_memory = texture_image_memory; +``` + +버퍼 생성 로직을 `create_buffer`로 리팩토링했듯이, 이미지 생성 로직도 `create_image`라는 헬퍼 함수로 추상화하는 것이 좋습니다. + +```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); + + let image = unsafe { self.device.create_image(&image_info, None)? }; + + let mem_requirements = unsafe { self.device.get_image_memory_requirements(image) }; + + 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 image_memory = unsafe { self.device.allocate_memory(&alloc_info, None)? }; + + unsafe { self.device.bind_image_memory(image, image_memory, 0)? }; + + Ok((image, image_memory)) + } +} +``` + +`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`는 셰이더에서 샘플링할 수 있음을 의미합니다. +* 나머지 필드는 버퍼 생성과 유사합니다. + +이미지를 생성한 후, `get_image_memory_requirements`로 메모리 요구사항을 얻고, 적절한 메모리 타입을 찾아 `allocate_memory`로 메모리를 할당한 뒤, `bind_image_memory`로 이미지와 메모리를 바인딩합니다. + +## 레이아웃 전환 + +이제부터 커맨드 버퍼를 기록하고 실행하는 작업이 반복되므로, 이 로직을 헬퍼 함수로 분리합시다. + +```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); + + let command_buffer = unsafe { self.device.allocate_command_buffers(&alloc_info)?[0] }; + + let begin_info = vk::CommandBufferBeginInfo::builder() + .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT); + + unsafe { self.device.begin_command_buffer(command_buffer, &begin_info)? }; + + Ok(command_buffer) + } + + fn end_single_time_commands(&self, command_buffer: vk::CommandBuffer) -> Result<(), Box> { + unsafe { + self.device.end_command_buffer(command_buffer)?; + + let submit_info = vk::SubmitInfo::builder() + .command_buffers(&[command_buffer]); + + self.device.queue_submit(self.graphics_queue, &[submit_info.build()], vk::Fence::null())?; + self.device.queue_wait_idle(self.graphics_queue)?; + + self.device.free_command_buffers(self.command_pool, &[command_buffer]); + } + Ok(()) + } +} +``` + +이 두 함수는 이전에 `copy_buffer`에서 사용했던 로직과 동일합니다. 일회성 커맨드 버퍼를 할당하고 시작하며, 제출 후 대기하고 해제합니다. 이제 `copy_buffer` 함수를 이 헬퍼들을 사용해 단순화할 수 있습니다. + +```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()?; + + let copy_region = vk::BufferCopy::builder().size(size); + unsafe { + self.device.cmd_copy_buffer(command_buffer, src_buffer, dst_buffer, &[copy_region.build()]); + } + + self.end_single_time_commands(command_buffer)?; + + Ok(()) +} +``` + +이제 이미지 레이아웃 전환을 위한 함수를 만듭니다. 레이아웃 전환에는 *이미지 메모리 배리어*를 사용한 파이프라인 배리어가 주로 사용됩니다. + +```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()], + ); + } + + self.end_single_time_commands(command_buffer)?; + + Ok(()) +} +``` + +배리어는 리소스 접근을 동기화하는 데 사용됩니다. +* `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()], + ); + } + + self.end_single_time_commands(command_buffer)?; + + Ok(()) +} +``` +`vkCmdCopyBufferToImage`는 버퍼의 어느 영역을 이미지의 어느 영역으로 복사할지 `VkBufferImageCopy` 구조체로 지정받습니다. 여기서 이미지는 복사 대상에 최적화된 레이아웃인 `TRANSFER_DST_OPTIMAL` 상태여야 합니다. + +## 텍스처 이미지 준비하기 + +이제 모든 헬퍼 함수를 사용하여 `create_texture_image` 함수를 완성할 수 있습니다. + +```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; + + let (staging_buffer, staging_buffer_memory) = self.create_buffer( + image_size, + vk::BufferUsageFlags::TRANSFER_SRC, + vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT, + )?; + + // 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); + } + + 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); + } + + Ok(()) +} +``` + +전체 프로세스는 다음과 같습니다: +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); + // ... + } + } +} +``` + +이제 이미지가 텍스처를 포함하게 되었지만, 아직 그래픽 파이프라인에서 접근할 방법이 없습니다. 다음 장에서 이 부분을 다루겠습니다. \ 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 new file mode 100644 index 00000000..38024473 --- /dev/null +++ b/ko-rust/06_Texture_mapping/01_Image_view_and_sampler.md @@ -0,0 +1,365 @@ +이번 장에서는 그래픽스 파이프라인이 이미지를 샘플링하는 데 필요한 두 가지 리소스를 더 만들 것입니다. 첫 번째 리소스는 스왑 체인 이미지에서 이미 다루었던 것이지만, 두 번째 리소스는 새로운 것으로 셰이더가 이미지에서 텍셀(texel)을 어떻게 읽을지와 관련이 있습니다. + +## 텍스처 이미지 뷰 + +우리는 이전에 스왑 체인 이미지와 프레임버퍼에서 이미지가 직접 접근되는 대신 이미지 뷰를 통해 접근된다는 것을 보았습니다. 텍스처 이미지에 대해서도 이러한 이미지 뷰를 만들어야 합니다. + +텍스처 이미지의 `ash::vk::ImageView`를 저장할 구조체 필드를 추가하고, 이를 생성할 `create_texture_image_view` 메서드를 새로 만듭니다. + +```rust +struct HelloTriangleApplication { + // ... + texture_image: vk::Image, + texture_image_memory: vk::DeviceMemory, + texture_image_view: vk::ImageView, + // ... +} + +impl HelloTriangleApplication { + pub fn new(window: &Window) -> Self { + // ... + } + + fn init_vulkan(&mut self, window: &Window) { + // ... + self.create_texture_image(); + self.create_texture_image_view(); + self.create_vertex_buffer(); + // ... + } + + // ... + + fn create_texture_image_view(&mut self) { + // ... + } +} +``` + +이 메서드의 코드는 `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!") + } + } + // ... +} +``` + +이제 `create_texture_image_view` 메서드는 다음과 같이 단순화할 수 있습니다. + +```rust +fn create_texture_image_view(&mut self) { + self.texture_image_view = + self.create_image_view(self.texture_image, vk::Format::R8G8B8A8_SRGB); +} +``` + +그리고 `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(); +} +``` + +프로그램이 끝날 때, 이미지 자체를 파괴하기 직전에 이미지 뷰를 파괴하도록 `cleanup` 메서드를 수정해야 합니다. + +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + self.cleanup_swapchain(); + + 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); + // ... + } + } +} +``` + +## 샘플러 + +셰이더가 이미지에서 직접 텍셀을 읽는 것도 가능하지만, 이미지가 텍스처로 사용될 때는 흔한 방식이 아닙니다. 텍스처는 보통 샘플러를 통해 접근되며, 샘플러는 최종적으로 검색될 색상을 계산하기 위해 필터링과 변환을 적용합니다. + +이러한 필터들은 오버샘플링(oversampling) 같은 문제를 해결하는 데 유용합니다. 텍셀보다 더 많은 프래그먼트가 있는 지오메트리에 텍스처가 매핑되는 경우를 생각해보세요. 만약 각 프래그먼트의 텍스처 좌표에 가장 가까운 텍셀을 단순히 가져온다면, 아래 첫 번째 이미지와 같은 결과를 얻게 될 것입니다. + +![](/images/texture_filtering.png) + +만약 가장 가까운 4개의 텍셀을 선형 보간(linear interpolation)으로 혼합한다면, 오른쪽 이미지처럼 더 부드러운 결과를 얻을 수 있습니다. 물론 애플리케이션의 아트 스타일에 따라 왼쪽 스타일(마인크래프트처럼)이 더 적합할 수도 있지만, 일반적인 그래픽스 애플리케이션에서는 오른쪽 방식이 선호됩니다. 샘플러 객체는 텍스처에서 색상을 읽을 때 이 필터링을 자동으로 적용해줍니다. + +언더샘플링(undersampling)은 그 반대의 문제로, 프래그먼트보다 텍셀이 더 많은 경우입니다. 이는 체커보드 텍스처처럼 고주파 패턴을 예리한 각도에서 샘플링할 때 아티팩트를 유발합니다. + +![](/images/anisotropic_filtering.png) + +왼쪽 이미지에서 보듯이, 텍스처가 멀어질수록 흐릿한 덩어리로 변합니다. 이에 대한 해결책은 [비등방성 필터링(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)이며, 이 또한 샘플러에 의해 자동으로 적용될 수 있습니다. + +이러한 필터 외에도, 샘플러는 변환도 처리할 수 있습니다. 샘플러는 *주소 지정 모드(addressing mode)*를 통해 이미지 외부의 텍셀을 읽으려고 할 때 어떤 일이 일어날지를 결정합니다. 아래 이미지는 몇 가지 가능한 옵션을 보여줍니다. + +![](/images/texture_addressing.png) + +이제 이러한 샘플러 객체를 설정하기 위해 `create_texture_sampler` 메서드를 만들 것입니다. 나중에 셰이더에서 이 샘플러를 사용해 텍스처로부터 색상을 읽게 됩니다. + +```rust +struct HelloTriangleApplication { + // ... + texture_image_view: vk::ImageView, + texture_sampler: vk::Sampler, + // ... +} + +// ... +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) { + // ... +} +``` + +샘플러는 `ash::vk::SamplerCreateInfo` 구조체를 통해 구성되며, 이 구조체는 샘플러가 적용해야 할 모든 필터와 변환을 명시합니다. Ash의 빌더 패턴을 사용하여 생성합니다. + +```rust +let sampler_info = vk::SamplerCreateInfo::builder() + .mag_filter(vk::Filter::LINEAR) + .min_filter(vk::Filter::LINEAR); +``` + +`mag_filter`와 `min_filter` 필드는 텍셀이 확대되거나 축소될 때 어떻게 보간할지를 지정합니다. 확대는 위에서 설명한 오버샘플링 문제와 관련이 있고, 축소는 언더샘플링 문제와 관련이 있습니다. 선택지는 `VK_FILTER_NEAREST`와 `VK_FILTER_LINEAR`이며, 이는 위 이미지에서 보여준 모드에 해당합니다. + +```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); +``` + +주소 지정 모드는 축별로 지정할 수 있습니다. 사용 가능한 값은 다음과 같습니다. 대부분은 위 이미지에서 시연되었습니다. 축이 X, Y, Z 대신 U, V, W로 불리는 점에 유의하세요. 이는 텍스처 공간 좌표의 관례입니다. + +* `vk::SamplerAddressMode::REPEAT`: 이미지 크기를 벗어날 때 텍스처를 반복합니다. +* `vk::SamplerAddressMode::MIRRORED_REPEAT`: 반복과 같지만, 크기를 벗어날 때 좌표를 반전시켜 이미지를 거울처럼 반사합니다. +* `vk::SamplerAddressMode::CLAMP_TO_EDGE`: 이미지 크기를 벗어나는 좌표에 대해 가장 가까운 가장자리의 색상을 사용합니다. +* `vk::SamplerAddressMode::MIRROR_CLAMP_TO_EDGE`: 가장자리 클램프와 비슷하지만, 가장 가까운 가장자리가 아닌 반대쪽 가장자리를 사용합니다. +* `vk::SamplerAddressMode::CLAMP_TO_BORDER`: 이미지 크기 밖을 샘플링할 때 지정된 단색을 반환합니다. + +이 튜토리얼에서는 이미지 외부를 샘플링하지 않을 것이므로 어떤 주소 지정 모드를 사용하든 큰 차이는 없습니다. 하지만 바닥이나 벽처럼 텍스처를 타일링하는 데 사용될 수 있기 때문에 반복 모드가 아마 가장 일반적일 것입니다. + +```rust +// ... + .anisotropy_enable(true) + .max_anisotropy(???) +// ... +``` + +이 두 필드는 비등방성 필터링을 사용할지 여부를 지정합니다. 성능이 우려되는 경우가 아니라면 사용하지 않을 이유가 없습니다. `max_anisotropy` 필드는 최종 색상을 계산하는 데 사용될 수 있는 텍셀 샘플의 양을 제한합니다. 값이 낮을수록 성능은 좋아지지만 결과물의 품질은 떨어집니다. 우리가 사용할 수 있는 값을 알아내려면, 다음과 같이 물리 장치의 속성을 가져와야 합니다. + +```rust +let properties = unsafe { self.instance.get_physical_device_properties(self.physical_device) }; +``` + +`ash::vk::PhysicalDeviceProperties` 구조체를 보면 `limits`라는 `ash::vk::PhysicalDeviceLimits` 타입의 필드가 있습니다. 이 구조체는 다시 `max_sampler_anisotropy`라는 필드를 가지고 있으며, 이것이 `max_anisotropy`에 지정할 수 있는 최대값입니다. 최고의 품질을 원한다면 이 값을 직접 사용하면 됩니다. + +```rust +// ... + .max_anisotropy(properties.limits.max_sampler_anisotropy) +// ... +``` + +`create_texture_sampler` 메서드 내에서 속성을 조회할 수 있습니다. + +```rust +// ... + .border_color(vk::BorderColor::INT_OPAQUE_BLACK) + .unnormalized_coordinates(false) +// ... +``` + +`border_color` 필드는 `clamp to border` 주소 지정 모드로 이미지 외부를 샘플링할 때 반환될 색상을 지정합니다. `unnormalized_coordinates` 필드는 텍셀 주소에 정규화된 좌표(`[0, 1)`)를 사용할지 여부를 지정합니다. 실제 애플리케이션에서는 거의 항상 정규화된 좌표를 사용합니다. + +```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!") + }; +} +``` + +샘플러는 어디에도 `vk::Image`를 참조하지 않는다는 점에 유의하세요. 샘플러는 텍스처에서 색상을 추출하는 인터페이스를 제공하는 별개의 객체입니다. + +프로그램이 종료될 때 샘플러를 파괴하도록 `drop` 구현을 수정합니다. + +```rust +impl Drop for HelloTriangleApplication { + fn drop(&mut self) { + unsafe { + self.cleanup_swapchain(); + + 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); + + // ... + } + } +} +``` + +## 비등방성 장치 기능 + +지금 프로그램을 실행하면 다음과 같은 검증 레이어 메시지를 볼 수 있습니다. + +![](/images/validation_layer_anisotropy.png) + +이는 비등방성 필터링이 사실 선택적(optional) 장치 기능이기 때문입니다. 이를 요청하도록 `create_logical_device` 메서드를 업데이트해야 합니다. + +```rust +// in create_logical_device +let mut features = vk::PhysicalDeviceFeatures::builder(); +features.sampler_anisotropy = vk::TRUE; + +// ... +let create_info = vk::DeviceCreateInfo::builder() + .queue_create_infos(&queue_create_infos) + .enabled_extension_names(&device_extensions_raw) + .enabled_features(&features); +``` + +그리고 최신 그래픽 카드가 이를 지원하지 않을 가능성은 매우 낮지만, `is_device_suitable` 함수를 업데이트하여 사용 가능한지 확인해야 합니다. + +```rust +// in is_device_suitable +let supported_features = unsafe { instance.get_physical_device_features(device) }; + +// ... +indices.is_complete() + && extensions_supported + && swapchain_adequate + && supported_features.sampler_anisotropy == vk::TRUE +``` + +`get_physical_device_features`는 `ash::vk::PhysicalDeviceFeatures` 구조체를 사용하여 지원되는 기능을 나타냅니다. + +비등방성 필터링의 사용 가능성을 강제하는 대신, 조건부로 사용하지 않도록 설정할 수도 있습니다. + +```rust +// ... + .anisotropy_enable(false) + .max_anisotropy(1.0) +// ... +``` + +다음 장에서는 이미지와 샘플러 객체를 셰이더에 노출하여 사각형에 텍스처를 그릴 것입니다. + +(참고: 코드 링크는 원본 C++ 튜토리얼을 가리킵니다.) + +[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 new file mode 100644 index 00000000..e16c6e29 --- /dev/null +++ b/ko-rust/06_Texture_mapping/02_Combined_image_sampler.md @@ -0,0 +1,264 @@ +## 소개 + +우리는 유니폼 버퍼(uniform buffers) 파트에서 처음으로 디스크립터(descriptor)를 살펴보았습니다. 이번 챕터에서는 새로운 유형의 디스크립터인 **결합 이미지 샘플러(combined image sampler)**에 대해 알아보겠습니다. 이 디스크립터를 사용하면 셰이더가 이전 챕터에서 생성한 것과 같은 샘플러 객체를 통해 이미지 리소스에 접근할 수 있습니다. + +우선 디스크립터 셋 레이아웃, 디스크립터 풀, 디스크립터 셋을 수정하여 결합 이미지 샘플러 디스크립터를 포함하는 것부터 시작하겠습니다. 그 후, `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) +}?; +``` + +`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) +}?; +``` + +부적절한 디스크립터 풀은 검증 레이어가 잡아내지 못하는 문제의 좋은 예입니다. Vulkan 1.1부터, 풀이 충분히 크지 않으면 `vkAllocateDescriptorSets`가 `VK_ERROR_POOL_OUT_OF_MEMORY` 오류 코드로 실패할 수 있지만, 드라이버가 내부적으로 이 문제를 해결하려고 시도할 수도 있습니다. 이는 때때로 (하드웨어, 풀 크기, 할당 크기에 따라) 드라이버가 디스크립터 풀의 한도를 초과하는 할당을 허용할 수 있음을 의미합니다. 다른 경우에는 `vkAllocateDescriptorSets`가 실패하고 `VK_ERROR_POOL_OUT_OF_MEMORY`를 반환합니다. 이는 일부 머신에서는 할당이 성공하고 다른 머신에서는 실패할 경우 특히 좌절스러울 수 있습니다. + +Vulkan은 할당에 대한 책임을 드라이버에게 넘기므로, 디스크립터 풀 생성 시 해당 `descriptor_count` 멤버로 지정된 만큼만 특정 유형의 디스크립터를 할당하는 것이 더 이상 엄격한 요구 사항은 아닙니다. 하지만, 여전히 그렇게 하는 것이 모범 사례로 남아 있습니다. + +마지막 단계는 실제 이미지와 샘플러 리소스를 디스크립터 셋의 디스크립터에 바인딩하는 것입니다. `create_descriptor_sets` 함수로 가세요. + +```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, + }]; + + let image_info = [vk::DescriptorImageInfo { + image_layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL, + image_view: self.texture_image_view, + sampler: self.texture_sampler, + }]; + + // ... +} +``` + +결합 이미지 샘플러 구조를 위한 리소스는 유니폼 버퍼 디스크립터의 버퍼 리소스가 `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` 슬라이스를 사용합니다. 이제 디스크립터는 셰이더에서 사용할 준비가 되었습니다! + +## 텍스처 좌표 + +텍스처 매핑에 있어 아직 빠진 중요한 요소가 하나 있는데, 바로 각 정점에 대한 실제 텍스처 좌표입니다. 텍스처 좌표는 이미지가 지오메트리에 실제로 어떻게 매핑될지 결정합니다. + +```rust +// glam이나 다른 수학 라이브러리를 사용한다고 가정합니다. +use glam::{Vec2, Vec3}; +// C++의 offsetof와 같은 기능을 위해 memoffset 크레이트가 필요합니다. +// Cargo.toml에 memoffset = "0.9" 를 추가하세요. +use memoffset::offset_of; + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct Vertex { + pos: Vec2, + color: Vec3, + tex_coord: Vec2, +} + +impl Vertex { + fn get_binding_description() -> vk::VertexInputBindingDescription { + vk::VertexInputBindingDescription { + binding: 0, + stride: std::mem::size_of::() as u32, + input_rate: vk::VertexInputRate::VERTEX, + } + } + + 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, + }, + ] + } +} +``` + +`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) }, +]; +``` + +이 튜토리얼에서는 왼쪽 위 모서리의 `(0, 0)`에서 오른쪽 아래 모서리의 `(1, 1)`까지의 좌표를 사용하여 텍스처로 사각형을 채울 것입니다. 자유롭게 다른 좌표로 실험해보세요. `0` 미만 또는 `1` 초과의 좌표를 사용하여 주소 지정 모드가 실제로 어떻게 작동하는지 확인해보세요! + +## 셰이더 + +마지막 단계는 셰이더를 수정하여 텍스처에서 색상을 샘플링하는 것입니다. 먼저 버텍스 셰이더를 수정하여 텍스처 좌표를 프래그먼트 셰이더로 전달해야 합니다. + +```glsl +// vertex shader +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; +} +``` + +정점별 색상과 마찬가지로 `fragTexCoord` 값은 래스터라이저에 의해 사각형 영역 전체에 걸쳐 부드럽게 보간됩니다. 프래그먼트 셰이더가 텍스처 좌표를 색상으로 출력하게 하여 이를 시각화할 수 있습니다. + +```glsl +// fragment shader +#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); +} +``` + +셰이더를 다시 컴파일하는 것을 잊지 마세요! 아래와 같은 이미지를 보게 될 것입니다. + +![](/images/texcoord_visualization.png) + +녹색 채널은 수평 좌표를, 적색 채널은 수직 좌표를 나타냅니다. 검은색과 노란색 모서리는 텍스처 좌표가 사각형에 걸쳐 `0, 0`에서 `1, 1`까지 올바르게 보간되었음을 확인시켜 줍니다. 색상을 사용한 데이터 시각화는 셰이더 프로그래밍에서 더 나은 대안이 없을 때 사용하는 `println!` 디버깅과 같습니다! + +결합 이미지 샘플러 디스크립터는 GLSL에서 샘플러 유니폼으로 표현됩니다. 프래그먼트 셰이더에 이에 대한 참조를 추가하세요. + +```glsl +// fragment shader +layout(binding = 1) uniform sampler2D texSampler; +``` + +다른 유형의 이미지를 위한 `sampler1D` 및 `sampler3D`와 같은 타입도 있습니다. 여기서 올바른 바인딩(`binding = 1`)을 사용해야 합니다. + +```glsl +// fragment shader main() +void main() { + outColor = texture(texSampler, fragTexCoord); +} +``` + +텍스처는 내장 함수 `texture`를 사용하여 샘플링됩니다. 이 함수는 `sampler`와 좌표를 인수로 받습니다. 샘플러는 백그라운드에서 필터링과 변환을 자동으로 처리합니다. 이제 애플리케이션을 실행하면 사각형 위에 텍스처가 표시될 것입니다. + +![](/images/texture_on_square.png) + +텍스처 좌표를 `1`보다 큰 값으로 조정하여 주소 지정 모드를 실험해보세요. 예를 들어, 다음 프래그먼트 셰이더는 `VK_SAMPLER_ADDRESS_MODE_REPEAT`를 사용할 때 아래 이미지와 같은 결과를 생성합니다. + +```glsl +// fragment shader main() +void main() { + outColor = texture(texSampler, fragTexCoord * 2.0); +} +``` + +![](/images/texture_on_square_repeated.png) + +정점 색상을 사용하여 텍스처 색상을 조작할 수도 있습니다. + +```glsl +// fragment shader main() +void main() { + outColor = vec4(fragColor * texture(texSampler, fragTexCoord).rgb, 1.0); +} +``` + +알파 채널이 변하지 않도록 RGB와 알파 채널을 분리했습니다. + +![](/images/texture_on_square_colorized.png) + +이제 셰이더에서 이미지에 접근하는 방법을 알게 되었습니다! 이 기술은 프레임버퍼에 쓰여지기도 하는 이미지와 결합될 때 매우 강력합니다. 이러한 이미지를 입력으로 사용하여 후처리(post-processing)나 3D 세계 내 카메라 디스플레이와 같은 멋진 효과를 구현할 수 있습니다. \ No newline at end of file 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..6ccf987b --- /dev/null +++ b/ko/03_Drawing_a_triangle/00_Setup/00_Base_code.md @@ -0,0 +1,159 @@ +## 일반적인 구조 + +이전 장에서 여러분은 모든 설정을 마친 Vulkan 프로젝트를 만들고 예제 코드로 테스트했습니다. 이번 장에서는 다음 코드를 가지고 처음부터 시작합니다. + +```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; +} +``` + +먼저 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 +``` + +이렇게 하면 GLFW가 자체 정의를 포함하고 Vulkan 헤더를 자동으로 함께 로드합니다. `initWindow` 함수를 추가하고 `run` 함수에서 다른 호출들보다 먼저 호출하도록 추가하세요. 이 함수를 사용하여 GLFW를 초기화하고 창을 생성할 것입니다. + +```c++ +void run() { + initWindow(); + initVulkan(); + mainLoop(); + cleanup(); +} + +private: + void initWindow() { + + } +``` + +`initWindow`의 가장 첫 번째 호출은 GLFW 라이브러리를 초기화하는 `glfwInit()`이어야 합니다. GLFW는 원래 OpenGL 컨텍스트를 생성하도록 설계되었기 때문에, 다음 호출을 통해 OpenGL 컨텍스트를 생성하지 않도록 알려줘야 합니다. + +```c++ +glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); +``` + +크기가 조절된 창을 처리하는 것은 나중에 다룰 특별한 주의가 필요하기 때문에, 지금은 다른 윈도우 힌트 호출로 비활성화합니다. + +```c++ +glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); +``` + +이제 남은 것은 실제 창을 만드는 것뿐입니다. 창에 대한 참조를 저장할 `GLFWwindow* window;` 비공개 클래스 멤버를 추가하고 다음 코드로 창을 초기화하세요. + +```c++ +window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr); +``` + +처음 세 매개변수는 창의 너비, 높이, 제목을 지정합니다. 네 번째 매개변수는 창을 열 모니터를 선택적으로 지정할 수 있게 하고, 마지막 매개변수는 OpenGL에만 관련이 있습니다. + +너비와 높이를 하드코딩된 숫자 대신 상수로 사용하는 것이 좋습니다. 앞으로 이 값들을 여러 번 참조할 것이기 때문입니다. 저는 `HelloTriangleApplication` 클래스 정의 위에 다음 줄을 추가했습니다. + +```c++ +const uint32_t WIDTH = 800; +const uint32_t HEIGHT = 600; +``` + +그리고 창 생성 호출을 다음과 같이 바꿨습니다. + +```c++ +window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); +``` + +이제 `initWindow` 함수는 다음과 같아야 합니다. + +```c++ +void initWindow() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + + window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); +} +``` + +오류가 발생하거나 창이 닫힐 때까지 애플리케이션을 계속 실행하려면, `mainLoop` 함수에 다음과 같이 이벤트 루프를 추가해야 합니다. + +```c++ +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + } +} +``` + +이 코드는 상당히 자명해야 합니다. 사용자가 창을 닫을 때까지 X 버튼 누르기와 같은 이벤트를 확인하며 반복합니다. 이곳은 나중에 단일 프레임을 렌더링하는 함수를 호출할 루프이기도 합니다. + +창이 닫히면, 창을 파괴하고 GLFW 자체를 종료하여 리소스를 정리해야 합니다. 이것이 우리의 첫 번째 `cleanup` 코드가 될 것입니다. + +```c++ +void cleanup() { + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +이제 프로그램을 실행하면 "Vulkan"이라는 제목의 창이 나타나고, 창을 닫아 애플리케이션이 종료될 때까지 유지됩니다. 이제 Vulkan 애플리케이션의 골격을 갖추었으니, [첫 번째 Vulkan 객체 생성하기](!ko/Drawing_a_triangle/Setup/Instance)로 넘어갑시다! + +[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 new file mode 100644 index 00000000..48042682 --- /dev/null +++ b/ko/03_Drawing_a_triangle/00_Setup/01_Instance.md @@ -0,0 +1,166 @@ +## 인스턴스 생성 + +가장 먼저 해야 할 일은 *인스턴스(instance)*를 생성하여 Vulkan 라이브러리를 초기화하는 것입니다. 인스턴스는 애플리케이션과 Vulkan 라이브러리 간의 연결고리이며, 인스턴스를 생성하는 과정에는 애플리케이션에 대한 몇 가지 세부 정보를 드라이버에 지정하는 작업이 포함됩니다. + +먼저 `createInstance` 함수를 추가하고 `initVulkan` 함수에서 호출합니다. + +```c++ +void initVulkan() { + createInstance(); +} +``` + +또한 인스턴스 핸들을 저장할 데이터 멤버를 추가합니다. + +```c++ +private: +VkInstance instance; +``` + +이제 인스턴스를 생성하기 위해 먼저 애플리케이션에 대한 정보가 담긴 구조체를 채워야 합니다. 이 데이터는 기술적으로는 선택 사항이지만, 드라이버가 우리의 특정 애플리케이션을 최적화하는 데 유용한 정보를 제공할 수 있습니다(예: 특정 특수 동작을 하는 잘 알려진 그래픽 엔진을 사용하는 경우). 이 구조체는 `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; +} +``` + +앞서 언급했듯이, Vulkan의 많은 구조체는 `sType` 멤버에 명시적으로 타입을 지정해야 합니다. 이 구조체는 또한 나중에 확장 정보를 가리킬 수 있는 `pNext` 멤버를 가진 많은 구조체 중 하나입니다. 여기서는 값 초기화(value initialization)를 사용하여 `nullptr`로 남겨둡니다. + +Vulkan에서는 많은 정보가 함수 매개변수 대신 구조체를 통해 전달되며, 인스턴스 생성을 위한 충분한 정보를 제공하기 위해 또 다른 구조체를 채워야 합니다. 이 다음 구조체는 선택 사항이 아니며, 우리가 사용하려는 전역(global) 확장과 유효성 검사 레이어를 Vulkan 드라이버에 알려줍니다. 여기서 전역이라는 의미는 특정 장치가 아닌 프로그램 전체에 적용된다는 뜻이며, 이는 다음 몇 장에서 명확해질 것입니다. + +```c++ +VkInstanceCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; +createInfo.pApplicationInfo = &appInfo; +``` + +처음 두 매개변수는 간단합니다. 다음 두 멤버는 원하는 전역 확장을 지정합니다. 개요 장에서 언급했듯이, Vulkan은 플랫폼에 구애받지 않는(platform agnostic) API이므로, 창 시스템과 상호작용하려면 확장이 필요합니다. GLFW에는 이를 위해 필요한 확장을 반환하는 편리한 내장 함수가 있으며, 이 함수가 반환하는 값을 구조체에 전달할 수 있습니다. + +```c++ +uint32_t glfwExtensionCount = 0; +const char** glfwExtensions; + +glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + +createInfo.enabledExtensionCount = glfwExtensionCount; +createInfo.ppEnabledExtensionNames = glfwExtensions; +``` + +구조체의 마지막 두 멤버는 활성화할 전역 유효성 검사 레이어를 결정합니다. 이에 대해서는 다음 장에서 더 자세히 다룰 것이므로 지금은 비워둡니다. + +```c++ +createInfo.enabledLayerCount = 0; +``` + +이제 Vulkan이 인스턴스를 생성하는 데 필요한 모든 것을 지정했으므로, 마침내 `vkCreateInstance` 호출을 실행할 수 있습니다. + +```c++ +VkResult result = vkCreateInstance(&createInfo, nullptr, &instance); +``` + +보시다시피, Vulkan에서 객체 생성 함수의 매개변수는 일반적으로 다음과 같은 패턴을 따릅니다: + +* 생성 정보가 담긴 구조체에 대한 포인터 +* 사용자 정의 할당자 콜백에 대한 포인터, 이 튜토리얼에서는 항상 `nullptr` +* 새 객체의 핸들을 저장할 변수에 대한 포인터 + +모든 것이 순조롭게 진행되었다면 인스턴스 핸들이 `VkInstance` 클래스 멤버에 저장되었을 것입니다. 거의 모든 Vulkan 함수는 `VK_SUCCESS` 또는 오류 코드인 `VkResult` 타입의 값을 반환합니다. 인스턴스가 성공적으로 생성되었는지 확인하기 위해 결과를 저장할 필요 없이 성공 값에 대한 검사만 사용하면 됩니다. + +```c++ +if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); +} +``` + +이제 프로그램을 실행하여 인스턴스가 성공적으로 생성되는지 확인하세요. + +## 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` 확장이 필수로 요구됩니다. + +이 오류를 해결하려면, 먼저 `VkInstanceCreateInfo` 구조체의 `flags`에 `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!"); +} +``` + +## 확장 지원 여부 확인 + +`vkCreateInstance` 문서를 보면 가능한 오류 코드 중 하나가 `VK_ERROR_EXTENSION_NOT_PRESENT`임을 알 수 있습니다. 우리는 단순히 필요한 확장을 지정하고 해당 오류 코드가 반환되면 프로그램을 종료할 수 있습니다. 이는 창 시스템 인터페이스와 같은 필수적인 확장에는 합리적이지만, 선택적 기능에 대한 지원 여부를 확인하고 싶다면 어떻게 해야 할까요? + +인스턴스를 생성하기 전에 지원되는 확장 목록을 가져오려면 `vkEnumerateInstanceExtensionProperties` 함수가 있습니다. 이 함수는 확장의 수를 저장할 변수에 대한 포인터와 확장의 세부 정보를 저장할 `VkExtensionProperties` 배열을 매개변수로 받습니다. 또한 특정 유효성 검사 레이어로 확장을 필터링할 수 있는 선택적 첫 번째 매개변수가 있지만, 지금은 무시하겠습니다. + +확장 세부 정보를 담을 배열을 할당하려면 먼저 몇 개가 있는지 알아야 합니다. 뒤쪽 매개변수를 비워두면 확장의 수만 요청할 수 있습니다. + +```c++ +uint32_t extensionCount = 0; +vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); +``` + +이제 확장 세부 정보를 담을 배열을 할당합니다(`#include ` 필요). + +```c++ +std::vector extensions(extensionCount); +``` + +마지막으로 확장 세부 정보를 쿼리할 수 있습니다. + +```c++ +vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data()); +``` + +각 `VkExtensionProperties` 구조체에는 확장의 이름과 버전이 포함됩니다. 간단한 for 루프로 목록을 출력할 수 있습니다(`\t`는 들여쓰기를 위한 탭입니다). + +```c++ +std::cout << "available extensions:\n"; + +for (const auto& extension : extensions) { + std::cout << '\t' << extension.extensionName << '\n'; +} +``` + +Vulkan 지원에 대한 세부 정보를 제공하고 싶다면 이 코드를 `createInstance` 함수에 추가할 수 있습니다. 도전 과제로, `glfwGetRequiredInstanceExtensions`가 반환한 모든 확장이 지원되는 확장 목록에 포함되어 있는지 확인하는 함수를 만들어 보세요. + +## 정리 + +`VkInstance`는 프로그램이 종료되기 직전에만 파괴되어야 합니다. `cleanup` 함수에서 `vkDestroyInstance` 함수를 사용하여 파괴할 수 있습니다. + +```c++ +void cleanup() { + vkDestroyInstance(instance, nullptr); + + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +`vkDestroyInstance` 함수의 매개변수는 간단합니다. 이전 장에서 언급했듯이, Vulkan의 할당 및 해제 함수에는 선택적인 할당자 콜백이 있는데, 우리는 `nullptr`를 전달하여 이를 무시할 것입니다. 다음 장에서 생성할 다른 모든 Vulkan 리소스는 인스턴스가 파괴되기 전에 정리해야 합니다. + +인스턴스 생성 후의 더 복잡한 단계로 넘어가기 전에, [유효성 검사 레이어](!ko/Drawing_a_triangle/Setup/Validation_layers)를 살펴봄으로써 디버깅 옵션을 평가해 볼 시간입니다. + +[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 new file mode 100644 index 00000000..14632969 --- /dev/null +++ b/ko/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md @@ -0,0 +1,374 @@ +## 밸리데이션 레이어란 무엇인가? + +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( + const VkInstanceCreateInfo* pCreateInfo, + const VkAllocationCallbacks* pAllocator, + VkInstance* instance) { + + if (pCreateInfo == nullptr || instance == nullptr) { + log("필수 파라미터에 null 포인터가 전달되었습니다!"); + return VK_ERROR_INITIALIZATION_FAILED; + } + + return real_vkCreateInstance(pCreateInfo, pAllocator, instance); +} +``` + +이러한 밸리데이션 레이어들은 원하는 모든 디버깅 기능을 포함하도록 자유롭게 쌓아서(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; +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 +``` + +요청한 모든 레이어가 사용 가능한지 확인하는 `checkValidationLayerSupport`라는 새 함수를 추가하겠습니다. 먼저 `vkEnumerateInstanceLayerProperties` 함수를 사용하여 사용 가능한 모든 레이어를 나열합니다. 이 함수의 사용법은 인스턴스 생성 챕터에서 다룬 `vkEnumerateInstanceExtensionProperties`와 동일합니다. + +```c++ +bool checkValidationLayerSupport() { + uint32_t layerCount; + vkEnumerateInstanceLayerProperties(&layerCount, nullptr); + + std::vector availableLayers(layerCount); + vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data()); + + return false; +} +``` + +다음으로, `validationLayers`에 있는 모든 레이어가 `availableLayers` 목록에 존재하는지 확인합니다. `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; +``` + +이제 이 함수를 `createInstance`에서 사용할 수 있습니다. + +```c++ +void createInstance() { + if (enableValidationLayers && !checkValidationLayerSupport()) { + throw std::runtime_error("요청한 밸리데이션 레이어를 사용할 수 없습니다!"); + } + + ... +} +``` + +이제 디버그 모드로 프로그램을 실행하여 오류가 발생하지 않는지 확인하세요. 만약 오류가 발생한다면 FAQ를 확인해 보세요. + +마지막으로, `VkInstanceCreateInfo` 구조체 인스턴스화 부분을 수정하여 밸리데이션 레이어가 활성화된 경우 레이어 이름을 포함하도록 합니다. + +```c++ +if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); +} else { + createInfo.enabledLayerCount = 0; +} +``` + +검사가 성공했다면 `vkCreateInstance`는 `VK_ERROR_LAYER_NOT_PRESENT` 오류를 반환하지 않아야 하지만, 프로그램을 실행하여 확실히 확인해야 합니다. + +## 메시지 콜백 + +밸리데이션 레이어는 기본적으로 디버그 메시지를 표준 출력으로 인쇄하지만, 프로그램에 명시적인 콜백을 제공하여 직접 처리할 수도 있습니다. 이를 통해 어떤 종류의 메시지를 보고 싶은지 결정할 수 있습니다. 모든 메시지가 반드시 (치명적인) 오류는 아니기 때문입니다. 지금 당장 이 작업을 하고 싶지 않다면 이 챕터의 마지막 섹션으로 건너뛰어도 좋습니다. + +메시지와 관련 세부 정보를 처리할 콜백을 프로그램에 설정하려면 `VK_EXT_debug_utils` 확장을 사용하여 디버그 메신저와 콜백을 설정해야 합니다. + +먼저 밸리데이션 레이어 활성화 여부에 따라 필요한 확장 목록을 반환하는 `getRequiredExtensions` 함수를 만들겠습니다. + +```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; +} +``` + +GLFW가 지정한 확장은 항상 필요하지만, 디버그 메신저 확장은 조건부로 추가됩니다. 여기서 저는 리터럴 문자열 "VK_EXT_debug_utils"와 동일한 `VK_EXT_DEBUG_UTILS_EXTENSION_NAME` 매크로를 사용했습니다. 이 매크로를 사용하면 오타를 방지할 수 있습니다. + +이제 이 함수를 `createInstance`에서 사용할 수 있습니다. + +```c++ +auto extensions = getRequiredExtensions(); +createInfo.enabledExtensionCount = static_cast(extensions.size()); +createInfo.ppEnabledExtensionNames = extensions.data(); +``` + +프로그램을 실행하여 `VK_ERROR_EXTENSION_NOT_PRESENT` 오류가 발생하지 않는지 확인하세요. 이 확장의 존재 여부를 굳이 확인할 필요는 없습니다. 밸리데이션 레이어가 사용 가능하다면 이 확장도 당연히 사용 가능해야 하기 때문입니다. + +이제 디버그 콜백 함수가 어떻게 생겼는지 봅시다. `PFN_vkDebugUtilsMessengerCallbackEXT` 프로토타입을 가진 `debugCallback`이라는 새 정적 멤버 함수를 추가합니다. `VKAPI_ATTR`과 `VKAPI_CALL`은 함수가 Vulkan이 호출할 수 있는 올바른 시그니처를 갖도록 보장합니다. + +```c++ +static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( + VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + VkDebugUtilsMessageTypeFlagsEXT messageType, + const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, + void* pUserData) { + + std::cerr << "밸리데이션 레이어: " << pCallbackData->pMessage << std::endl; + + return VK_FALSE; +} +``` + +첫 번째 파라미터는 메시지의 심각도(severity)를 지정하며, 다음 플래그 중 하나입니다: + +* `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`: 유효하지 않으며 크래시를 유발할 수 있는 동작에 대한 메시지 + +이 열거형의 값들은 비교 연산을 사용하여 메시지가 특정 심각도 수준과 같거나 더 나쁜지 확인할 수 있도록 설정되어 있습니다. 예를 들면 다음과 같습니다: + +```c++ +if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + // 메시지가 표시할 만큼 중요함 +} +``` + +`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`: Vulkan의 잠재적인 비최적 사용 + +`pCallbackData` 파라미터는 메시지 자체의 세부 정보를 포함하는 `VkDebugUtilsMessengerCallbackDataEXT` 구조체를 가리키며, 가장 중요한 멤버는 다음과 같습니다: + +* `pMessage`: null로 끝나는 문자열 형태의 디버그 메시지 +* `pObjects`: 메시지와 관련된 Vulkan 객체 핸들 배열 +* `objectCount`: 배열에 있는 객체의 수 + +마지막으로, `pUserData` 파라미터는 콜백 설정 중에 지정된 포인터를 포함하며, 이를 통해 자신의 데이터를 콜백에 전달할 수 있습니다. + +콜백은 밸리데이션 레이어 메시지를 촉발한 Vulkan 호출을 중단해야 하는지를 나타내는 불리언 값을 반환합니다. 콜백이 true를 반환하면 해당 호출은 `VK_ERROR_VALIDATION_FAILED_EXT` 오류와 함께 중단됩니다. 이는 보통 밸리데이션 레이어 자체를 테스트하는 데만 사용되므로, 항상 `VK_FALSE`를 반환해야 합니다. + +이제 남은 것은 Vulkan에 콜백 함수에 대해 알리는 것뿐입니다. 아마도 놀랍게도 Vulkan의 디버그 콜백조차도 명시적으로 생성하고 소멸해야 하는 핸들로 관리됩니다. 이러한 콜백은 *디버그 메신저*의 일부이며, 원하는 만큼 많이 가질 수 있습니다. `instance` 바로 아래에 이 핸들을 위한 클래스 멤버를 추가합니다: + +```c++ +VkDebugUtilsMessengerEXT debugMessenger; +``` + +이제 `createInstance` 직후 `initVulkan`에서 호출할 `setupDebugMessenger` 함수를 추가합니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); +} + +void setupDebugMessenger() { + if (!enableValidationLayers) return; + +} +``` + +메신저와 그 콜백에 대한 세부 정보로 구조체를 채워야 합니다: + +```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; // 선택 사항 +``` + +`messageSeverity` 필드를 사용하면 콜백이 호출되기를 원하는 모든 심각도 유형을 지정할 수 있습니다. 여기서는 상세한 일반 디버그 정보를 제외하고 가능한 문제에 대한 알림을 받기 위해 `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT`를 제외한 모든 유형을 지정했습니다. + +마찬가지로 `messageType` 필드를 사용하면 콜백이 알림을 받을 메시지 유형을 필터링할 수 있습니다. 여기서는 단순히 모든 유형을 활성화했습니다. 유용하지 않은 유형이 있다면 언제든지 비활성화할 수 있습니다. + +마지막으로 `pfnUserCallback` 필드는 콜백 함수에 대한 포인터를 지정합니다. 선택적으로 `pUserData` 필드에 포인터를 전달할 수 있으며, 이 포인터는 `pUserData` 파라미터를 통해 콜백 함수로 전달됩니다. 예를 들어, 이를 사용하여 `HelloTriangleApplication` 클래스에 대한 포인터를 전달할 수 있습니다. + +밸리데이션 레이어 메시지와 디버그 콜백을 구성하는 더 많은 방법이 있지만, 이 튜토리얼을 시작하기에는 이 설정이 좋습니다. 가능한 설정에 대한 자세한 내용은 [확장 사양](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap50.html#VK_EXT_debug_utils)을 참조하세요. + +이 구조체는 `vkCreateDebugUtilsMessengerEXT` 함수에 전달되어 `VkDebugUtilsMessengerEXT` 객체를 생성해야 합니다. 안타깝게도 이 함수는 확장 함수이므로 자동으로 로드되지 않습니다. `vkGetInstanceProcAddr`를 사용하여 주소를 직접 찾아야 합니다. 이 작업을 백그라운드에서 처리하는 프록시 함수를 직접 만들겠습니다. `HelloTriangleApplication` 클래스 정의 바로 위에 추가했습니다. + +```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; + } +} +``` + +`vkGetInstanceProcAddr` 함수는 함수를 로드할 수 없는 경우 `nullptr`를 반환합니다. 이제 이 함수를 호출하여 확장 객체를 생성할 수 있습니다. + +```c++ +if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { + throw std::runtime_error("디버그 메신저 설정에 실패했습니다!"); +} +``` + +끝에서 두 번째 파라미터는 다시 선택적 할당자 콜백으로, 여기서는 `nullptr`로 설정했습니다. 그 외의 파라미터는 매우 간단합니다. 디버그 메신저는 우리의 Vulkan 인스턴스와 그 레이어에 한정적이므로, 첫 번째 인자로 명시적으로 지정해야 합니다. 나중에 다른 *자식* 객체에서도 이 패턴을 보게 될 것입니다. + +`VkDebugUtilsMessengerEXT` 객체는 `vkDestroyDebugUtilsMessengerEXT` 호출로 정리해야 합니다. `vkCreateDebugUtilsMessengerEXT`와 마찬가지로 이 함수도 명시적으로 로드해야 합니다. + +`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); + } +} +``` + +이 함수가 정적 클래스 함수이거나 클래스 외부의 함수인지 확인하세요. 그런 다음 `cleanup` 함수에서 호출할 수 있습니다. + +```c++ +void cleanup() { + if (enableValidationLayers) { + DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr); + } + + vkDestroyInstance(instance, nullptr); + + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +## 인스턴스 생성 및 소멸 디버깅하기 + +지금까지 프로그램에 밸리데이션 레이어를 사용한 디버깅을 추가했지만 아직 모든 것을 다루지는 못했습니다. `vkCreateDebugUtilsMessengerEXT` 호출은 유효한 인스턴스가 생성되어 있어야 하고, `vkDestroyDebugUtilsMessengerEXT`는 인스턴스가 소멸되기 전에 호출되어야 합니다. 이로 인해 현재로서는 `vkCreateInstance` 및 `vkDestroyInstance` 호출의 문제를 디버깅할 수 없습니다. + +하지만 [확장 문서](https://github.com/KhronosGroup/Vulkan-Docs/blob/main/appendices/VK_EXT_debug_utils.adoc#examples)를 자세히 읽어보면, 이 두 함수 호출을 위해 별도의 디버그 유틸리티 메신저를 만드는 방법이 있다는 것을 알 수 있습니다. `VkInstanceCreateInfo`의 `pNext` 확장 필드에 `VkDebugUtilsMessengerCreateInfoEXT` 구조체에 대한 포인터를 전달하기만 하면 됩니다. 먼저 메신저 생성 정보 채우기를 별도의 함수로 추출합니다. + +```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("디버그 메신저 설정에 실패했습니다!"); + } +} +``` + +이제 이 함수를 `createInstance` 함수에서 재사용할 수 있습니다. + +```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("인스턴스 생성에 실패했습니다!"); + } +} +``` + +`debugCreateInfo` 변수는 `vkCreateInstance` 호출 전에 소멸되지 않도록 `if` 문 밖에 위치합니다. 이런 식으로 추가 디버그 메신저를 생성하면 `vkCreateInstance`와 `vkDestroyInstance` 동안 자동으로 사용되고 그 이후에 정리됩니다. + +## 테스트하기 + +이제 의도적으로 실수를 만들어 밸리데이션 레이어가 작동하는 것을 확인해 봅시다. `cleanup` 함수에서 `DestroyDebugUtilsMessengerEXT` 호출을 일시적으로 제거하고 프로그램을 실행하세요. 프로그램이 종료되면 다음과 같은 메시지를 보게 될 것입니다. + +![](/images/validation_layer_test.png) + +> 메시지가 보이지 않는다면 [설치를 확인](https://vulkan.lunarg.com/doc/view/1.2.131.1/windows/getting_started.html#user-content-verify-the-installation)하세요. + +어떤 호출이 메시지를 유발했는지 확인하고 싶다면, 메시지 콜백에 중단점(breakpoint)을 설정하고 스택 추적을 살펴보면 됩니다. + +## 설정 + +`VkDebugUtilsMessengerCreateInfoEXT` 구조체에 지정된 플래그 외에도 밸리데이션 레이어의 동작에 대한 설정은 훨씬 더 많습니다. Vulkan SDK로 이동하여 `Config` 디렉토리로 가보세요. 그곳에서 레이어를 구성하는 방법을 설명하는 `vk_layer_settings.txt` 파일을 찾을 수 있습니다. + +자신의 애플리케이션에 대한 레이어 설정을 구성하려면, 이 파일을 프로젝트의 `Debug` 및 `Release` 디렉토리에 복사하고 지침에 따라 원하는 동작을 설정하세요. 하지만 이 튜토리얼의 나머지 부분에서는 기본 설정을 사용한다고 가정하겠습니다. + +이 튜토리얼 전반에 걸쳐, 밸리데이션 레이어가 실수를 잡아내는 데 얼마나 도움이 되는지 보여주고 Vulkan으로 무엇을 하고 있는지 정확히 아는 것이 얼마나 중요한지 가르치기 위해 몇 가지 의도적인 실수를 할 것입니다. 이제 시스템의 [Vulkan 장치](!ko/Drawing_a_triangle/Setup/Physical_devices_and_queue_families)에 대해 알아볼 시간입니다. + +[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 new file mode 100644 index 00000000..31fb7dc0 --- /dev/null +++ b/ko/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md @@ -0,0 +1,309 @@ +## 물리 디바이스 선택하기 + +`VkInstance`를 통해 벌칸 라이브러리를 초기화한 후에는, 시스템에서 우리가 필요로 하는 기능을 지원하는 그래픽 카드를 찾아 선택해야 합니다. 사실 여러 개의 그래픽 카드를 선택하여 동시에 사용할 수도 있지만, 이 튜토리얼에서는 우리에게 필요한 첫 번째 그래픽 카드만 사용하겠습니다. + +`pickPhysicalDevice` 함수를 추가하고 `initVulkan` 함수에서 호출하도록 하겠습니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + pickPhysicalDevice(); +} + +void pickPhysicalDevice() { + +} +``` + +최종적으로 선택할 그래픽 카드는 `VkPhysicalDevice` 핸들에 저장되며, 이 핸들을 새로운 클래스 멤버로 추가합니다. 이 객체는 `VkInstance`가 소멸될 때 암시적으로 함께 소멸되므로, `cleanup` 함수에서 따로 처리할 필요는 없습니다. + +```c++ +VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; +``` + +그래픽 카드를 나열하는 것은 확장을 나열하는 것과 매우 유사하며, 먼저 개수만 쿼리하는 것으로 시작합니다. + +```c++ +uint32_t deviceCount = 0; +vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr); +``` + +만약 벌칸을 지원하는 디바이스가 하나도 없다면 더 이상 진행할 의미가 없습니다. + +```c++ +if (deviceCount == 0) { + throw std::runtime_error("failed to find GPUs with Vulkan support!"); +} +``` + +벌칸 지원 디바이스가 있다면, 모든 `VkPhysicalDevice` 핸들을 담을 배열을 할당할 수 있습니다. + +```c++ +std::vector devices(deviceCount); +vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()); +``` + +이제 각 디바이스를 평가하여 우리가 수행하려는 작업에 적합한지 확인해야 합니다. 모든 그래픽 카드가 동일하게 만들어지지는 않기 때문입니다. 이를 위해 새로운 함수를 도입하겠습니다. + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + return true; +} +``` + +그리고 물리 디바이스 중 어느 것이든 이 함수에 추가할 요구사항을 충족하는지 확인할 것입니다. + +```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!"); +} +``` + +다음 섹션에서는 `isDeviceSuitable` 함수에서 확인할 첫 번째 요구사항을 소개할 것입니다. 이후 챕터에서 더 많은 벌칸 기능을 사용하게 되면서, 이 함수에 더 많은 검사를 추가하여 확장할 것입니다. + +## 기본적인 디바이스 적합성 검사 + +디바이스의 적합성을 평가하기 위해 몇 가지 세부 정보를 쿼리하는 것으로 시작할 수 있습니다. 이름, 타입, 지원하는 벌칸 버전과 같은 기본적인 디바이스 속성은 `vkGetPhysicalDeviceProperties`를 사용하여 쿼리할 수 있습니다. + +```c++ +VkPhysicalDeviceProperties deviceProperties; +vkGetPhysicalDeviceProperties(device, &deviceProperties); +``` + +텍스처 압축, 64비트 부동소수점, 다중 뷰포트 렌더링(VR에 유용함)과 같은 선택적 기능에 대한 지원 여부는 `vkGetPhysicalDeviceFeatures`를 사용하여 쿼리할 수 있습니다. + +```c++ +VkPhysicalDeviceFeatures deviceFeatures; +vkGetPhysicalDeviceFeatures(device, &deviceFeatures); +``` + +디바이스 메모리 및 큐 패밀리(다음 섹션 참조)에 관해 나중에 논의할 더 많은 세부 정보가 있습니다. + +예를 들어, 우리 애플리케이션이 지오메트리 셰이더를 지원하는 외장 그래픽 카드에서만 사용 가능하다고 가정해 봅시다. 그렇다면 `isDeviceSuitable` 함수는 다음과 같이 보일 것입니다. + +```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; +} +``` + +디바이스가 적합한지 아닌지만 확인하고 첫 번째 것을 사용하는 대신, 각 디바이스에 점수를 매겨 가장 높은 점수를 받은 디바이스를 선택할 수도 있습니다. 이런 방식을 사용하면 외장 그래픽 카드에 더 높은 점수를 주어 선호하되, 사용 가능한 유일한 GPU가 내장 GPU일 경우 차선책으로 선택할 수 있습니다. 다음과 같이 구현할 수 있습니다. + +```c++ +#include + +... + +void pickPhysicalDevice() { + ... + + // 순서가 있는 맵을 사용하여 후보들을 점수 오름차순으로 자동 정렬 + std::multimap candidates; + + for (const auto& device : devices) { + int score = rateDeviceSuitability(device); + candidates.insert(std::make_pair(score, device)); + } + + // 가장 좋은 후보가 사용 가능한지 확인 + 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; + + // 외장 GPU는 상당한 성능 이점을 가짐 + if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { + score += 1000; + } + + // 텍스처의 최대 크기는 그래픽 품질에 영향을 줌 + score += deviceProperties.limits.maxImageDimension2D; + + // 애플리케이션은 지오메트리 셰이더 없이는 작동할 수 없음 + if (!deviceFeatures.geometryShader) { + return 0; + } + + return score; +} +``` + +이 튜토리얼에서 이 모든 것을 구현할 필요는 없지만, 디바이스 선택 프로세스를 어떻게 설계할 수 있는지에 대한 아이디어를 제공하기 위한 것입니다. 물론, 선택 가능한 디바이스 목록을 보여주고 사용자가 선택하게 할 수도 있습니다. + +우리는 이제 막 시작하는 단계이므로, 벌칸 지원 여부만이 유일한 요구사항입니다. 따라서 어떤 GPU든 상관없이 사용하겠습니다. + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + return true; +} +``` + +다음 섹션에서는 확인해야 할 첫 번째 실질적인 필수 기능에 대해 논의할 것입니다. + +## 큐 패밀리 (Queue families) + +이전에도 잠시 언급했듯이, 드로잉부터 텍스처 업로드에 이르기까지 벌칸의 거의 모든 작업은 명령(command)을 큐(queue)에 제출해야 합니다. 큐에는 여러 종류가 있으며, 이들은 각기 다른 *큐 패밀리(queue families)*에서 비롯됩니다. 각 큐 패밀리는 특정 종류의 명령만 허용합니다. 예를 들어, 컴퓨트(compute) 명령만 처리하는 큐 패밀리가 있을 수 있고, 메모리 전송 관련 명령만 처리하는 큐 패밀리가 있을 수도 있습니다. + +우리는 디바이스가 어떤 큐 패밀리를 지원하는지, 그리고 그중 어떤 큐 패밀리가 우리가 사용하려는 명령을 지원하는지 확인해야 합니다. 이를 위해, 우리가 필요로 하는 모든 큐 패밀리를 찾는 새로운 함수 `findQueueFamilies`를 추가하겠습니다. + +지금은 그래픽 명령을 지원하는 큐만 찾을 것이므로, 함수는 다음과 같을 수 있습니다. + +```c++ +uint32_t findQueueFamilies(VkPhysicalDevice device) { + // 그래픽 큐 패밀리를 찾는 로직 +} +``` + +하지만 다음 챕터 중 하나에서 곧바로 다른 큐를 찾게 될 것이므로, 미리 대비하여 인덱스들을 구조체로 묶는 것이 좋습니다. + +```c++ +struct QueueFamilyIndices { + uint32_t graphicsFamily; +}; + +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + // 구조체를 채우기 위해 큐 패밀리 인덱스를 찾는 로직 + return indices; +} +``` + +그런데 만약 큐 패밀리를 찾을 수 없다면 어떻게 해야 할까요? `findQueueFamilies` 함수에서 예외를 던질 수도 있지만, 이 함수는 디바이스 적합성을 결정하기에 적절한 위치가 아닙니다. 예를 들어, 우리는 전용 전송(transfer) 큐 패밀리를 가진 디바이스를 *선호*할 수는 있지만, 필수 조건으로 삼고 싶지는 않을 수 있습니다. 따라서 특정 큐 패밀리가 발견되었는지 여부를 나타낼 방법이 필요합니다. + +큐 패밀리가 존재하지 않음을 나타내기 위해 특별한 값(magic value)을 사용하는 것은 사실상 불가능합니다. `0`을 포함한 모든 `uint32_t` 값이 이론적으로 유효한 큐 패밀리 인덱스가 될 수 있기 때문입니다. 다행히 C++17에서는 값이 존재하는 경우와 그렇지 않은 경우를 구별하기 위한 데이터 구조를 도입했습니다. + +```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`은 값을 할당하기 전까지는 아무 값도 포함하지 않는 래퍼(wrapper)입니다. 언제든지 `has_value()` 멤버 함수를 호출하여 값이 포함되어 있는지 여부를 쿼리할 수 있습니다. 이를 통해 로직을 다음과 같이 변경할 수 있습니다. + +```c++ +#include + +... + +struct QueueFamilyIndices { + std::optional graphicsFamily; +}; + +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + // 찾은 큐 패밀리에 인덱스 할당 + return indices; +} +``` + +이제 `findQueueFamilies`를 실제로 구현해 보겠습니다. + +```c++ +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + + ... + + return indices; +} +``` + +큐 패밀리 목록을 가져오는 과정은 예상대로이며, `vkGetPhysicalDeviceQueueFamilyProperties`를 사용합니다. + +```c++ +uint32_t queueFamilyCount = 0; +vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr); + +std::vector queueFamilies(queueFamilyCount); +vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data()); +``` + +`VkQueueFamilyProperties` 구조체는 지원되는 작업 유형 및 해당 패밀리를 기반으로 생성할 수 있는 큐의 수를 포함하여 큐 패밀리에 대한 일부 세부 정보를 담고 있습니다. 우리는 `VK_QUEUE_GRAPHICS_BIT`를 지원하는 큐 패밀리를 최소 하나 이상 찾아야 합니다. + +```c++ +int i = 0; +for (const auto& queueFamily : queueFamilies) { + if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) { + indices.graphicsFamily = i; + } + + i++; +} +``` + +이제 이 멋진 큐 패밀리 조회 함수를 `isDeviceSuitable` 함수에서 검사 항목으로 사용하여, 디바이스가 우리가 사용하려는 명령을 처리할 수 있는지 확인할 수 있습니다. + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + QueueFamilyIndices indices = findQueueFamilies(device); + + return indices.graphicsFamily.has_value(); +} +``` + +이를 좀 더 편리하게 만들기 위해, 구조체 자체에 일반적인 검사 함수를 추가하겠습니다. + +```c++ +struct QueueFamilyIndices { + std::optional graphicsFamily; + + bool isComplete() { + return graphicsFamily.has_value(); + } +}; + +... + +bool isDeviceSuitable(VkPhysicalDevice device) { + QueueFamilyIndices indices = findQueueFamilies(device); + + return indices.isComplete(); +} +``` + +이제 이 함수를 `findQueueFamilies`에서 조기 탈출하는 데에도 사용할 수 있습니다. + +```c++ +for (const auto& queueFamily : queueFamilies) { + ... + + if (indices.isComplete()) { + break; + } + + i++; +} +``` + +좋습니다, 이것으로 적절한 물리 디바이스를 찾는 데 필요한 모든 작업이 끝났습니다! 다음 단계는 [논리 디바이스를 생성하여](!ko/Drawing_a_triangle/Setup/Logical_device_and_queues) 물리 디바이스와 상호작용하는 것입니다. + +[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 new file mode 100644 index 00000000..6cbeae88 --- /dev/null +++ b/ko/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md @@ -0,0 +1,132 @@ +## 도입 + +사용할 물리 장치를 선택한 후에는, 이와 상호작용하기 위한 *논리 장치*를 설정해야 합니다. 논리 장치 생성 과정은 인스턴스 생성 과정과 유사하며, 우리가 사용하고자 하는 기능들을 기술합니다. 또한, 어떤 큐 패밀리를 사용할 수 있는지 질의했으므로 이제 어떤 큐를 생성할지 명시해야 합니다. 요구 사항이 다양하다면 동일한 물리 장치에서 여러 개의 논리 장치를 생성할 수도 있습니다. + +먼저 클래스 멤버를 새로 추가하여 논리 장치 핸들을 저장하도록 합시다. + +```c++ +VkDevice device; +``` + +다음으로, `initVulkan`에서 호출될 `createLogicalDevice` 함수를 추가합니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + pickPhysicalDevice(); + createLogicalDevice(); +} + +void createLogicalDevice() { + +} +``` + +## 생성할 큐 명시하기 + +논리 장치 생성 과정에는 여러 구조체에 상세 정보를 지정하는 작업이 포함됩니다. 그중 첫 번째는 `VkDeviceQueueCreateInfo`입니다. 이 구조체는 단일 큐 패밀리에 대해 우리가 원하는 큐의 개수를 기술합니다. 지금 당장은 그래픽스 기능이 있는 큐에만 관심이 있습니다. + +```c++ +QueueFamilyIndices indices = findQueueFamilies(physicalDevice); + +VkDeviceQueueCreateInfo queueCreateInfo{}; +queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; +queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value(); +queueCreateInfo.queueCount = 1; +``` + +현재 사용 가능한 드라이버들은 각 큐 패밀리마다 소수의 큐만 생성하도록 허용하며, 실제로 하나보다 더 많이 필요한 경우는 거의 없습니다. 그 이유는 여러 스레드에서 모든 커맨드 버퍼를 생성한 다음, 메인 스레드에서 단 한 번의 오버헤드가 적은 호출로 모두 제출할 수 있기 때문입니다. + +Vulkan에서는 `0.0`에서 `1.0` 사이의 부동 소수점 숫자를 사용하여 큐에 우선순위를 할당하고 커맨드 버퍼 실행 스케줄링에 영향을 줄 수 있습니다. 큐가 하나만 있는 경우에도 이 설정은 필수입니다. + +```c++ +float queuePriority = 1.0f; +queueCreateInfo.pQueuePriorities = &queuePriority; +``` + +## 사용할 장치 기능 명시하기 + +다음으로 명시할 정보는 우리가 사용할 장치 기능의 집합입니다. 이는 이전 장에서 `vkGetPhysicalDeviceFeatures`로 지원 여부를 질의했던 지오메트리 셰이더와 같은 기능들입니다. 지금 당장은 특별한 기능이 필요 없으므로, 구조체를 정의하고 모든 값을 `VK_FALSE`로 두면 됩니다. Vulkan으로 더 흥미로운 작업을 시작할 때 이 구조체로 다시 돌아올 것입니다. + +```c++ +VkPhysicalDeviceFeatures deviceFeatures{}; +``` + +## 논리 장치 생성하기 + +앞선 두 구조체가 준비되었으니, 이제 메인 `VkDeviceCreateInfo` 구조체를 채워나갈 수 있습니다. + +```c++ +VkDeviceCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; +``` + +먼저 큐 생성 정보와 장치 기능 구조체를 가리키는 포인터를 추가합니다. + +```c++ +createInfo.pQueueCreateInfos = &queueCreateInfo; +createInfo.queueCreateInfoCount = 1; + +createInfo.pEnabledFeatures = &deviceFeatures; +``` + +나머지 정보는 `VkInstanceCreateInfo` 구조체와 유사하며, 확장과 유효성 검사 레이어를 명시해야 합니다. 차이점은 이번에는 이것들이 장치에 한정된다는 점입니다. + +장치별 확장의 한 예로 `VK_KHR_swapchain`이 있습니다. 이 확장은 해당 장치에서 렌더링된 이미지를 창에 표시할 수 있게 해줍니다. 시스템에 있는 Vulkan 장치 중에는 이 기능이 없는 경우도 있을 수 있는데, 예를 들어 연산 작업만 지원하는 장치가 그렇습니다. 이 확장에 대해서는 스왑 체인 장에서 다시 다룰 것입니다. + +이전 Vulkan 구현에서는 인스턴스와 장치별 유효성 검사 레이어를 구분했지만, [이제는 그렇지 않습니다](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap40.html#extendingvulkan-layers-devicelayerdeprecation). 이는 최신 구현에서 `VkDeviceCreateInfo`의 `enabledLayerCount`와 `ppEnabledLayerNames` 필드가 무시된다는 의미입니다. 하지만 구버전 구현과의 호환성을 위해 여전히 설정해두는 것이 좋습니다. + +```c++ +createInfo.enabledExtensionCount = 0; + +if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); +} else { + createInfo.enabledLayerCount = 0; +} +``` + +지금은 장치별 확장이 필요하지 않습니다. + +이제 모든 준비가 끝났습니다. 이름에 걸맞게 `vkCreateDevice` 함수를 호출하여 논리 장치를 인스턴스화할 준비가 되었습니다. + +```c++ +if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) { + throw std::runtime_error("failed to create logical device!"); +} +``` + +파라미터는 각각 상호작용할 물리 장치, 방금 명시한 큐 및 사용 정보, 선택적인 할당 콜백 포인터, 그리고 논리 장치 핸들을 저장할 변수를 가리키는 포인터입니다. 인스턴스 생성 함수와 유사하게, 이 호출은 존재하지 않는 확장을 활성화하거나 지원되지 않는 기능의 사용을 명시하는 경우 오류를 반환할 수 있습니다. + +장치는 `cleanup`에서 `vkDestroyDevice` 함수로 소멸시켜야 합니다. + +```c++ +void cleanup() { + vkDestroyDevice(device, nullptr); + ... +} +``` + +논리 장치는 인스턴스와 직접적으로 상호작용하지 않기 때문에, 파라미터로 포함되지 않습니다. + +## 큐 핸들 가져오기 + +큐는 논리 장치와 함께 자동으로 생성되지만, 아직 큐와 상호작용할 핸들을 가지고 있지는 않습니다. 먼저 클래스 멤버를 추가하여 그래픽스 큐의 핸들을 저장하도록 합시다. + +```c++ +VkQueue graphicsQueue; +``` + +장치 큐는 장치가 소멸될 때 암시적으로 정리되므로, `cleanup`에서 따로 처리할 필요가 없습니다. + +`vkGetDeviceQueue` 함수를 사용하여 각 큐 패밀리에 대한 큐 핸들을 가져올 수 있습니다. 파라미터는 논리 장치, 큐 패밀리, 큐 인덱스, 그리고 큐 핸들을 저장할 변수를 가리키는 포인터입니다. 이 패밀리에서는 큐를 하나만 생성하므로, 인덱스는 간단히 `0`을 사용합니다. + +```c++ +vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue); +``` + +이제 논리 장치와 큐 핸들을 사용해 그래픽 카드로 무언가를 실제로 시작할 수 있습니다! 다음 몇 개의 장에 걸쳐, 결과를 창 시스템에 표시하기 위한 리소스를 설정해 보겠습니다. + +[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 new file mode 100644 index 00000000..ce23effa --- /dev/null +++ b/ko/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md @@ -0,0 +1,169 @@ +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; +``` + +`VkSurfaceKHR` 객체와 그 사용법은 플랫폼에 독립적이지만, 그 생성 과정은 윈도우 시스템의 세부 사항에 의존하기 때문에 플랫폼 종속적입니다. 예를 들어, Windows에서는 `HWND`와 `HMODULE` 핸들이 필요합니다. 따라서 플랫폼별 추가 확장이 있으며, Windows에서는 이를 `VK_KHR_win32_surface`라고 부릅니다. 이 확장 역시 `glfwGetRequiredInstanceExtensions`가 반환하는 목록에 자동으로 포함됩니다. + +이 플랫폼별 확장을 사용하여 Windows에서 서피스를 만드는 방법을 보여드리겠지만, 이 튜토리얼에서 실제로 사용하지는 않을 것입니다. GLFW와 같은 라이브러리를 사용하면서 플랫폼 종속적인 코드를 사용하는 것은 이치에 맞지 않기 때문입니다. 사실 GLFW에는 `glfwCreateWindowSurface`라는 함수가 있어 플랫폼 간의 차이점을 알아서 처리해줍니다. 그럼에도 불구하고, GLFW에 의존하기 전에 내부적으로 어떤 작업이 이루어지는지 살펴보는 것은 좋은 경험이 될 것입니다. + +네이티브 플랫폼 함수에 접근하려면 상단의 include 구문을 다음과 같이 수정해야 합니다. + +```c++ +#define VK_USE_PLATFORM_WIN32_KHR +#define GLFW_INCLUDE_VULKAN +#include +#define GLFW_EXPOSE_NATIVE_WIN32 +#include +``` + +윈도우 서피스는 Vulkan 객체이므로, `VkWin32SurfaceCreateInfoKHR` 구조체를 채워야 합니다. 여기에는 `hwnd`와 `hinstance`라는 두 가지 중요한 매개변수가 있습니다. 이들은 각각 창과 프로세스에 대한 핸들입니다. + +```c++ +VkWin32SurfaceCreateInfoKHR createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; +createInfo.hwnd = glfwGetWin32Window(window); +createInfo.hinstance = GetModuleHandle(nullptr); +``` + +`glfwGetWin32Window` 함수는 GLFW 윈도우 객체로부터 원시 `HWND`를 가져오는 데 사용됩니다. `GetModuleHandle` 호출은 현재 프로세스의 `HINSTANCE` 핸들을 반환합니다. + +그 후 `vkCreateWin32SurfaceKHR` 함수로 서피스를 생성할 수 있습니다. 이 함수는 인스턴스, 서피스 생성 정보, 사용자 정의 할당자, 그리고 서피스 핸들을 저장할 변수를 매개변수로 받습니다. 기술적으로 이 함수는 WSI 확장 함수이지만 매우 보편적으로 사용되기 때문에 표준 Vulkan 로더에 포함되어 있습니다. 따라서 다른 확장과 달리 명시적으로 함수 포인터를 로드할 필요가 없습니다. + +```c++ +if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) { + throw std::runtime_error("failed to create window surface!"); +} +``` + +이 과정은 X11을 사용하는 리눅스와 같은 다른 플랫폼에서도 비슷합니다. 리눅스에서는 `vkCreateXcbSurfaceKHR` 함수가 XCB 연결과 윈도우를 생성 정보로 받습니다. + +`glfwCreateWindowSurface` 함수는 각 플랫폼에 맞춰 정확히 이 작업을 수행합니다. 이제 이 함수를 우리 프로그램에 통합해 보겠습니다. `initVulkan` 함수에서 인스턴스 생성과 `setupDebugMessenger` 호출 직후에 호출될 `createSurface` 함수를 추가합니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); +} + +void createSurface() { + +} +``` + +GLFW 호출은 구조체 대신 간단한 매개변수를 받기 때문에 함수 구현이 매우 간단합니다. + +```c++ +void createSurface() { + if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) { + throw std::runtime_error("failed to create window surface!"); + } +} +``` + +매개변수는 `VkInstance`, GLFW 윈도우 포인터, 사용자 정의 할당자, 그리고 `VkSurfaceKHR` 변수를 가리키는 포인터입니다. 이 함수는 각 플랫폼에 맞는 생성 함수의 `VkResult`를 그대로 반환합니다. GLFW는 서피스 파괴를 위한 특별한 함수를 제공하지 않지만, 원래 Vulkan API를 통해 쉽게 처리할 수 있습니다. + +```c++ +void cleanup() { + ... + vkDestroySurfaceKHR(instance, surface, nullptr); + vkDestroyInstance(instance, nullptr); + ... + } +``` + +서피스는 반드시 인스턴스보다 먼저 파괴되어야 한다는 점을 명심하십시오. + +## 프레젠테이션 지원 여부 쿼리 + +Vulkan 구현이 윈도우 시스템 통합을 지원하더라도, 시스템의 모든 장치가 이를 지원한다는 의미는 아닙니다. 따라서 `isDeviceSuitable` 함수를 확장하여 장치가 우리가 생성한 서피스로 이미지를 출력(present)할 수 있는지 확인해야 합니다. 프레젠테이션은 큐에 특화된 기능이므로, 이 문제는 결국 우리가 만든 서피스로의 프레젠테이션을 지원하는 큐 패밀리를 찾는 문제가 됩니다. + +드로잉 커맨드를 지원하는 큐 패밀리와 프레젠테이션을 지원하는 큐 패밀리가 서로 겹치지 않을 수도 있습니다. 따라서 별도의 프레젠테이션 큐가 존재할 수 있다는 점을 고려하여 `QueueFamilyIndices` 구조체를 수정해야 합니다. + +```c++ +struct QueueFamilyIndices { + std::optional graphicsFamily; + std::optional presentFamily; + + bool isComplete() { + return graphicsFamily.has_value() && presentFamily.has_value(); + } +}; +``` + +다음으로, `findQueueFamilies` 함수를 수정하여 우리 윈도우 서피스로 프레젠테이션할 수 있는 큐 패밀리를 찾도록 합니다. 이를 확인하는 함수는 `vkGetPhysicalDeviceSurfaceSupportKHR`이며, 물리 장치, 큐 패밀리 인덱스, 서피스를 매개변수로 받습니다. `VK_QUEUE_GRAPHICS_BIT`를 확인하는 루프 안에서 이 함수를 호출합니다. + +```c++ +VkBool32 presentSupport = false; +vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport); +``` + +그런 다음 이 불리언 값을 확인하고 프레젠테이션 큐 패밀리의 인덱스를 저장합니다. + +```c++ +if (presentSupport) { + indices.presentFamily = i; +} +``` + +이 두 큐 패밀리가 결국 동일한 큐 패밀리로 귀결될 가능성이 매우 높지만, 이 프로그램 전반에 걸쳐 일관된 접근 방식을 위해 서로 다른 큐인 것처럼 다룰 것입니다. 그럼에도 불구하고, 성능 향상을 위해 드로잉과 프레젠테이션을 동일한 큐에서 지원하는 물리 장치를 명시적으로 선호하도록 로직을 추가할 수도 있습니다. + +## 프레젠테이션 큐 생성하기 + +이제 남은 작업은 논리 장치 생성 절차를 수정하여 프레젠테이션 큐를 만들고 `VkQueue` 핸들을 가져오는 것입니다. 큐 핸들을 위한 멤버 변수를 추가합니다. + +```c++ +VkQueue presentQueue; +``` + +다음으로, 두 큐 패밀리로부터 큐를 생성하기 위해 여러 개의 `VkDeviceQueueCreateInfo` 구조체가 필요할 수 있습니다. 이를 우아하게 처리하는 방법은 필요한 큐에 대해 모든 고유한 큐 패밀리의 집합(set)을 만드는 것입니다. + +```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); +} +``` + +그리고 `VkDeviceCreateInfo`가 이 벡터를 가리키도록 수정합니다. + +```c++ +createInfo.queueCreateInfoCount = static_cast(queueCreateInfos.size()); +createInfo.pQueueCreateInfos = queueCreateInfos.data(); +``` + +만약 큐 패밀리가 동일하다면, 우리는 해당 인덱스를 한 번만 전달하게 됩니다. 마지막으로, 큐 핸들을 가져오는 호출을 추가합니다. + +```c++ +vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue); +``` + +만약 그래픽스 큐 패밀리와 프레젠테이션 큐 패밀리가 같다면, 두 큐 핸들(`graphicsQueue`와 `presentQueue`)은 이제 동일한 값을 가질 가능성이 높습니다. 다음 장에서는 스왑 체인(swap chain)에 대해 알아보고, 이를 통해 어떻게 서피스에 이미지를 출력할 수 있는지 살펴보겠습니다. + +[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 new file mode 100644 index 00000000..48e3c3c6 --- /dev/null +++ b/ko/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md @@ -0,0 +1,446 @@ +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 = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME +}; +``` + +다음으로, `isDeviceSuitable`에서 추가 검사로 호출될 새로운 함수 `checkDeviceExtensionSupport`를 만듭니다. + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + QueueFamilyIndices indices = findQueueFamilies(device); + + bool extensionsSupported = checkDeviceExtensionSupport(device); + + return indices.isComplete() && extensionsSupported; +} + +bool checkDeviceExtensionSupport(VkPhysicalDevice device) { + return true; +} +``` + +이제 함수의 본문을 수정하여 확장 기능을 열거하고 필요한 모든 확장이 그 안에 포함되어 있는지 확인합니다. + +```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(); +} +``` + +저는 여기서 아직 확인되지 않은 필수 확장 기능들을 표현하기 위해 문자열 집합(set)을 사용했습니다. 이렇게 하면 사용 가능한 확장 기능 시퀀스를 열거하면서 쉽게 하나씩 지워나갈(tick off) 수 있습니다. 물론 `checkValidationLayerSupport`에서처럼 중첩 루프를 사용할 수도 있습니다. 성능 차이는 무시할 수 있는 수준입니다. 이제 코드를 실행하여 그래픽 카드가 실제로 스왑 체인을 생성할 수 있는지 확인하십시오. 이전 장에서 확인했던 표현 큐(presentation queue)의 가용성은 스왑 체인 확장이 지원되어야 함을 의미합니다. 하지만, 여전히 명시적으로 확인하는 것이 좋으며, 확장은 명시적으로 활성화되어야 합니다. + +## 장치 확장 기능 활성화 + +스왑 체인을 사용하려면 먼저 `VK_KHR_swapchain` 확장을 활성화해야 합니다. 확장 기능을 활성화하려면 논리 장치 생성 구조체를 약간 변경하면 됩니다. + +```c++ +createInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); +createInfo.ppEnabledExtensionNames = deviceExtensions.data(); +``` + +이 코드를 추가할 때 기존의 `createInfo.enabledExtensionCount = 0;` 줄을 교체해야 합니다. + +## 스왑 체인 지원 상세 정보 쿼리 + +스왑 체인이 사용 가능한지 확인하는 것만으로는 충분하지 않습니다. 스왑 체인이 우리 창 표면(window surface)과 호환되지 않을 수 있기 때문입니다. 또한 스왑 체인 생성에는 인스턴스 및 장치 생성보다 훨씬 더 많은 설정이 포함되므로, 계속 진행하기 전에 몇 가지 세부 정보를 더 쿼리해야 합니다. + +기본적으로 세 가지 종류의 속성을 확인해야 합니다. + +* 기본 표면 기능 (스왑 체인의 최소/최대 이미지 수, 이미지의 최소/최대 너비 및 높이) +* 표면 형식 (픽셀 형식, 색 공간) +* 사용 가능한 표현 모드 + +`findQueueFamilies`와 유사하게, 이러한 세부 정보가 쿼리되면 구조체를 사용하여 전달할 것입니다. 위에서 언급한 세 가지 속성 유형은 다음 구조체 및 구조체 목록의 형태로 제공됩니다. + +```c++ +struct SwapChainSupportDetails { + VkSurfaceCapabilitiesKHR capabilities; + std::vector formats; + std::vector presentModes; +}; +``` + +이제 이 구조체를 채울 새로운 함수 `querySwapChainSupport`를 만들겠습니다. + +```c++ +SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) { + SwapChainSupportDetails details; + + return details; +} +``` + +이 섹션에서는 이 정보를 포함하는 구조체를 쿼리하는 방법을 다룹니다. 이 구조체의 의미와 정확히 어떤 데이터가 포함되어 있는지는 다음 섹션에서 논의합니다. + +먼저 기본 표면 기능부터 시작하겠습니다. 이러한 속성은 쿼리하기 간단하며 단일 `VkSurfaceCapabilitiesKHR` 구조체로 반환됩니다. + +```c++ +vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities); +``` + +이 함수는 지원되는 기능을 결정할 때 지정된 `VkPhysicalDevice`와 `VkSurfaceKHR` 창 표면을 고려합니다. 모든 지원 쿼리 함수는 이 두 가지를 첫 번째 매개변수로 사용하는데, 이들이 스왑 체인의 핵심 구성 요소이기 때문입니다. + +다음 단계는 지원되는 표면 형식을 쿼리하는 것입니다. 이것은 구조체 목록이므로, 두 번의 함수 호출이라는 익숙한 절차를 따릅니다. + +```c++ +uint32_t formatCount; +vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr); + +if (formatCount != 0) { + details.formats.resize(formatCount); + vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data()); +} +``` + +벡터가 사용 가능한 모든 형식을 담을 수 있도록 크기가 조정되었는지 확인하십시오. 마지막으로, 지원되는 표현 모드를 쿼리하는 것도 `vkGetPhysicalDeviceSurfacePresentModesKHR`를 사용하여 정확히 동일한 방식으로 작동합니다. + +```c++ +uint32_t presentModeCount; +vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr); + +if (presentModeCount != 0) { + details.presentModes.resize(presentModeCount); + vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data()); +} +``` + +이제 모든 세부 정보가 구조체에 담겼습니다. `isDeviceSuitable`을 한 번 더 확장하여 이 함수를 활용해 스왑 체인 지원이 적절한지 확인합시다. 이 튜토리얼에서는 우리가 가진 창 표면을 고려할 때, 지원되는 이미지 형식이 하나 이상이고 지원되는 표현 모드가 하나 이상이면 스왑 체인 지원은 충분하다고 봅니다. + +```c++ +bool swapChainAdequate = false; +if (extensionsSupported) { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); + swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); +} +``` + +확장 기능이 사용 가능한지 확인한 후에만 스왑 체인 지원을 쿼리하는 것이 중요합니다. 함수의 마지막 줄은 다음과 같이 변경됩니다. + +```c++ +return indices.isComplete() && extensionsSupported && swapChainAdequate; +``` + +## 스왑 체인에 적합한 설정 선택하기 + +`swapChainAdequate` 조건이 충족되면 지원은 확실히 충분하지만, 최적성이 다양한 여러 모드가 있을 수 있습니다. 이제 최상의 스왑 체인을 위한 올바른 설정을 찾는 몇 가지 함수를 작성해 보겠습니다. 결정해야 할 설정에는 세 가지 유형이 있습니다. + +* 표면 형식 (색상 깊이) +* 표현 모드 (이미지를 화면으로 "스왑"하는 조건) +* 스왑 범위 (스왑 체인 이미지의 해상도) + +각 설정에 대해 이상적인 값을 염두에 두고, 사용 가능하다면 그 값을 사용하고, 그렇지 않다면 차선책을 찾는 로직을 만들 것입니다. + +### 표면 형식 + +이 설정을 위한 함수는 다음과 같이 시작합니다. 나중에 `SwapChainSupportDetails` 구조체의 `formats` 멤버를 인자로 전달할 것입니다. + +```c++ +VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { + +} +``` + +각 `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`로 불렸습니다. + +색 공간으로는 SRGB를 사용할 것입니다. [더 정확하게 인식되는 색상을 표현해주기](http://stackoverflow.com/questions/12524623/) 때문입니다. 또한 나중에 사용할 텍스처와 같은 이미지의 표준 색 공간이기도 합니다. 따라서 `VK_FORMAT_B8G8R8A8_SRGB`와 같은 SRGB 색상 형식을 사용하는 것이 좋습니다. + +목록을 살펴보고 선호하는 조합이 사용 가능한지 확인해 봅시다. + +```c++ +for (const auto& availableFormat : availableFormats) { + if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + return availableFormat; + } +} +``` + +이 방법이 실패한다면 사용 가능한 형식들의 "좋음" 정도에 따라 순위를 매길 수도 있지만, 대부분의 경우 목록의 첫 번째 형식을 사용하는 것으로도 충분합니다. + +```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]; +} +``` + +### 표현 모드 + +표현 모드는 스왑 체인에서 가장 중요한 설정이라고 할 수 있습니다. 이미지를 화면에 보여주는 실제 조건을 나타내기 때문입니다. Vulkan에는 네 가지 가능한 모드가 있습니다. + +* `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)"으로 알려져 있지만, 세 개의 버퍼가 존재한다고 해서 반드시 프레임 속도가 제한되지 않는다는 의미는 아닙니다. + +`VK_PRESENT_MODE_FIFO_KHR` 모드만 보장되므로, 사용 가능한 최상의 모드를 찾는 함수를 다시 작성해야 합니다. + +```c++ +VkPresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { + return VK_PRESENT_MODE_FIFO_KHR; +} +``` + +개인적으로 에너지 사용량이 문제가 되지 않는다면 `VK_PRESENT_MODE_MAILBOX_KHR`가 매우 좋은 절충안이라고 생각합니다. 수직 블랭크 직전까지 가능한 한 최신 이미지를 렌더링하여 티어링을 피하면서도 상당히 낮은 지연 시간을 유지할 수 있습니다. 에너지 사용량이 더 중요한 모바일 장치에서는 `VK_PRESENT_MODE_FIFO_KHR`를 사용하는 것이 좋습니다. 이제 목록을 살펴보고 `VK_PRESENT_MODE_MAILBOX_KHR`가 사용 가능한지 확인해 봅시다. + +```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; +} +``` + +### 스왑 범위 + +이제 마지막 주요 속성 하나가 남았으며, 이를 위해 마지막 함수를 하나 더 추가하겠습니다. + +```c++ +VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) { + +} +``` + +스왑 범위(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에 필요 + +... + +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; + } +} +``` + +여기서 `clamp` 함수는 `width`와 `height` 값을 구현체에서 지원하는 허용된 최소 및 최대 범위 사이로 제한하는 데 사용됩니다. + +## 스왑 체인 생성 + +이제 런타임에 내려야 할 선택을 도와주는 모든 헬퍼 함수를 갖추었으므로, 마침내 작동하는 스왑 체인을 만드는 데 필요한 모든 정보를 갖게 되었습니다. + +이러한 호출 결과를 가지고 시작하는 `createSwapChain` 함수를 만들고, 논리 장치를 생성한 후 `initVulkan`에서 이 함수를 호출하도록 합니다. + +```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); +} +``` + +이러한 속성 외에도 스왑 체인에 몇 개의 이미지를 가질지 결정해야 합니다. 구현체는 작동에 필요한 최소 이미지 수를 지정합니다. + +```c++ +uint32_t imageCount = swapChainSupport.capabilities.minImageCount; +``` + +그러나 이 최소값에만 머무르면, 렌더링할 다른 이미지를 얻기 전에 드라이버가 내부 작업을 완료할 때까지 기다려야 할 수 있습니다. 따라서 최소값보다 최소 하나 이상의 이미지를 요청하는 것이 좋습니다. + +```c++ +uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1; +``` + +또한 이 작업을 수행하는 동안 최대 이미지 수를 초과하지 않도록 해야 합니다. 여기서 `0`은 최대값이 없음을 의미하는 특별한 값입니다. + +```c++ +if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) { + imageCount = swapChainSupport.capabilities.maxImageCount; +} +``` + +Vulkan 객체의 전통처럼, 스왑 체인 객체를 생성하려면 큰 구조체를 채워야 합니다. 시작은 매우 익숙합니다. + +```c++ +VkSwapchainCreateInfoKHR createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; +createInfo.surface = surface; +``` + +스왑 체인이 연결될 표면을 지정한 후, 스왑 체인 이미지의 세부 정보를 지정합니다. + +```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; +``` + +`imageArrayLayers`는 각 이미지가 구성되는 레이어의 양을 지정합니다. 입체 3D 애플리케이션을 개발하지 않는 한 항상 `1`입니다. `imageUsage` 비트 필드는 스왑 체인의 이미지를 어떤 종류의 작업에 사용할지를 지정합니다. 이 튜토리얼에서는 이미지에 직접 렌더링할 것이므로, 색상 첨부 파일(color attachment)로 사용됨을 의미합니다. 후처리(post-processing)와 같은 작업을 수행하기 위해 이미지를 별도의 이미지에 먼저 렌더링할 수도 있습니다. 이 경우 `VK_IMAGE_USAGE_TRANSFER_DST_BIT`와 같은 값을 사용하고 메모리 연산을 통해 렌더링된 이미지를 스왑 체인 이미지로 전송할 수 있습니다. + +```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; // 선택 사항 + createInfo.pQueueFamilyIndices = nullptr; // 선택 사항 +} +``` + +다음으로, 여러 큐 패밀리에서 사용될 스왑 체인 이미지를 처리하는 방법을 지정해야 합니다. 우리 애플리케이션에서는 그래픽 큐 패밀리가 표현 큐와 다른 경우에 해당됩니다. 우리는 그래픽 큐에서 스왑 체인의 이미지에 렌더링한 다음 표현 큐에 제출할 것입니다. 여러 큐에서 접근하는 이미지를 처리하는 방법에는 두 가지가 있습니다. + +* `VK_SHARING_MODE_EXCLUSIVE`: 이미지는 한 번에 하나의 큐 패밀리에 의해 소유되며, 다른 큐 패밀리에서 사용하기 전에 명시적으로 소유권을 이전해야 합니다. 이 옵션은 최고의 성능을 제공합니다. +* `VK_SHARING_MODE_CONCURRENT`: 이미지는 명시적인 소유권 이전 없이 여러 큐 패밀리에서 사용될 수 있습니다. + +큐 패밀리가 다른 경우, 이 튜토리얼에서는 동시 모드(concurrent mode)를 사용할 것입니다. 소유권 이전 관련 장을 피하기 위해서인데, 이 내용은 나중에 더 잘 설명될 개념을 포함하기 때문입니다. 동시 모드를 사용하려면 `queueFamilyIndexCount`와 `pQueueFamilyIndices` 매개변수를 사용하여 어떤 큐 패밀리 간에 소유권이 공유될지 미리 지정해야 합니다. 대부분의 하드웨어에서처럼 그래픽 큐 패밀리와 표현 큐 패밀리가 같다면, 배타적 모드(exclusive mode)를 고수해야 합니다. 동시 모드는 최소 두 개의 다른 큐 패밀리를 지정해야 하기 때문입니다. + +```c++ +createInfo.preTransform = swapChainSupport.capabilities.currentTransform; +``` + +`capabilities`의 `supportedTransforms`에서 지원된다면, 90도 시계 방향 회전이나 수평 뒤집기와 같은 특정 변환을 스왑 체인의 이미지에 적용하도록 지정할 수 있습니다. 변환을 원하지 않는다고 지정하려면 현재 변환을 그대로 지정하면 됩니다. + +```c++ +createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; +``` + +`compositeAlpha` 필드는 알파 채널을 창 시스템의 다른 창과 혼합하는 데 사용할지 여부를 지정합니다. 거의 항상 알파 채널을 무시하고 싶을 것이므로 `VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR`를 사용합니다. + +```c++ +createInfo.presentMode = presentMode; +createInfo.clipped = VK_TRUE; +``` + +`presentMode` 멤버는 이름 그대로입니다. `clipped` 멤버가 `VK_TRUE`로 설정되면, 다른 창이 앞에 가려져 보이지 않는 픽셀의 색상은 신경 쓰지 않겠다는 의미입니다. 이러한 픽셀을 다시 읽어 예측 가능한 결과를 얻을 필요가 없다면, 클리핑을 활성화하여 최상의 성능을 얻을 수 있습니다. + +```c++ +createInfo.oldSwapchain = VK_NULL_HANDLE; +``` + +마지막 필드인 `oldSwapchain`이 남았습니다. Vulkan에서는 애플리케이션이 실행되는 동안 스왑 체인이 유효하지 않거나 최적화되지 않을 수 있습니다(예: 창 크기 조정). 이 경우 스왑 체인을 처음부터 다시 만들어야 하며, 이전 스왑 체인에 대한 참조를 이 필드에 지정해야 합니다. 이것은 [추후 장](!en/Drawing_a_triangle/Swap_chain_recreation)에서 더 자세히 배울 복잡한 주제입니다. 지금은 스왑 체인을 한 번만 생성한다고 가정하겠습니다. + +이제 `VkSwapchainKHR` 객체를 저장할 클래스 멤버를 추가합니다. + +```c++ +VkSwapchainKHR swapChain; +``` + +스왑 체인 생성은 이제 `vkCreateSwapchainKHR`를 호출하는 것만큼 간단합니다. + +```c++ +if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) { + throw std::runtime_error("failed to create swap chain!"); +} +``` + +매개변수는 논리 장치, 스왑 체인 생성 정보, 선택적 사용자 지정 할당자, 그리고 핸들을 저장할 변수에 대한 포인터입니다. 놀랄 것은 없습니다. 스왑 체인은 장치보다 먼저 `vkDestroySwapchainKHR`를 사용하여 정리해야 합니다. + +```c++ +void cleanup() { + vkDestroySwapchainKHR(device, swapChain, nullptr); + ... +} +``` + +이제 애플리케이션을 실행하여 스왑 체인이 성공적으로 생성되었는지 확인하십시오! 이 시점에서 `vkCreateSwapchainKHR`에서 접근 위반 오류가 발생하거나 `Failed to find 'vkGetInstanceProcAddress' in layer SteamOverlayVulkanLayer.dll`과 같은 메시지가 표시되면 Steam 오버레이 레이어에 대한 [FAQ 항목](!en/FAQ)을 참조하십시오. + +유효성 검사 레이어를 활성화한 상태에서 `createInfo.imageExtent = extent;` 줄을 제거해 보십시오. 유효성 검사 레이어 중 하나가 즉시 실수를 잡아내고 유용한 메시지를 출력하는 것을 볼 수 있습니다. + +![유효성 검사 레이어가 스왑 체인 생성 오류를 보고하는 이미지](/images/swap_chain_validation_layer.png) + +## 스왑 체인 이미지 가져오기 + +이제 스왑 체인이 생성되었으므로, 남은 일은 스왑 체인에 있는 `VkImage`들의 핸들을 가져오는 것입니다. 이 핸들은 이후 장에서 렌더링 작업 중에 참조할 것입니다. 핸들을 저장할 클래스 멤버를 추가합니다. + +```c++ +std::vector swapChainImages; +``` + +이미지들은 스왑 체인을 위해 구현체에 의해 생성되었으며, 스왑 체인이 파괴되면 자동으로 정리되므로 별도의 정리 코드를 추가할 필요가 없습니다. + +저는 `vkCreateSwapchainKHR` 호출 직후, `createSwapChain` 함수의 끝에 핸들을 가져오는 코드를 추가하겠습니다. 핸들을 가져오는 것은 Vulkan에서 객체 배열을 가져오는 다른 경우와 매우 유사합니다. 우리는 스왑 체인에 최소 이미지 수만 지정했으므로 구현체는 더 많은 이미지를 가진 스왑 체인을 생성할 수 있습니다. 따라서 먼저 `vkGetSwapchainImagesKHR`로 최종 이미지 수를 쿼리한 다음, 컨테이너의 크기를 조정하고, 마지막으로 다시 호출하여 핸들을 가져옵니다. + +```c++ +vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr); +swapChainImages.resize(imageCount); +vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data()); +``` + +마지막으로, 스왑 체인 이미지에 대해 선택한 형식과 범위를 멤버 변수에 저장합니다. 이는 앞으로의 장에서 필요할 것입니다. + +```c++ +VkSwapchainKHR swapChain; +std::vector swapChainImages; +VkFormat swapChainImageFormat; +VkExtent2D swapChainExtent; + +... + +swapChainImageFormat = surfaceFormat.format; +swapChainExtent = extent; +``` + +이제 우리는 렌더링하고 창에 표시할 수 있는 이미지 집합을 갖게 되었습니다. 다음 장에서는 이미지를 렌더링 대상으로 설정하는 방법을 다루고, 그 다음 실제 그래픽 파이프라인과 그리기 명령에 대해 알아보기 시작하겠습니다! + +[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 new file mode 100644 index 00000000..5167e037 --- /dev/null +++ b/ko/03_Drawing_a_triangle/01_Presentation/02_Image_views.md @@ -0,0 +1,104 @@ +스왑 체인에 있는 이미지를 포함한 모든 `VkImage`를 렌더 파이프라인에서 사용하려면 `VkImageView` 객체를 생성해야 합니다. 이미지 뷰는 말 그대로 이미지로의 뷰(view)입니다. 이는 이미지에 접근하는 방법과 접근할 이미지의 부분을 기술합니다. 예를 들어, 밉매핑 레벨이 없는 2D 텍스처나 깊이 텍스처로 취급해야 하는지 등을 명시합니다. + +이번 장에서는 스왑 체인의 모든 이미지에 대한 기본적인 이미지 뷰를 만드는 `createImageViews` 함수를 작성할 것입니다. 이렇게 하면 나중에 이미지 뷰들을 컬러 타겟으로 사용할 수 있습니다. + +먼저, 이미지 뷰를 저장할 클래스 멤버를 추가합니다. + +```c++ +std::vector swapChainImageViews; +``` + +`createImageViews` 함수를 만들고 스왑 체인 생성 직후에 호출하도록 합니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); +} + +void createImageViews() { + +} +``` + +가장 먼저 할 일은 생성할 모든 이미지 뷰를 담을 수 있도록 리스트의 크기를 조절하는 것입니다. + +```c++ +void createImageViews() { + swapChainImageViews.resize(swapChainImages.size()); + +} +``` + +다음으로, 스왑 체인의 모든 이미지를 순회하는 루프를 설정합니다. + +```c++ +for (size_t i = 0; i < swapChainImages.size(); i++) { + +} +``` + +이미지 뷰 생성을 위한 파라미터들은 `VkImageViewCreateInfo` 구조체에 명시됩니다. 처음 몇 개의 파라미터는 간단합니다. + +```c++ +VkImageViewCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; +createInfo.image = swapChainImages[i]; +``` + +`viewType`과 `format` 필드는 이미지 데이터를 어떻게 해석할지를 지정합니다. `viewType` 파라미터를 사용하면 이미지를 1D 텍스처, 2D 텍스처, 3D 텍스처, 큐브 맵으로 다룰 수 있습니다. + +```c++ +createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; +createInfo.format = swapChainImageFormat; +``` + +`components` 필드를 사용하면 컬러 채널을 스위즐(swizzle)할 수 있습니다. 예를 들어, 단색 텍스처를 위해 모든 채널을 빨간색 채널에 매핑할 수 있습니다. 또한 `0`이나 `1`과 같은 상수 값을 채널에 매핑할 수도 있습니다. 우리의 경우에는 기본 매핑을 사용할 것입니다. + +```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; +``` + +`subresourceRange` 필드는 이미지의 용도와 접근할 이미지의 부분을 기술합니다. 우리의 이미지는 밉매핑 레벨이나 여러 레이어 없이 컬러 타겟으로 사용될 것입니다. + +```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; +``` + +만약 입체 3D 애플리케이션을 작업한다면, 여러 레이어를 가진 스왑 체인을 생성할 것입니다. 그런 다음 각 이미지에 대해 여러 이미지 뷰를 생성하여, 서로 다른 레이어에 접근함으로써 왼쪽 눈과 오른쪽 눈에 대한 뷰를 표현할 수 있습니다. + +이제 `vkCreateImageView`를 호출하여 이미지 뷰를 생성하기만 하면 됩니다. + +```c++ +if (vkCreateImageView(device, &createInfo, nullptr, &swapChainImageViews[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create image views!"); +} +``` + +이미지와 달리, 이미지 뷰는 우리가 명시적으로 생성했으므로, 프로그램이 끝날 때 이를 다시 파괴하기 위한 비슷한 루프를 추가해야 합니다. + +```c++ +void cleanup() { + for (auto imageView : swapChainImageViews) { + vkDestroyImageView(device, imageView, nullptr); + } + + ... +} +``` + +이미지 뷰는 이미지를 텍스처로 사용하기 시작하기에는 충분하지만, 렌더 타겟으로 사용되기에는 아직 완전히 준비되지 않았습니다. 이를 위해서는 프레임버퍼(framebuffer)라고 알려진 한 단계의 간접(indirection) 과정이 더 필요합니다. 하지만 그 전에 그래픽 파이프라인을 먼저 설정해야 합니다. + +[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 new file mode 100644 index 00000000..7af1d48a --- /dev/null +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md @@ -0,0 +1,50 @@ +앞으로 몇 개의 챕터에 걸쳐 첫 번째 삼각형을 그리기 위해 구성된 그래픽스 파이프라인을 설정할 것입니다. 그래픽스 파이프라인은 메시의 정점(vertices)과 텍스처(textures)를 가져와 렌더 타겟의 픽셀에 이르는 일련의 작업 순서입니다. 아래에 간소화된 개요가 표시되어 있습니다: + +![](/images/vulkan_simplified_pipeline.svg) + +*입력 조립기(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() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createGraphicsPipeline(); +} + +... + +void createGraphicsPipeline() { + +} +``` + +[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 new file mode 100644 index 00000000..ed62b75b --- /dev/null +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md @@ -0,0 +1,331 @@ +이전 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) + +이전에 컴퓨터 그래픽스를 다뤄본 적이 있다면 이 좌표계에 이미 익숙할 것입니다. 만약 이전에 OpenGL을 사용해봤다면, Y 좌표의 부호가 뒤집힌 것을 알 수 있습니다. Z 좌표는 이제 Direct3D에서처럼 0에서 1까지의 범위를 사용합니다. + +첫 번째 삼각형에서는 어떠한 변환도 적용하지 않을 것입니다. 대신 세 정점의 위치를 정규화된 디바이스 좌표로 직접 명시하여 다음 모양을 만들 것입니다. + +![](/images/triangle_coordinates.svg) + +버텍스 셰이더에서 마지막 구성 요소를 `1`로 설정하여 클립 좌표로 출력함으로써 정규화된 디바이스 좌표를 직접 출력할 수 있습니다. 이렇게 하면 클립 좌표를 정규화된 디바이스 좌표로 변환하는 나눗셈 과정에서 아무것도 변하지 않게 됩니다. + +일반적으로 이 좌표들은 버텍스 버퍼에 저장되지만, 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); +} +``` + +`main` 함수는 모든 정점에 대해 호출됩니다. 내장 변수인 `gl_VertexIndex`는 현재 정점의 인덱스를 포함합니다. 이는 보통 버텍스 버퍼의 인덱스이지만, 우리 경우에는 하드코딩된 정점 데이터 배열의 인덱스가 될 것입니다. 각 정점의 위치는 셰이더 내의 상수 배열에서 접근되며, 더미 `z`와 `w` 구성 요소와 결합하여 클립 좌표에서의 위치를 생성합니다. 내장 변수인 `gl_Position`이 출력으로 사용됩니다. + +## 프래그먼트 셰이더 + +버텍스 셰이더의 위치로 형성된 삼각형은 화면의 한 영역을 프래그먼트(fragment)로 채웁니다. 프래그먼트 셰이더는 이 프래그먼트들에 대해 호출되어 프레임버퍼(들)를 위한 색상과 깊이 값을 생성합니다. 전체 삼각형에 대해 빨간색을 출력하는 간단한 프래그먼트 셰이더는 다음과 같습니다. + +```glsl +#version 450 + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(1.0, 0.0, 0.0, 1.0); +} +``` + +`main` 함수는 버텍스 셰이더의 `main` 함수가 모든 정점에 대해 호출되는 것처럼 모든 프래그먼트에 대해 호출됩니다. GLSL에서 색상은 [0, 1] 범위의 R, G, B, 알파 채널을 가진 4-요소 벡터입니다. 버텍스 셰이더의 `gl_Position`과 달리, 현재 프래그먼트의 색상을 출력하기 위한 내장 변수는 없습니다. 각 프레임버퍼에 대해 자신만의 출력 변수를 지정해야 하며, `layout(location = 0)` 지정자는 프레임버퍼의 인덱스를 명시합니다. 빨간색은 인덱스 `0`에 있는 첫 번째 (그리고 유일한) 프레임버퍼에 연결된 `outColor` 변수에 기록됩니다. + +## 정점별 색상 + +삼각형 전체를 빨갛게 만드는 것은 별로 흥미롭지 않습니다. 다음과 같은 모양이 훨씬 더 멋지지 않을까요? + +![](/images/triangle_coordinates_colors.png) + +이를 달성하기 위해 두 셰이더 모두에 몇 가지 변경을 해야 합니다. 먼저, 세 정점 각각에 대해 고유한 색상을 지정해야 합니다. 버텍스 셰이더는 이제 위치와 마찬가지로 색상 배열을 포함해야 합니다. + +```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) +); +``` + +이제 이 정점별 색상을 프래그먼트 셰이더로 전달하여 보간된 값을 프레임버퍼에 출력하도록 해야 합니다. 버텍스 셰이더에 색상 출력을 추가하고 `main` 함수에서 값을 기록합니다. + +```glsl +layout(location = 0) out vec3 fragColor; + +void main() { + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); + fragColor = colors[gl_VertexIndex]; +} +``` + +다음으로, 프래그먼트 셰이더에 일치하는 입력을 추가해야 합니다. + +```glsl +layout(location = 0) in vec3 fragColor; + +void main() { + outColor = vec4(fragColor, 1.0); +} +``` + +입력 변수는 반드시 같은 이름을 사용할 필요는 없으며, `location` 지시자에 의해 지정된 인덱스를 사용하여 연결됩니다. `main` 함수는 알파 값과 함께 색상을 출력하도록 수정되었습니다. 위 이미지에서 볼 수 있듯이, `fragColor`의 값들은 세 정점 사이의 프래그먼트들에 대해 자동으로 보간되어, 부드러운 그라데이션을 만들어냅니다. + +## 셰이더 컴파일하기 + +프로젝트의 루트 디렉토리에 `shaders`라는 디렉토리를 만들고, 버텍스 셰이더를 `shader.vert` 파일에, 프래그먼트 셰이더를 `shader.frag` 파일에 저장하십시오. GLSL 셰이더에는 공식적인 확장자가 없지만, 이 두 확장자는 셰이더를 구별하기 위해 일반적으로 사용됩니다. + +`shader.vert`의 내용은 다음과 같아야 합니다. + +```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]; +} +``` + +그리고 `shader.frag`의 내용은 다음과 같아야 합니다. + +```glsl +#version 450 + +layout(location = 0) in vec3 fragColor; + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(fragColor, 1.0); +} +``` + +이제 `glslc` 프로그램을 사용하여 이들을 SPIR-V 바이트코드로 컴파일할 것입니다. + +**Windows** + +다음 내용으로 `compile.bat` 파일을 만드십시오. + +```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 +``` + +`glslc.exe`의 경로를 여러분이 Vulkan SDK를 설치한 경로로 교체하십시오. 파일을 더블 클릭하여 실행합니다. + +**Linux** + +다음 내용으로 `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 +``` + +`glslc`의 경로를 여러분이 Vulkan SDK를 설치한 경로로 교체하십시오. `chmod +x compile.sh`로 스크립트를 실행 가능하게 만들고 실행합니다. + +**플랫폼별 지침 끝** + +이 두 명령어는 컴파일러에게 GLSL 소스 파일을 읽고 `-o`(output) 플래그를 사용하여 SPIR-V 바이트코드 파일을 출력하도록 지시합니다. + +셰이더에 문법 오류가 있으면 컴파일러가 예상대로 줄 번호와 문제를 알려줄 것입니다. 예를 들어 세미콜론을 하나 빼고 컴파일 스크립트를 다시 실행해보십시오. 또한 아무 인수 없이 컴파일러를 실행하여 어떤 종류의 플래그를 지원하는지 확인해볼 수도 있습니다. 예를 들어, 바이트코드를 사람이 읽을 수 있는 형식으로 출력하여 셰이더가 정확히 무엇을 하는지, 이 단계에서 어떤 최적화가 적용되었는지 볼 수도 있습니다. + +명령줄에서 셰이더를 컴파일하는 것은 가장 간단한 옵션 중 하나이며 이 튜토리얼에서 사용할 방법이지만, 자신의 코드에서 직접 셰이더를 컴파일하는 것도 가능합니다. Vulkan SDK에는 프로그램 내에서 GLSL 코드를 SPIR-V로 컴파일하는 라이브러리인 [libshaderc](https://github.com/google/shaderc)가 포함되어 있습니다. + +## 셰이더 로드하기 + +이제 SPIR-V 셰이더를 생성하는 방법을 알았으니, 이를 프로그램에 로드하여 그래픽 파이프라인의 특정 지점에 연결할 시간입니다. 먼저 파일에서 바이너리 데이터를 로드하는 간단한 헬퍼 함수를 작성하겠습니다. + +```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!"); + } +} +``` + +`readFile` 함수는 지정된 파일의 모든 바이트를 읽어 `std::vector`가 관리하는 바이트 배열로 반환합니다. 다음 두 플래그를 사용하여 파일을 엽니다. + +* `ate`: 파일의 끝에서 읽기 시작 +* `binary`: 파일을 바이너리 파일로 읽기 (텍스트 변환 방지) + +파일의 끝에서 읽기를 시작하는 것의 장점은 읽기 위치를 사용하여 파일 크기를 결정하고 버퍼를 할당할 수 있다는 것입니다. + +```c++ +size_t fileSize = (size_t) file.tellg(); +std::vector buffer(fileSize); +``` + +그런 다음, 파일의 시작 부분으로 다시 이동하여 모든 바이트를 한 번에 읽을 수 있습니다. + +```c++ +file.seekg(0); +file.read(buffer.data(), fileSize); +``` + +마지막으로 파일을 닫고 바이트를 반환합니다. + +```c++ +file.close(); + +return buffer; +``` + +이제 `createGraphicsPipeline` 함수에서 이 함수를 호출하여 두 셰이더의 바이트코드를 로드합니다. + +```c++ +void createGraphicsPipeline() { + auto vertShaderCode = readFile("shaders/vert.spv"); + auto fragShaderCode = readFile("shaders/frag.spv"); +} +``` + +버퍼의 크기를 출력하여 실제 파일 크기(바이트 단위)와 일치하는지 확인하여 셰이더가 올바르게 로드되었는지 확인하십시오. 코드는 바이너리 코드이므로 null로 끝나지 않아도 되며, 나중에 코드의 크기를 명시적으로 지정할 것입니다. + +## 셰이더 모듈 생성하기 + +코드를 파이프라인에 전달하기 전에 `VkShaderModule` 객체로 감싸야 합니다. 이를 위해 `createShaderModule`이라는 헬퍼 함수를 만들어 보겠습니다. + +```c++ +VkShaderModule createShaderModule(const std::vector& code) { + +} +``` + +이 함수는 바이트코드가 담긴 버퍼를 파라미터로 받아 `VkShaderModule`을 생성합니다. + +셰이더 모듈을 만드는 것은 간단합니다. 바이트코드가 있는 버퍼에 대한 포인터와 그 길이를 지정하기만 하면 됩니다. 이 정보는 `VkShaderModuleCreateInfo` 구조체에 명시됩니다. 한 가지 주의할 점은 바이트코드의 크기는 바이트 단위로 지정되지만, 바이트코드 포인터는 `char` 포인터가 아닌 `uint32_t` 포인터라는 것입니다. 따라서 아래와 같이 `reinterpret_cast`를 사용하여 포인터를 캐스팅해야 합니다. 이와 같은 캐스팅을 수행할 때는 데이터가 `uint32_t`의 정렬(alignment) 요구 사항을 만족하는지 확인해야 합니다. 다행히도, 데이터는 `std::vector`에 저장되며, 기본 할당자는 이미 데이터가 최악의 경우의 정렬 요구 사항을 만족하도록 보장합니다. + +```c++ +VkShaderModuleCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; +createInfo.codeSize = code.size(); +createInfo.pCode = reinterpret_cast(code.data()); +``` + +`VkShaderModule`은 `vkCreateShaderModule` 호출로 생성할 수 있습니다. + +```c++ +VkShaderModule shaderModule; +if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) { + throw std::runtime_error("failed to create shader module!"); +} +``` + +파라미터는 이전 객체 생성 함수들과 동일합니다: 논리 장치, 생성 정보 구조체 포인터, 사용자 정의 할당자(선택 사항) 포인터, 핸들 출력 변수입니다. 코드가 담긴 버퍼는 셰이더 모듈을 생성한 직후에 해제할 수 있습니다. 생성된 셰이더 모듈을 반환하는 것을 잊지 마십시오. + +```c++ +return shaderModule; +``` + +셰이더 모듈은 우리가 이전에 파일에서 로드한 셰이더 바이트코드와 그 안에 정의된 함수들을 얇게 감싼 래퍼일 뿐입니다. SPIR-V 바이트코드를 GPU가 실행할 수 있는 기계어 코드로 컴파일하고 링크하는 작업은 그래픽 파이프라인이 생성될 때까지 일어나지 않습니다. 즉, 파이프라인 생성이 완료되는 즉시 셰이더 모듈을 다시 파괴해도 괜찮다는 의미입니다. 따라서 클래스 멤버가 아닌 `createGraphicsPipeline` 함수의 지역 변수로 만들 것입니다. + +```c++ +void createGraphicsPipeline() { + auto vertShaderCode = readFile("shaders/vert.spv"); + auto fragShaderCode = readFile("shaders/frag.spv"); + + VkShaderModule vertShaderModule = createShaderModule(vertShaderCode); + VkShaderModule fragShaderModule = createShaderModule(fragShaderCode); +``` + +정리는 함수의 끝에서 `vkDestroyShaderModule`에 대한 두 번의 호출을 추가하여 이루어져야 합니다. 이 장의 나머지 모든 코드는 이 두 줄 앞에 삽입될 것입니다. + +```c++ + ... + vkDestroyShaderModule(device, fragShaderModule, nullptr); + vkDestroyShaderModule(device, vertShaderModule, nullptr); +} +``` + +## 셰이더 스테이지 생성 + +셰이더를 실제로 사용하려면 실제 파이프라인 생성 과정의 일부로서 `VkPipelineShaderStageCreateInfo` 구조체를 통해 특정 파이프라인 단계에 할당해야 합니다. + +`createGraphicsPipeline` 함수에서 버텍스 셰이더를 위한 구조체를 채우는 것으로 시작하겠습니다. + +```c++ +VkPipelineShaderStageCreateInfo vertShaderStageInfo{}; +vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; +vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; +``` + +필수적인 `sType` 멤버 외에 첫 번째 단계는 Vulkan에게 셰이더가 사용될 파이프라인 단계를 알려주는 것입니다. 이전 장에서 설명한 프로그래밍 가능한 각 단계에 대한 열거형 값이 있습니다. + +```c++ +vertShaderStageInfo.module = vertShaderModule; +vertShaderStageInfo.pName = "main"; +``` + +다음 두 멤버는 코드를 포함하는 셰이더 모듈과 호출할 함수, 즉 *엔트리포인트(entrypoint)*를 지정합니다. 이는 여러 프래그먼트 셰이더를 단일 셰이더 모듈로 결합하고 다른 엔트리포인트를 사용하여 그들의 동작을 구별할 수 있음을 의미합니다. 하지만 이 경우에는 표준적인 `main`을 사용할 것입니다. + +여기서는 사용하지 않지만 논의할 가치가 있는 `pSpecializationInfo`라는 또 다른 (선택적) 멤버가 있습니다. 이 멤버를 사용하면 셰이더 상수에 대한 값을 지정할 수 있습니다. 단일 셰이더 모듈을 사용하면서 파이프라인 생성 시 셰이더에 사용된 상수에 대해 다른 값을 지정하여 동작을 구성할 수 있습니다. 이는 렌더링 시 변수를 사용하여 셰이더를 구성하는 것보다 더 효율적인데, 왜냐하면 컴파일러가 이러한 값에 의존하는 `if` 문을 제거하는 것과 같은 최적화를 수행할 수 있기 때문입니다. 만약 그런 상수가 없다면, 멤버를 `nullptr`로 설정할 수 있으며, 우리 구조체 초기화가 자동으로 그렇게 합니다. + +프래그먼트 셰이더에 맞게 구조체를 수정하는 것은 쉽습니다. + +```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"; +``` + +마지막으로 이 두 구조체를 포함하는 배열을 정의합니다. 이 배열은 나중에 실제 파이프라인 생성 단계에서 이들을 참조하는 데 사용될 것입니다. + +```c++ +VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; +``` + +이것으로 파이프라인의 프로그래밍 가능 단계를 기술하는 작업이 모두 끝났습니다. 다음 장에서는 고정 함수 단계를 살펴보겠습니다. + +[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 new file mode 100644 index 00000000..963b7836 --- /dev/null +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md @@ -0,0 +1,323 @@ +이전의 그래픽 API들은 그래픽 파이프라인의 대부분 단계에 대한 기본 상태를 제공했습니다. Vulkan에서는 대부분의 파이프라인 상태를 명시적으로 지정해야 하며, 이 상태들은 불변(immutable)의 파이프라인 상태 객체(PSO)로 구워지기(baked) 때문입니다. 이번 장에서는 이러한 고정 함수(fixed-function) 연산을 구성하기 위한 모든 구조체를 채워 넣을 것입니다. + +## 동적 상태 (Dynamic state) + +*대부분의* 파이프라인 상태는 파이프라인 상태 객체에 구워져야 하지만, 제한된 일부 상태는 파이프라인을 다시 만들지 않고도 드로우 타임(draw time)에 변경할 수 *있습니다*. 뷰포트의 크기, 선 두께, 블렌딩 상수 등이 그 예입니다. 만약 동적 상태를 사용하고 이러한 속성들을 파이프라인 생성 시에 고정하지 않으려면, 다음과 같이 `VkPipelineDynamicStateCreateInfo` 구조체를 채워야 합니다. + +```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(); +``` + +이렇게 하면 이 값들의 구성이 파이프라인 생성 시에는 무시되며, 드로잉 시점에 이 데이터를 지정할 수 있게 되고 또 지정해야만 합니다. 이는 더 유연한 설정을 가능하게 하며, 뷰포트나 시저 상태처럼 파이프라인 상태에 고정시킬 경우 설정이 더 복잡해질 수 있는 항목들에 대해 매우 일반적인 방식입니다. + +## 정점 입력 (Vertex input) + +`VkPipelineVertexInputStateCreateInfo` 구조체는 정점 셰이더로 전달될 정점 데이터의 형식을 설명합니다. 이 설명은 크게 두 가지 방식으로 이루어집니다. + +* **바인딩(Bindings)**: 데이터 간의 간격 및 데이터가 정점별(per-vertex)인지 인스턴스별(per-instance)인지 여부 (자세한 내용은 [인스턴싱](https://ko.wikipedia.org/wiki/인스턴싱) 참조) +* **속성 서술(Attribute descriptions)**: 정점 셰이더로 전달되는 속성의 유형, 어떤 바인딩에서 로드할지, 그리고 어떤 오프셋에 있는지 + +지금은 정점 데이터를 정점 셰이더에 직접 하드코딩하고 있으므로, 이 구조체를 채워서 로드할 정점 데이터가 없음을 명시할 것입니다. 이 부분은 정점 버퍼(vertex buffer) 장에서 다시 다룰 것입니다. + +```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 +``` + +`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{}; +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) + +뷰포트(viewport)는 기본적으로 출력이 렌더링될 프레임버퍼의 영역을 설명합니다. 이는 거의 항상 `(0, 0)`에서 `(width, height)`까지이며, 이 튜토리얼에서도 마찬가지입니다. + +```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; +``` + +스왑체인과 그 이미지들의 크기는 창의 `WIDTH`와 `HEIGHT`와 다를 수 있음을 기억하세요. 스왑체인 이미지들은 나중에 프레임버퍼로 사용될 것이므로, 그 크기를 따라야 합니다. + +`minDepth`와 `maxDepth` 값은 프레임버퍼에 사용할 깊이 값의 범위를 지정합니다. 이 값들은 `[0.0f, 1.0f]` 범위 내에 있어야 하지만, `minDepth`가 `maxDepth`보다 클 수도 있습니다. 특별한 작업을 하지 않는다면 표준 값인 `0.0f`와 `1.0f`를 사용해야 합니다. + +뷰포트가 이미지에서 프레임버퍼로의 변환을 정의하는 반면, 시저 사각형(scissor rectangle)은 실제로 픽셀이 저장될 영역을 정의합니다. 시저 사각형 밖의 모든 픽셀은 래스터라이저에 의해 버려집니다. 이는 변환이라기보다는 필터처럼 작동합니다. 차이점은 아래 그림에 설명되어 있습니다. 왼쪽 시저 사각형은 뷰포트보다 크기만 하다면 해당 이미지를 만들어내는 많은 가능성 중 하나일 뿐입니다. + +![](/images/viewports_scissors.png) + +따라서 전체 프레임버퍼에 그리고 싶다면, 프레임버퍼 전체를 덮는 시저 사각형을 지정하면 됩니다. + +```c++ +VkRect2D scissor{}; +scissor.offset = {0, 0}; +scissor.extent = swapChainExtent; +``` + +뷰포트와 시저 사각형은 파이프라인의 정적(static) 부분으로 지정하거나, 커맨드 버퍼에서 설정되는 [동적 상태](#동적-상태)로 지정할 수 있습니다. 전자가 다른 상태들과 더 일관성이 있지만, 뷰포트와 시저 상태를 동적으로 만드는 것이 훨씬 더 많은 유연성을 제공하기 때문에 종종 편리합니다. 이는 매우 일반적인 방식이며 모든 구현체에서 성능 저하 없이 이 동적 상태를 처리할 수 있습니다. + +동적 뷰포트와 시저 사각형을 선택하는 경우, 파이프라인에 대해 해당 동적 상태를 활성화해야 합니다. + +```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(); +``` + +그런 다음 파이프라인 생성 시에는 그들의 개수만 지정하면 됩니다. + +```c++ +VkPipelineViewportStateCreateInfo viewportState{}; +viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; +viewportState.viewportCount = 1; +viewportState.scissorCount = 1; +``` + +실제 뷰포트와 시저 사각형은 나중에 드로잉 시점에 설정될 것입니다. 동적 상태를 사용하면 단일 커맨드 버퍼 내에서 다른 뷰포트 및/또는 시저 사각형을 지정하는 것조차 가능합니다. + +동적 상태를 사용하지 않는다면, 뷰포트와 시저 사각형은 `VkPipelineViewportStateCreateInfo` 구조체를 사용하여 파이프라인에 설정되어야 합니다. 이는 이 파이프라인의 뷰포트와 시저 사각형을 불변으로 만듭니다. 이 값들을 변경하려면 새 값으로 새 파이프라인을 생성해야 합니다. + +```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; +``` + +어떻게 설정하든, 일부 그래픽 카드에서는 여러 개의 뷰포트와 시저 사각형을 사용할 수 있으며, 이를 위해 구조체 멤버들이 배열을 참조합니다. 여러 개를 사용하려면 GPU 기능을 활성화해야 합니다(논리 장치 생성 참조). + +## 래스터라이저 (Rasterizer) + +래스터라이저는 정점 셰이더에서 만들어진 지오메트리를 가져와 프래그먼트 셰이더에서 색상을 칠할 프래그먼트(fragment)로 변환합니다. 또한 [깊이 테스팅](https://ko.wikipedia.org/wiki/Z-버퍼링), [면 컬링](https://ko.wikipedia.org/wiki/후면_추려내기), 시저 테스트를 수행하며, 폴리곤 전체를 채우는 프래그먼트나 가장자리만(와이어프레임 렌더링) 출력하도록 구성할 수 있습니다. 이 모든 것은 `VkPipelineRasterizationStateCreateInfo` 구조체를 사용하여 구성됩니다. + +```c++ +VkPipelineRasterizationStateCreateInfo rasterizer{}; +rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; +rasterizer.depthClampEnable = VK_FALSE; +``` + +`depthClampEnable`이 `VK_TRUE`로 설정되면, 근평면(near plane)과 원평면(far plane)을 벗어나는 프래그먼트는 버려지는 대신 평면에 클램핑됩니다. 이는 섀도우 맵과 같은 일부 특수한 경우에 유용합니다. 이를 사용하려면 GPU 기능을 활성화해야 합니다. + +```c++ +rasterizer.rasterizerDiscardEnable = VK_FALSE; +``` + +`rasterizerDiscardEnable`이 `VK_TRUE`로 설정되면, 지오메트리는 래스터라이저 단계를 통과하지 않습니다. 이는 기본적으로 프레임버퍼로의 모든 출력을 비활성화합니다. + +```c++ +rasterizer.polygonMode = VK_POLYGON_MODE_FILL; +``` + +`polygonMode`는 지오메트리에 대해 프래그먼트가 생성되는 방식을 결정합니다. 사용 가능한 모드는 다음과 같습니다. + +* `VK_POLYGON_MODE_FILL`: 폴리곤 영역을 프래그먼트로 채움 +* `VK_POLYGON_MODE_LINE`: 폴리곤 가장자리를 선으로 그림 +* `VK_POLYGON_MODE_POINT`: 폴리곤 정점을 점으로 그림 + +`fill` 이외의 모드를 사용하려면 GPU 기능을 활성화해야 합니다. + +```c++ +rasterizer.lineWidth = 1.0f; +``` + +`lineWidth` 멤버는 직관적으로, 프래그먼트 개수 단위로 선의 두께를 나타냅니다. 지원되는 최대 선 두께는 하드웨어에 따라 다르며, `1.0f`보다 두꺼운 선을 사용하려면 `wideLines` GPU 기능을 활성화해야 합니다. + +```c++ +rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; +rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE; +``` + +`cullMode` 변수는 사용할 면 컬링(face culling)의 유형을 결정합니다. 컬링을 비활성화하거나, 앞면(front face)을 컬링하거나, 뒷면(back face)을 컬링하거나, 둘 다 컬링할 수 있습니다. `frontFace` 변수는 앞면으로 간주될 면의 정점 순서를 지정하며, 시계 방향(clockwise) 또는 반시계 방향(counter-clockwise)이 될 수 있습니다. + +```c++ +rasterizer.depthBiasEnable = VK_FALSE; +rasterizer.depthBiasConstantFactor = 0.0f; // Optional +rasterizer.depthBiasClamp = 0.0f; // Optional +rasterizer.depthBiasSlopeFactor = 0.0f; // Optional +``` + +래스터라이저는 상수 값을 더하거나 프래그먼트의 기울기에 따라 깊이 값을 변경할 수 있습니다. 이는 때때로 섀도우 매핑에 사용되지만, 우리는 사용하지 않을 것입니다. `depthBiasEnable`을 `VK_FALSE`로 설정하세요. + +## 멀티샘플링 (Multisampling) + +`VkPipelineMultisampleStateCreateInfo` 구조체는 [안티 앨리어싱](https://ko.wikipedia.org/wiki/다중샘플링_안티에일리어싱)을 수행하는 방법 중 하나인 멀티샘플링을 구성합니다. 동일한 픽셀로 래스터화되는 여러 폴리곤의 프래그먼트 셰이더 결과를 결합하여 작동합니다. 이는 주로 가장자리에서 발생하며, 가장 눈에 띄는 앨리어싱 현상이 발생하는 곳이기도 합니다. 하나의 폴리곤만 픽셀에 매핑되는 경우에는 프래그먼트 셰이더를 여러 번 실행할 필요가 없기 때문에, 단순히 더 높은 해상도로 렌더링한 다음 다운스케일링하는 것보다 훨씬 비용이 적게 듭니다. 이를 활성화하려면 GPU 기능을 활성화해야 합니다. + +```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 +``` + +멀티샘플링은 나중 장에서 다시 다룰 것이므로, 지금은 비활성화 상태로 두겠습니다. + +## 깊이 및 스텐실 테스팅 (Depth and stencil testing) + +깊이 및/또는 스텐실 버퍼를 사용하는 경우, `VkPipelineDepthStencilStateCreateInfo`를 사용하여 깊이 및 스텐실 테스트도 구성해야 합니다. 지금은 없으므로 해당 구조체에 대한 포인터 대신 단순히 `nullptr`를 전달할 수 있습니다. 이 부분은 깊이 버퍼링 장에서 다시 다루겠습니다. + +## 색상 혼합 (Color blending) + +프래그먼트 셰이더가 색상을 반환한 후, 프레임버퍼에 이미 있는 색상과 결합되어야 합니다. 이 변환은 색상 혼합(color blending)으로 알려져 있으며, 두 가지 방법이 있습니다. + +* 이전 값과 새 값을 혼합하여 최종 색상을 생성 +* 비트 연산을 사용하여 이전 값과 새 값을 결합 + +색상 혼합을 구성하는 데는 두 가지 유형의 구조체가 있습니다. 첫 번째 구조체인 `VkPipelineColorBlendAttachmentState`는 연결된 각 프레임버퍼별 구성을 포함하고, 두 번째 구조체인 `VkPipelineColorBlendStateCreateInfo`는 *전역* 색상 혼합 설정을 포함합니다. 우리의 경우 프레임버퍼는 하나뿐입니다. + +```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 +``` + +이 프레임버퍼별 구조체를 사용하면 첫 번째 방식의 색상 혼합을 구성할 수 있습니다. 수행될 연산은 다음 의사 코드로 가장 잘 설명할 수 있습니다. + +```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; +``` + +`blendEnable`이 `VK_FALSE`로 설정되면 프래그먼트 셰이더의 새 색상이 수정 없이 그대로 전달됩니다. 그렇지 않으면 두 혼합 연산이 수행되어 새 색상을 계산합니다. 결과 색상은 `colorWriteMask`와 AND 연산되어 실제로 어떤 채널이 통과할지 결정됩니다. + +색상 혼합을 사용하는 가장 일반적인 방법은 알파 블렌딩을 구현하는 것입니다. 여기서 우리는 새 색상이 불투명도에 따라 이전 색상과 혼합되기를 원합니다. `finalColor`는 다음과 같이 계산되어야 합니다. + +```c++ +finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor; +finalColor.a = newAlpha.a; +``` + +이는 다음 매개변수로 달성할 수 있습니다. + +```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; +``` + +모든 가능한 연산은 명세서의 `VkBlendFactor` 및 `VkBlendOp` 열거형에서 찾을 수 있습니다. + +두 번째 구조체는 모든 프레임버퍼에 대한 구조체 배열을 참조하며, 앞서 언급한 계산에서 혼합 인자로 사용할 수 있는 블렌드 상수를 설정할 수 있습니다. + +```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 +``` + +두 번째 혼합 방법(비트 조합)을 사용하려면 `logicOpEnable`을 `VK_TRUE`로 설정해야 합니다. 비트 연산은 `logicOp` 필드에서 지정할 수 있습니다. 이렇게 하면 연결된 모든 프레임버퍼에 대해 `blendEnable`을 `VK_FALSE`로 설정한 것처럼 첫 번째 방법이 자동으로 비활성화됩니다! `colorWriteMask`는 이 모드에서도 프레임버퍼의 어떤 채널이 실제로 영향을 받을지 결정하는 데 사용됩니다. 여기서처럼 두 모드를 모두 비활성화하는 것도 가능하며, 이 경우 프래그먼트 색상이 수정 없이 프레임버퍼에 쓰여집니다. + +## 파이프라인 레이아웃 (Pipeline layout) + +셰이더에서 `uniform` 값을 사용할 수 있습니다. 이는 동적 상태 변수와 유사한 전역 변수로, 드로잉 시점에 변경하여 셰이더를 다시 만들지 않고도 동작을 변경할 수 있습니다. 일반적으로 변환 행렬을 정점 셰이더에 전달하거나 프래그먼트 셰이더에서 텍스처 샘플러를 생성하는 데 사용됩니다. + +이러한 uniform 값은 파이프라인 생성 중에 `VkPipelineLayout` 객체를 생성하여 지정해야 합니다. 나중 장까지는 사용하지 않겠지만, 비어있는 파이프라인 레이아웃이라도 반드시 생성해야 합니다. + +나중에 다른 함수에서 이 객체를 참조할 것이므로 클래스 멤버를 만들어 이 객체를 저장합니다. + +```c++ +VkPipelineLayout pipelineLayout; +``` + +그리고 `createGraphicsPipeline` 함수에서 객체를 생성합니다. + +```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!"); +} +``` + +이 구조체는 *푸시 상수(push constants)*도 지정하는데, 이는 셰이더에 동적 값을 전달하는 또 다른 방법으로, 나중에 다룰 수 있습니다. 파이프라인 레이아웃은 프로그램 수명 내내 참조되므로 마지막에 파괴되어야 합니다. + +```c++ +void cleanup() { + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + ... +} +``` + +## 결론 + +이것으로 모든 고정 함수 상태에 대한 설정이 끝났습니다! 모든 것을 처음부터 설정하는 것은 많은 작업이지만, 그 장점은 이제 그래픽 파이프라인에서 일어나는 거의 모든 일을 완전히 인지하게 되었다는 것입니다! 이는 특정 컴포넌트의 기본 상태가 예상과 달라 예기치 않은 동작에 부딪힐 가능성을 줄여줍니다. + +하지만 그래픽 파이프라인을 최종적으로 생성하기 전에 만들어야 할 객체가 하나 더 있으며, 그것은 바로 [렌더 패스](!en/Drawing_a_triangle/Graphics_pipeline_basics/Render_passes)입니다. + +[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 new file mode 100644 index 00000000..076f6927 --- /dev/null +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md @@ -0,0 +1,156 @@ +## 설정 + +파이프라인 생성을 완료하기 전에, Vulkan에게 렌더링 중에 사용될 프레임버퍼 어태치먼트(attachment)에 대해 알려주어야 합니다. 우리는 몇 개의 색상 및 깊이 버퍼가 있을지, 각각에 몇 개의 샘플을 사용할지, 그리고 렌더링 작업 전반에 걸쳐 해당 콘텐츠를 어떻게 처리해야 하는지 지정해야 합니다. 이 모든 정보는 *렌더 패스(render pass)* 객체에 담기게 되며, 이를 위해 새로운 `createRenderPass` 함수를 만들 것입니다. 이 함수를 `initVulkan`에서 `createGraphicsPipeline` 앞에 호출하세요. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); +} + +... + +void createRenderPass() { + +} +``` + +## 어태치먼트 명세 (Attachment description) + +우리의 경우, 스왑체인의 이미지 중 하나로 표현되는 단일 색상 버퍼 어태치먼트만 갖게 될 것입니다. + +```c++ +void createRenderPass() { + VkAttachmentDescription colorAttachment{}; + colorAttachment.format = swapChainImageFormat; + colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; +} +``` + +색상 어태치먼트의 `format`은 스왑체인 이미지의 포맷과 일치해야 하며, 아직 멀티샘플링은 다루지 않으므로 1개의 샘플을 사용하겠습니다. + +```c++ +colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; +colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; +``` + +`loadOp`와 `storeOp`는 렌더링 전후에 어태치먼트의 데이터를 어떻게 처리할지를 결정합니다. `loadOp`에 사용할 수 있는 선택지는 다음과 같습니다: + +* `VK_ATTACHMENT_LOAD_OP_LOAD`: 어태치먼트의 기존 내용을 보존합니다. +* `VK_ATTACHMENT_LOAD_OP_CLEAR`: 시작 시 값을 특정 상수로 지웁니다. +* `VK_ATTACHMENT_LOAD_OP_DONT_CARE`: 기존 내용이 정의되지 않음(undefined); 신경 쓰지 않습니다. + +우리의 경우, 새 프레임을 그리기 전에 프레임버퍼를 검은색으로 지우기 위해 clear 작업을 사용할 것입니다. `storeOp`에는 두 가지 가능성만 있습니다: + +* `VK_ATTACHMENT_STORE_OP_STORE`: 렌더링된 내용은 메모리에 저장되어 나중에 읽을 수 있습니다. +* `VK_ATTACHMENT_STORE_OP_DONT_CARE`: 렌더링 작업 후 프레임버퍼의 내용은 정의되지 않습니다. + +우리는 렌더링된 삼각형을 화면에서 보고 싶으므로, store 작업을 사용할 것입니다. + +```c++ +colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; +colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; +``` + +`loadOp`와 `storeOp`는 색상 및 깊이 데이터에 적용되며, `stencilLoadOp` / `stencilStoreOp`는 스텐실 데이터에 적용됩니다. 우리 애플리케이션은 스텐실 버퍼를 사용하지 않으므로, 로딩과 저장 결과는 중요하지 않습니다. + +```c++ +colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; +colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; +``` + +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{}; +colorAttachmentRef.attachment = 0; +colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; +``` + +`attachment` 파라미터는 어태치먼트 명세 배열의 인덱스를 통해 참조할 어태치먼트를 지정합니다. 우리 배열은 단일 `VkAttachmentDescription`으로 구성되어 있으므로 인덱스는 `0`입니다. `layout`은 이 참조를 사용하는 서브패스 동안 어태치먼트가 가지길 원하는 레이아웃을 지정합니다. Vulkan은 서브패스가 시작될 때 자동으로 어태치먼트를 이 레이아웃으로 전환합니다. 우리는 이 어태치먼트를 색상 버퍼로 사용할 것이며, 이름에서 알 수 있듯이 `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL` 레이아웃이 최상의 성능을 제공할 것입니다. + +서브패스는 `VkSubpassDescription` 구조체를 사용하여 설명됩니다: + +```c++ +VkSubpassDescription subpass{}; +subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; +``` + +Vulkan은 미래에 컴퓨트 서브패스도 지원할 수 있으므로, 이것이 그래픽스 서브패스임을 명시적으로 지정해야 합니다. 다음으로, 색상 어태치먼트에 대한 참조를 지정합니다. + +```c++ +subpass.colorAttachmentCount = 1; +subpass.pColorAttachments = &colorAttachmentRef; +``` + +이 배열에서 어태치먼트의 인덱스는 프래그먼트 셰이더에서 `layout(location = 0) out vec4 outColor` 지시문을 통해 직접 참조됩니다! + +서브패스에서 참조할 수 있는 다른 유형의 어태치먼트는 다음과 같습니다: + +* `pInputAttachments`: 셰이더에서 읽어오는 어태치먼트 +* `pResolveAttachments`: 멀티샘플링된 색상 어태치먼트를 위해 사용되는 어태치먼트 +* `pDepthStencilAttachment`: 깊이 및 스텐실 데이터를 위한 어태치먼트 +* `pPreserveAttachments`: 이 서브패스에서는 사용되지 않지만 데이터가 보존되어야 하는 어태치먼트 + +## 렌더 패스 + +이제 어태치먼트와 이를 참조하는 기본 서브패스가 설명되었으므로, 렌더 패스 자체를 생성할 수 있습니다. `pipelineLayout` 변수 바로 위에 `VkRenderPass` 객체를 담을 새로운 클래스 멤버 변수를 만드세요. + +```c++ +VkRenderPass renderPass; +VkPipelineLayout pipelineLayout; +``` + +그런 다음 `VkRenderPassCreateInfo` 구조체를 어태치먼트와 서브패스 배열로 채워서 렌더 패스 객체를 생성할 수 있습니다. `VkAttachmentReference` 객체들은 이 배열의 인덱스를 사용하여 어태치먼트를 참조합니다. + +```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!"); +} +``` + +파이프라인 레이아웃과 마찬가지로 렌더 패스는 프로그램 전반에서 참조되므로, 프로그램이 끝날 때 정리해야 합니다. + +```c++ +void cleanup() { + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + vkDestroyRenderPass(device, renderPass, nullptr); + ... +} +``` + +상당히 많은 작업이었지만, 다음 장에서는 이 모든 것이 합쳐져 마침내 그래픽스 파이프라인 객체를 생성하게 될 것입니다! + +[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 new file mode 100644 index 00000000..8baa3f86 --- /dev/null +++ b/ko/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md @@ -0,0 +1,84 @@ +이제 이전 챕터들에서 다룬 모든 구조체와 객체를 조합하여 그래픽스 파이프라인을 만들 수 있습니다! 우리가 지금까지 다룬 객체 유형을 간단히 요약하면 다음과 같습니다: + +* **셰이더 스테이지(Shader stages)**: 그래픽스 파이프라인의 프로그래밍 가능한 스테이지의 기능을 정의하는 셰이더 모듈 +* **고정 함수 상태(Fixed-function state)**: 입력 어셈블리, 래스터라이저, 뷰포트, 색상 혼합과 같이 파이프라인의 고정 함수 스테이지를 정의하는 모든 구조체 +* **파이프라인 레이아웃(Pipeline layout)**: 셰이더에서 참조하며 드로우 타임에 업데이트할 수 있는 유니폼 및 푸시 값 +* **렌더 패스(Render pass)**: 파이프라인 스테이지에서 참조하는 어태치먼트와 그 사용법 + +이 모든 것을 합치면 그래픽스 파이프라인의 기능이 완벽하게 정의됩니다. 따라서 이제 `createGraphicsPipeline` 함수의 끝부분에서 `VkGraphicsPipelineCreateInfo` 구조체를 채워 넣기 시작할 수 있습니다. 단, 셰이더 모듈은 파이프라인 생성 중에 여전히 사용되므로 `vkDestroyShaderModule` 호출보다는 앞에 위치해야 합니다. + +```c++ +VkGraphicsPipelineCreateInfo pipelineInfo{}; +pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; +pipelineInfo.stageCount = 2; +pipelineInfo.pStages = shaderStages; +``` + +먼저 `VkPipelineShaderStageCreateInfo` 구조체 배열을 참조하는 것으로 시작합니다. + +```c++ +pipelineInfo.pVertexInputState = &vertexInputInfo; +pipelineInfo.pInputAssemblyState = &inputAssembly; +pipelineInfo.pViewportState = &viewportState; +pipelineInfo.pRasterizationState = &rasterizer; +pipelineInfo.pMultisampleState = &multisampling; +pipelineInfo.pDepthStencilState = nullptr; // 선택 사항 +pipelineInfo.pColorBlendState = &colorBlending; +pipelineInfo.pDynamicState = &dynamicState; +``` + +그다음, 고정 함수 스테이지를 설명하는 모든 구조체를 참조합니다. + +```c++ +pipelineInfo.layout = pipelineLayout; +``` + +그다음은 파이프라인 레이아웃인데, 이것은 구조체 포인터가 아닌 Vulkan 핸들입니다. + +```c++ +pipelineInfo.renderPass = renderPass; +pipelineInfo.subpass = 0; +``` + +마지막으로 렌더 패스와, 이 그래픽스 파이프라인이 사용될 서브패스의 인덱스에 대한 참조가 있습니다. 이 파이프라인을 이 특정 인스턴스 대신 다른 렌더 패스와 함께 사용하는 것도 가능하지만, 그 렌더 패스들은 `renderPass`와 *호환 가능(compatible)*해야 합니다. 호환성 요구 사항은 [여기](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap8.html#renderpass-compatibility)에 설명되어 있지만, 이 튜토리얼에서는 해당 기능을 사용하지 않을 것입니다. + +```c++ +pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // 선택 사항 +pipelineInfo.basePipelineIndex = -1; // 선택 사항 +``` + +실제로는 두 개의 매개변수가 더 있습니다: `basePipelineHandle`과 `basePipelineIndex`. Vulkan에서는 기존 파이프라인에서 파생하여 새로운 그래픽스 파이프라인을 생성할 수 있습니다. 파이프라인 파생(derivatives)의 개념은, 기존 파이프라인과 많은 기능이 공통될 때 파이프라인을 설정하는 비용이 저렴해지고, 동일한 부모에서 파생된 파이프라인 간의 전환도 더 빠르게 수행할 수 있다는 것입니다. `basePipelineHandle`로 기존 파이프라인의 핸들을 지정하거나, `basePipelineIndex`로 지금 생성하려는 다른 파이프라인을 인덱스로 참조할 수 있습니다. 지금은 파이프라인이 하나뿐이므로, null 핸들과 유효하지 않은 인덱스를 지정하겠습니다. 이 값들은 `VkGraphicsPipelineCreateInfo`의 `flags` 필드에 `VK_PIPELINE_CREATE_DERIVATIVE_BIT` 플래그가 지정된 경우에만 사용됩니다. + +이제 마지막 단계를 위해 `VkPipeline` 객체를 담을 클래스 멤버를 만듭니다: + +```c++ +VkPipeline graphicsPipeline; +``` + +그리고 마침내 그래픽스 파이프라인을 생성합니다: + +```c++ +if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) { + throw std::runtime_error("failed to create graphics pipeline!"); +} +``` + +`vkCreateGraphicsPipelines` 함수는 사실 Vulkan의 일반적인 객체 생성 함수들보다 매개변수가 더 많습니다. 이 함수는 여러 개의 `VkGraphicsPipelineCreateInfo` 객체를 받아 한 번의 호출로 여러 `VkPipeline` 객체를 생성하도록 설계되었습니다. + +우리가 `VK_NULL_HANDLE` 인자를 전달한 두 번째 매개변수는 선택적인 `VkPipelineCache` 객체를 참조합니다. 파이프라인 캐시는 여러 `vkCreateGraphicsPipelines` 호출에 걸쳐 파이프라인 생성과 관련된 데이터를 저장하고 재사용하는 데 사용될 수 있으며, 캐시를 파일에 저장하면 프로그램 실행 간에도 재사용할 수 있습니다. 이렇게 하면 나중에 파이프라인 생성 속도를 크게 높일 수 있습니다. 이 내용은 파이프라인 캐시 챕터에서 다룰 것입니다. + +그래픽스 파이프라인은 모든 일반적인 드로잉 작업에 필요하므로 프로그램이 끝날 때 파괴되어야 합니다. + +```c++ +void cleanup() { + vkDestroyPipeline(device, graphicsPipeline, nullptr); + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + ... +} +``` + +이제 프로그램을 실행하여 이 모든 노력이 성공적인 파이프라인 생성으로 이어졌는지 확인하세요! 이제 화면에 무언가 나타나는 것에 꽤 가까워졌습니다. 다음 몇 개의 챕터에서는 스왑 체인 이미지로부터 실제 프레임버퍼를 설정하고 드로잉 커맨드를 준비할 것입니다. + +[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 new file mode 100644 index 00000000..b73b8dc4 --- /dev/null +++ b/ko/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md @@ -0,0 +1,89 @@ +### 프레임버퍼 + +지난 몇 장에 걸쳐 프레임버퍼에 대해 많이 이야기했고, 스왑 체인 이미지와 동일한 포맷을 가진 단일 프레임버퍼를 사용하도록 렌더 패스를 설정했지만, 아직 실제로 생성하지는 않았습니다. + +렌더 패스를 생성할 때 지정한 첨부(attachment)들은 `VkFramebuffer` 객체로 감싸서 바인딩됩니다. 프레임버퍼 객체는 첨부를 나타내는 모든 `VkImageView` 객체를 참조합니다. 우리의 경우에는 단 하나, 바로 색상 첨부(color attachment)입니다. 하지만 첨부에 사용해야 할 이미지는 우리가 프레젠테이션을 위해 스왑 체인에서 이미지를 가져올 때 어떤 이미지를 반환하는지에 따라 달라집니다. 이는 스왑 체인의 모든 이미지에 대해 프레임버퍼를 생성하고, 드로잉 시점에는 가져온 이미지에 해당하는 것을 사용해야 한다는 의미입니다. + +이를 위해, 프레임버퍼를 담을 또 다른 `std::vector` 클래스 멤버를 생성합니다: + +```c++ +std::vector swapChainFramebuffers; +``` + +이 배열을 위한 객체들은 `initVulkan`에서 그래픽 파이프라인을 생성한 직후에 호출되는 새로운 함수 `createFramebuffers`에서 생성할 것입니다: + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); +} + +... + +void createFramebuffers() { + +} +``` + +먼저 컨테이너의 크기를 조절하여 모든 프레임버퍼를 담을 수 있도록 합니다: + +```c++ +void createFramebuffers() { + swapChainFramebuffers.resize(swapChainImageViews.size()); +} +``` + +그런 다음 이미지 뷰를 순회하며 프레임버퍼를 생성합니다: + +```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!"); + } +} +``` + +보시다시피, 프레임버퍼 생성은 매우 간단합니다. 먼저 프레임버퍼가 어떤 `renderPass`와 호환되어야 하는지 지정해야 합니다. 프레임버퍼는 호환되는 렌더 패스와만 사용할 수 있는데, 이는 대략적으로 말해 동일한 수와 유형의 첨부를 사용한다는 것을 의미합니다. + +`attachmentCount`와 `pAttachments` 매개변수는 렌더 패스의 `pAttachment` 배열에 있는 각 첨부 설명에 바인딩될 `VkImageView` 객체를 지정합니다. + +`width`와 `height` 매개변수는 이름에서 알 수 있듯이 명확하며, `layers`는 이미지 배열의 레이어 수를 나타냅니다. 우리의 스왑 체인 이미지는 단일 이미지이므로 레이어 수는 `1`입니다. + +프레임버퍼는 그것들이 기반으로 하는 이미지 뷰와 렌더 패스보다 먼저 삭제되어야 하지만, 렌더링을 모두 마친 후에만 삭제해야 합니다: + +```c++ +void cleanup() { + for (auto framebuffer : swapChainFramebuffers) { + vkDestroyFramebuffer(device, framebuffer, nullptr); + } + + ... +} +``` + +이제 우리는 렌더링에 필요한 모든 객체를 갖추는 중요한 단계에 도달했습니다. 다음 장에서는 첫 실제 드로잉 명령을 작성할 것입니다. + +[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 new file mode 100644 index 00000000..369c70c7 --- /dev/null +++ b/ko/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md @@ -0,0 +1,267 @@ +Vulkan에서 그리기 연산이나 메모리 전송과 같은 커맨드(command)는 함수 호출을 통해 직접 실행되지 않습니다. 대신, 수행하려는 모든 작업을 커맨드 버퍼(command buffer) 객체에 기록(record)해야 합니다. 이 방식의 장점은 Vulkan에게 무엇을 할지 알려줄 준비가 되었을 때 모든 커맨드가 함께 제출된다는 것입니다. 그러면 Vulkan은 모든 커맨드를 한 번에 사용할 수 있으므로 더 효율적으로 처리할 수 있습니다. 또한, 원한다면 여러 스레드에서 커맨드 기록을 수행할 수도 있습니다. + +## 커맨드 풀 (Command pools) + +커맨드 버퍼를 생성하기 전에 먼저 커맨드 풀(command pool)을 생성해야 합니다. 커맨드 풀은 버퍼를 저장하는 데 사용되는 메모리를 관리하며, 커맨드 버퍼는 이 풀에서 할당됩니다. `VkCommandPool`을 저장할 새 클래스 멤버를 추가합니다. + +```c++ +VkCommandPool commandPool; +``` + +그런 다음 `createCommandPool`이라는 새 함수를 만들고, `initVulkan`에서 프레임버퍼가 생성된 후에 호출합니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); +} + +... + +void createCommandPool() { + +} +``` + +커맨드 풀 생성에는 단 두 개의 매개변수만 필요합니다. + +```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(); +``` + +커맨드 풀에는 두 가지 가능한 플래그가 있습니다: + +* `VK_COMMAND_POOL_CREATE_TRANSIENT_BIT`: 커맨드 버퍼가 새로운 커맨드로 매우 자주 다시 기록될 것임을 암시합니다 (메모리 할당 동작이 변경될 수 있음). +* `VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT`: 커맨드 버퍼를 개별적으로 다시 기록할 수 있도록 허용합니다. 이 플래그가 없으면 모든 커맨드 버퍼를 함께 리셋해야 합니다. + +우리는 매 프레임마다 커맨드 버퍼를 기록할 것이므로, 이를 리셋하고 다시 기록할 수 있어야 합니다. 따라서 커맨드 풀에 `VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT` 플래그 비트를 설정해야 합니다. + +커맨드 버퍼는 우리가 가져온 그래픽스 및 프레젠테이션 큐와 같은 장치 큐 중 하나에 제출하여 실행됩니다. 각 커맨드 풀은 단일 유형의 큐에 제출되는 커맨드 버퍼만 할당할 수 있습니다. 우리는 그리기를 위한 커맨드를 기록할 것이므로 그래픽스 큐 패밀리를 선택했습니다. + +```c++ +if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) { + throw std::runtime_error("failed to create command pool!"); +} +``` + +`vkCreateCommandPool` 함수를 사용하여 커맨드 풀 생성을 완료합니다. 이 함수에는 특별한 매개변수가 없습니다. 커맨드는 프로그램 전반에 걸쳐 화면에 무언가를 그리는 데 사용되므로, 풀은 프로그램이 끝날 때만 파괴되어야 합니다. + +```c++ +void cleanup() { + vkDestroyCommandPool(device, commandPool, nullptr); + + ... +} +``` + +## 커맨드 버퍼 할당 + +이제 커맨드 버퍼 할당을 시작할 수 있습니다. + +`VkCommandBuffer` 객체를 클래스 멤버로 생성합니다. 커맨드 버퍼는 커맨드 풀이 파괴될 때 자동으로 해제되므로, 명시적인 정리 코드가 필요하지 않습니다. + +```c++ +VkCommandBuffer commandBuffer; +``` + +이제 커맨드 풀에서 단일 커맨드 버퍼를 할당하는 `createCommandBuffer` 함수 작업을 시작하겠습니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); + createCommandBuffer(); +} + +... + +void createCommandBuffer() { + +} +``` + +커맨드 버퍼는 `vkAllocateCommandBuffers` 함수로 할당되며, 이 함수는 커맨드 풀과 할당할 버퍼 수를 지정하는 `VkCommandBufferAllocateInfo` 구조체를 매개변수로 받습니다. + +```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!"); +} +``` + +`level` 매개변수는 할당된 커맨드 버퍼가 주(primary) 커맨드 버퍼인지 보조(secondary) 커맨드 버퍼인지를 지정합니다. + +* `VK_COMMAND_BUFFER_LEVEL_PRIMARY`: 큐에 제출하여 실행할 수 있지만, 다른 커맨드 버퍼에서 호출될 수는 없습니다. +* `VK_COMMAND_BUFFER_LEVEL_SECONDARY`: 직접 제출할 수는 없지만, 주 커맨드 버퍼에서 호출될 수 있습니다. + +여기서는 보조 커맨드 버퍼 기능을 사용하지 않겠지만, 주 커맨드 버퍼에서 공통 작업을 재사용하는 데 유용하다는 것을 상상할 수 있습니다. + +우리는 하나의 커맨드 버퍼만 할당하므로 `commandBufferCount` 매개변수는 1입니다. + +## 커맨드 버퍼 기록 + +이제 실행하려는 커맨드를 커맨드 버퍼에 작성하는 `recordCommandBuffer` 함수 작업을 시작하겠습니다. 사용될 `VkCommandBuffer`와 현재 작성하려는 스왑체인 이미지의 인덱스가 매개변수로 전달됩니다. + +```c++ +void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) { + +} +``` + +커맨드 버퍼 기록은 항상 `vkBeginCommandBuffer`를 호출하는 것으로 시작합니다. 이 함수는 해당 커맨드 버퍼의 사용에 대한 세부 정보를 지정하는 작은 `VkCommandBufferBeginInfo` 구조체를 인자로 받습니다. + +```c++ +VkCommandBufferBeginInfo beginInfo{}; +beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; +beginInfo.flags = 0; // 선택 사항 +beginInfo.pInheritanceInfo = nullptr; // 선택 사항 + +if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) { + throw std::runtime_error("failed to begin recording command buffer!"); +} +``` + +`flags` 매개변수는 우리가 커맨드 버퍼를 어떻게 사용할지를 지정합니다. 다음과 같은 값들을 사용할 수 있습니다: + +* `VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT`: 커맨드 버퍼는 한 번 실행된 직후 다시 기록될 것입니다. +* `VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT`: 이것은 단일 렌더 패스 내에서만 사용될 보조 커맨드 버퍼입니다. +* `VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT`: 커맨드 버퍼가 이미 실행 대기 중인 상태에서도 다시 제출될 수 있습니다. + +지금 우리에게는 이 플래그들 중 어느 것도 해당되지 않습니다. + +`pInheritanceInfo` 매개변수는 보조 커맨드 버퍼에만 관련이 있습니다. 이 매개변수는 호출하는 주 커맨드 버퍼로부터 어떤 상태를 상속받을지 지정합니다. + +커맨드 버퍼가 이미 한 번 기록되었다면, `vkBeginCommandBuffer`를 호출하면 암시적으로 리셋됩니다. 나중에 버퍼에 커맨드를 추가하는 것은 불가능합니다. + +## 렌더 패스 시작하기 + +그리기는 `vkCmdBeginRenderPass`로 렌더 패스를 시작하는 것으로 시작됩니다. 렌더 패스는 `VkRenderPassBeginInfo` 구조체의 몇 가지 매개변수를 사용하여 구성됩니다. + +```c++ +VkRenderPassBeginInfo renderPassInfo{}; +renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; +renderPassInfo.renderPass = renderPass; +renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex]; +``` + +첫 번째 매개변수는 렌더 패스 자체이고, 두 번째는 바인딩할 어태치먼트입니다. 우리는 각 스왑 체인 이미지에 대해 프레임버퍼를 생성했으며, 각 이미지는 컬러 어태치먼트로 지정되었습니다. 따라서 우리가 그리려는 스왑체인 이미지에 맞는 프레임버퍼를 바인딩해야 합니다. 전달된 `imageIndex` 매개변수를 사용하여 현재 스왑체인 이미지에 적합한 프레임버퍼를 선택할 수 있습니다. + +```c++ +renderPassInfo.renderArea.offset = {0, 0}; +renderPassInfo.renderArea.extent = swapChainExtent; +``` + +다음 두 매개변수는 렌더 영역의 크기를 정의합니다. 렌더 영역은 셰이더 로드 및 저장이 일어날 위치를 정의합니다. 이 영역 밖의 픽셀은 정의되지 않은 값을 갖게 됩니다. 최상의 성능을 위해서는 어태치먼트의 크기와 일치해야 합니다. + +```c++ +VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}}; +renderPassInfo.clearValueCount = 1; +renderPassInfo.pClearValues = &clearColor; +``` + +마지막 두 매개변수는 `VK_ATTACHMENT_LOAD_OP_CLEAR`에 사용할 소거 값(clear value)을 정의합니다. 우리는 이 값을 컬러 어태치먼트의 로드 작업으로 사용했습니다. 저는 소거 색상을 100% 불투명도의 검은색으로 정의했습니다. + +```c++ +vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); +``` + +이제 렌더 패스를 시작할 수 있습니다. 커맨드를 기록하는 모든 함수는 `vkCmd` 접두사로 식별할 수 있습니다. 이 함수들은 모두 `void`를 반환하므로, 기록이 끝날 때까지 오류 처리는 없습니다. + +모든 커맨드의 첫 번째 매개변수는 항상 커맨드를 기록할 커맨드 버퍼입니다. 두 번째 매개변수는 우리가 방금 제공한 렌더 패스의 세부 정보를 지정합니다. 마지막 매개변수는 렌더 패스 내의 드로잉 커맨드가 어떻게 제공될지를 제어합니다. 이 값은 다음 두 가지 중 하나일 수 있습니다: + +* `VK_SUBPASS_CONTENTS_INLINE`: 렌더 패스 커맨드가 주 커맨드 버퍼 자체에 포함되며, 보조 커맨드 버퍼는 실행되지 않습니다. +* `VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS`: 렌더 패스 커맨드가 보조 커맨드 버퍼에서 실행됩니다. + +우리는 보조 커맨드 버퍼를 사용하지 않을 것이므로, 첫 번째 옵션을 선택합니다. + +## 기본 드로잉 커맨드 + +이제 그래픽스 파이프라인을 바인딩할 수 있습니다. + +```c++ +vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); +``` + +두 번째 매개변수는 파이프라인 객체가 그래픽스 파이프라인인지 컴퓨트 파이프라인인지를 지정합니다. 이제 Vulkan에게 그래픽스 파이프라인에서 어떤 작업을 실행할지, 그리고 프래그먼트 셰이더에서 어떤 어태치먼트를 사용할지를 알려주었습니다. + +[고정 함수 챕터](../02_Graphics_pipeline_basics/02_Fixed_functions.md#dynamic-state)에서 언급했듯이, 우리는 이 파이프라인의 뷰포트와 시저 상태를 동적(dynamic)으로 지정했습니다. +따라서 드로우 커맨드를 실행하기 전에 커맨드 버퍼에서 이를 설정해야 합니다: + +```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); +``` + +이제 삼각형을 그리기 위한 드로우 커맨드를 실행할 준비가 되었습니다. + +```c++ +vkCmdDraw(commandBuffer, 3, 1, 0, 0); +``` + +실제 `vkCmdDraw` 함수는 다소 김이 빠지지만, 사전에 모든 정보를 지정했기 때문에 이렇게 간단합니다. 이 함수는 커맨드 버퍼 외에 다음과 같은 매개변수를 가집니다: + +* `vertexCount`: 정점 버퍼가 없지만, 기술적으로는 여전히 3개의 정점을 그려야 합니다. +* `instanceCount`: 인스턴스 렌더링에 사용됩니다. 사용하지 않을 경우 `1`을 사용합니다. +* `firstVertex`: 정점 버퍼의 오프셋으로 사용되며, `gl_VertexIndex`의 최솟값을 정의합니다. +* `firstInstance`: 인스턴스 렌더링의 오프셋으로 사용되며, `gl_InstanceIndex`의 최솟값을 정의합니다. + +## 마무리 + +이제 렌더 패스를 종료할 수 있습니다. + +```c++ +vkCmdEndRenderPass(commandBuffer); +``` + +그리고 커맨드 버퍼 기록을 마쳤습니다. + +```c++ +if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to record command buffer!"); +} +``` + +다음 챕터에서는 메인 루프 코드를 작성할 것입니다. 이 루프는 스왑 체인에서 이미지를 가져오고, 커맨드 버퍼를 기록 및 실행한 다음, 완성된 이미지를 스왑 체인으로 반환하는 작업을 수행합니다. + +[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 new file mode 100644 index 00000000..78ce4212 --- /dev/null +++ b/ko/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md @@ -0,0 +1,398 @@ +이번 장에서는 모든 것을 하나로 합칠 시간입니다. 메인 루프에서 호출되어 삼각형을 화면에 그리는 `drawFrame` 함수를 작성할 것입니다. 먼저 함수를 만들고 `mainLoop`에서 호출해 봅시다. + +```c++ +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + drawFrame(); + } +} + +... + +void drawFrame() { + +} +``` + +## 프레임의 개요 + +높은 수준에서 Vulkan으로 프레임을 렌더링하는 것은 다음과 같은 공통된 단계로 구성됩니다. + +* 이전 프레임이 끝나기를 기다립니다. +* 스왑 체인에서 이미지를 가져옵니다. +* 가져온 이미지에 장면을 그리는 커맨드 버퍼를 기록합니다. +* 기록된 커맨드 버퍼를 제출합니다. +* 스왑 체인 이미지를 제시(present)합니다. + +이후 장에서 드로잉 함수를 더 확장하겠지만, 지금으로서는 이것이 우리 렌더링 루프의 핵심입니다. + + + +## 동기화 + + + +Vulkan의 핵심 설계 철학 중 하나는 GPU에서의 실행 동기화가 명시적이라는 것입니다. 작업 순서는 우리가 다양한 동기화 프리미티브(primitive)를 사용하여 드라이버에 원하는 실행 순서를 알려주는 것에 달려 있습니다. 이는 GPU에서 작업을 시작하는 많은 Vulkan API 호출이 비동기적임을 의미합니다. 즉, 함수는 작업이 완료되기 전에 반환됩니다. + +이번 장에서는 GPU에서 발생하는 여러 이벤트를 명시적으로 순서를 정해야 합니다. 예를 들면 다음과 같습니다. + +* 스왑 체인에서 이미지 가져오기 +* 가져온 이미지에 그리는 커맨드 실행하기 +* 화면에 표시(presentation)하기 위해 이미지를 제시하고, 스왑 체인에 반환하기 + +이 각 이벤트는 단일 함수 호출로 시작되지만 모두 비동기적으로 실행됩니다. 함수 호출은 실제 작업이 끝나기 전에 반환되며 실행 순서 또한 정의되지 않습니다. 이는 안타까운 일입니다. 왜냐하면 각 작업은 이전 작업이 완료되는 것에 의존하기 때문입니다. 따라서 원하는 순서를 달성하기 위해 어떤 프리미티브를 사용할 수 있는지 알아봐야 합니다. + +### 세마포어(Semaphores) + +세마포어는 큐 작업 간에 순서를 추가하는 데 사용됩니다. 큐 작업이란 커맨드 버퍼나 나중에 보게 될 함수 내에서 큐에 제출하는 작업을 말합니다. 큐의 예로는 그래픽스 큐와 프레젠테이션 큐가 있습니다. 세마포어는 동일한 큐 내의 작업을 정렬하거나 서로 다른 큐 간의 작업을 정렬하는 데 모두 사용됩니다. + +Vulkan에는 바이너리(binary)와 타임라인(timeline) 두 종류의 세마포어가 있습니다. 이 튜토리얼에서는 바이너리 세마포어만 사용하므로 타임라인 세마포어는 다루지 않겠습니다. 앞으로 '세마포어'라는 용어는 바이너리 세마포어만을 지칭합니다. + +세마포어는 신호되지 않음(unsignaled) 또는 신호됨(signaled) 상태입니다. 처음에는 신호되지 않은 상태로 시작합니다. 세마포어를 사용하여 큐 작업의 순서를 정하는 방법은 한 큐 작업에서는 '신호(signal)' 세마포어로, 다른 큐 작업에서는 '대기(wait)' 세마포어로 동일한 세마포어를 제공하는 것입니다. 예를 들어, 순서대로 실행하고 싶은 세마포어 S와 큐 작업 A, B가 있다고 가정해 봅시다. Vulkan에 작업 A가 실행을 마치면 세마포어 S에 '신호'를 보내고, 작업 B는 실행을 시작하기 전에 세마포어 S를 '대기'하라고 알려줍니다. 작업 A가 끝나면 세마포어 S는 신호 상태가 되고, 작업 B는 S가 신호될 때까지 시작되지 않습니다. 작업 B가 실행을 시작하면 세마포어 S는 자동으로 신호되지 않은 상태로 재설정되어 다시 사용할 수 있게 됩니다. + +방금 설명한 내용의 의사 코드(pseudo-code)는 다음과 같습니다. +``` +VkCommandBuffer A, B = ... // 커맨드 버퍼 기록 +VkSemaphore S = ... // 세마포어 생성 + +// A를 큐에 넣고, 끝나면 S에 신호 보냄 - 즉시 실행 시작 +vkQueueSubmit(work: A, signal: S, wait: None) + +// B를 큐에 넣고, 시작하기 위해 S를 대기 +vkQueueSubmit(work: B, signal: None, wait: S) +``` + +이 코드 조각에서 `vkQueueSubmit()`에 대한 두 호출은 즉시 반환됩니다. 대기는 GPU에서만 발생합니다. CPU는 블로킹(blocking)되지 않고 계속 실행됩니다. CPU를 기다리게 하려면 다른 동기화 프리미티브가 필요하며, 이제 그것에 대해 설명하겠습니다. + +### 펜스(Fences) + +펜스도 실행을 동기화하는 데 사용된다는 점에서 비슷한 목적을 가지지만, 이는 CPU, 즉 호스트(host)에서의 실행 순서를 정하기 위한 것입니다. 간단히 말해, 호스트가 GPU가 무언가를 마쳤는지 알아야 할 때 펜스를 사용합니다. + +세마포어와 마찬가지로 펜스도 신호됨 또는 신호되지 않음 상태입니다. 작업을 제출하여 실행할 때마다 해당 작업에 펜스를 첨부할 수 있습니다. 작업이 완료되면 펜스는 신호 상태가 됩니다. 그런 다음 호스트가 펜스가 신호될 때까지 기다리게 할 수 있으며, 이를 통해 호스트가 계속 진행하기 전에 작업이 완료되었음을 보장합니다. + +구체적인 예로 스크린샷을 찍는 경우를 들어보겠습니다. GPU에서 필요한 작업을 이미 마쳤다고 가정합시다. 이제 GPU에서 호스트로 이미지를 전송한 다음 메모리를 파일에 저장해야 합니다. 전송을 실행하는 커맨드 버퍼 A와 펜스 F가 있습니다. 펜스 F와 함께 커맨드 버퍼 A를 제출한 다음, 즉시 호스트에게 F가 신호될 때까지 기다리라고 지시합니다. 이로 인해 호스트는 커맨드 버퍼 A의 실행이 끝날 때까지 블로킹됩니다. 따라서 메모리 전송이 완료되었으므로 호스트가 파일을 디스크에 안전하게 저장할 수 있습니다. + +설명한 내용에 대한 의사 코드는 다음과 같습니다. +``` +VkCommandBuffer A = ... // 전송을 포함한 커맨드 버퍼 기록 +VkFence F = ... // 펜스 생성 + +// A를 큐에 넣고, 즉시 작업 시작, 끝나면 F에 신호 보냄 +vkQueueSubmit(work: A, fence: F) + +vkWaitForFence(F) // A의 실행이 끝날 때까지 실행을 블로킹함 + +save_screenshot_to_disk() // 전송이 끝나기 전까지 실행될 수 없음 +``` + +세마포어 예제와 달리, 이 예제는 호스트 실행을 *블로킹*합니다. 이는 호스트가 실행이 끝날 때까지 기다리는 것 외에는 아무것도 하지 않는다는 것을 의미합니다. 이 경우, 스크린샷을 디스크에 저장하기 전에 전송이 완료되었는지 확인해야 했습니다. + +일반적으로, 필요하지 않다면 호스트를 블로킹하지 않는 것이 좋습니다. 우리는 GPU와 호스트에 유용한 작업을 계속 제공하고 싶습니다. 펜스가 신호되기를 기다리는 것은 유용한 작업이 아닙니다. 따라서 작업을 동기화하기 위해 세마포어나 아직 다루지 않은 다른 동기화 프리미티브를 선호합니다. + +펜스는 수동으로 재설정하여 신호되지 않은 상태로 되돌려야 합니다. 이는 펜스가 호스트의 실행을 제어하는 데 사용되므로, 언제 펜스를 재설정할지 호스트가 결정하기 때문입니다. 이는 호스트의 개입 없이 GPU 상의 작업 순서를 정하는 데 사용되는 세마포어와 대조적입니다. + +요약하자면, 세마포어는 GPU에서의 작업 실행 순서를 지정하는 데 사용되고, 펜스는 CPU와 GPU를 서로 동기화 상태로 유지하는 데 사용됩니다. + +### 무엇을 선택해야 할까? + +우리에게는 두 가지 동기화 프리미티브가 있고, 마침 동기화를 적용할 두 곳이 있습니다: 스왑 체인 작업과 이전 프레임이 끝나기를 기다리는 것. 스왑 체인 작업은 GPU에서 발생하므로 가능하다면 호스트를 기다리게 하고 싶지 않으므로 세마포어를 사용하고 싶습니다. 이전 프레임이 끝나기를 기다리는 것에는 반대의 이유로 펜스를 사용하고 싶습니다. 왜냐하면 호스트가 기다려야 하기 때문입니다. 이는 한 번에 한 프레임 이상을 그리지 않도록 하기 위함입니다. 우리는 매 프레임마다 커맨드 버퍼를 다시 기록하므로, 현재 프레임이 실행을 마칠 때까지 다음 프레임의 작업을 커맨드 버퍼에 기록할 수 없습니다. GPU가 커맨드 버퍼를 사용하는 동안 현재 내용을 덮어쓰고 싶지 않기 때문입니다. + +## 동기화 객체 생성하기 + +이미지가 스왑 체인에서 사용 가능해져 렌더링 준비가 되었음을 알리는 세마포어 하나, 렌더링이 끝나 프레젠테이션이 가능함을 알리는 세마포어 하나, 그리고 한 번에 한 프레임만 렌더링되도록 보장하는 펜스 하나가 필요합니다. + +이 세마포어 객체들과 펜스 객체를 저장할 세 개의 클래스 멤버를 만듭니다. + +```c++ +VkSemaphore imageAvailableSemaphore; +VkSemaphore renderFinishedSemaphore; +VkFence inFlightFence; +``` + +세마포어를 생성하기 위해 튜토리얼의 이 부분에 대한 마지막 `create` 함수인 `createSyncObjects`를 추가합니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); + createCommandBuffer(); + createSyncObjects(); +} + +... + +void createSyncObjects() { + +} +``` + +세마포어를 생성하려면 `VkSemaphoreCreateInfo`를 채워야 하지만, 현재 API 버전에서는 `sType` 외에는 필수 필드가 없습니다. + +```c++ +void createSyncObjects() { + VkSemaphoreCreateInfo semaphoreInfo{}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; +} +``` + +미래의 Vulkan API 버전이나 확장 기능은 다른 구조체와 마찬가지로 `flags`와 `pNext` 파라미터에 기능을 추가할 수 있습니다. + +펜스를 생성하려면 `VkFenceCreateInfo`를 채워야 합니다. + +```c++ +VkFenceCreateInfo fenceInfo{}; +fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; +``` + +세마포어와 펜스 생성은 `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!"); +} +``` + +세마포어와 펜스는 프로그램이 끝날 때, 모든 커맨드가 완료되고 더 이상 동기화가 필요 없을 때 정리되어야 합니다. + +```c++ +void cleanup() { + vkDestroySemaphore(device, imageAvailableSemaphore, nullptr); + vkDestroySemaphore(device, renderFinishedSemaphore, nullptr); + vkDestroyFence(device, inFlightFence, nullptr); +``` + +이제 메인 드로잉 함수로 넘어가 봅시다! + +## 이전 프레임 기다리기 + +프레임의 시작에서, 우리는 이전 프레임이 끝나기를 기다려서 커맨드 버퍼와 세마포어를 사용할 수 있게 하고 싶습니다. 이를 위해 `vkWaitForFences`를 호출합니다. + +```c++ +void drawFrame() { + vkWaitForFences(device, 1, &inFlightFence, VK_TRUE, UINT64_MAX); +} +``` + +`vkWaitForFences` 함수는 펜스 배열을 받아, 배열의 일부 또는 모든 펜스가 신호될 때까지 호스트에서 기다린 후 반환합니다. 여기서 전달하는 `VK_TRUE`는 모든 펜스를 기다리겠다는 의미이지만, 펜스가 하나일 경우에는 상관없습니다. 이 함수에는 타임아웃 파라미터도 있는데, 우리는 64비트 부호 없는 정수의 최댓값인 `UINT64_MAX`를 설정하여 타임아웃을 사실상 비활성화합니다. + +기다린 후에는 `vkResetFences` 호출로 펜스를 수동으로 신호되지 않은 상태로 재설정해야 합니다. + +```c++ + vkResetFences(device, 1, &inFlightFence); +``` + +계속 진행하기 전에 우리 설계에 약간의 문제가 있습니다. 첫 프레임에서 `drawFrame()`을 호출하면 즉시 `inFlightFence`가 신호되기를 기다립니다. `inFlightFence`는 프레임 렌더링이 완료된 후에만 신호되는데, 지금은 첫 프레임이므로 펜스에 신호를 보낼 이전 프레임이 없습니다! 따라서 `vkWaitForFences()`는 결코 일어나지 않을 일을 기다리며 무한정 블로킹됩니다. + +이 딜레마에 대한 많은 해결책 중, API에 내장된 영리한 해결 방법이 있습니다. 펜스를 신호된 상태로 생성하여 첫 번째 `vkWaitForFences()` 호출이 이미 신호된 펜스 덕분에 즉시 반환되도록 하는 것입니다. + +이를 위해 `VkFenceCreateInfo`에 `VK_FENCE_CREATE_SIGNALED_BIT` 플래그를 추가합니다. + +```c++ +void createSyncObjects() { + ... + + VkFenceCreateInfo fenceInfo{}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + + ... +} +``` + +## 스왑 체인에서 이미지 가져오기 + +`drawFrame` 함수에서 다음으로 할 일은 스왑 체인에서 이미지를 가져오는 것입니다. 스왑 체인은 확장 기능이므로 `vk*KHR` 명명 규칙을 가진 함수를 사용해야 한다는 것을 기억하세요. + +```c++ +void drawFrame() { + ... + + uint32_t imageIndex; + vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex); +} +``` + +`vkAcquireNextImageKHR`의 첫 두 파라미터는 로지컬 디바이스와 이미지를 가져올 스왑 체인입니다. 세 번째 파라미터는 이미지가 사용 가능해질 때까지의 타임아웃을 나노초 단위로 지정합니다. 64비트 부호 없는 정수의 최댓값을 사용하면 타임아웃을 효과적으로 비활성화합니다. + +다음 두 파라미터는 프레젠테이션 엔진이 이미지 사용을 마쳤을 때 신호를 보낼 동기화 객체를 지정합니다. 이 시점이 바로 우리가 이미지에 그리기를 시작할 수 있는 때입니다. 세마포어, 펜스 또는 둘 다 지정할 수 있습니다. 여기서는 `imageAvailableSemaphore`를 그 목적으로 사용할 것입니다. + +마지막 파라미터는 사용 가능해진 스왑 체인 이미지의 인덱스를 출력할 변수를 지정합니다. 이 인덱스는 `swapChainImages` 배열의 `VkImage`를 가리킵니다. 우리는 이 인덱스를 사용하여 `VkFramebuffer`를 선택할 것입니다. + +## 커맨드 버퍼 기록하기 + +사용할 스왑 체인 이미지를 지정하는 `imageIndex`를 확보했으므로 이제 커맨드 버퍼를 기록할 수 있습니다. 먼저 커맨드 버퍼에 `vkResetCommandBuffer`를 호출하여 기록할 수 있는 상태인지 확인합니다. + +```c++ +vkResetCommandBuffer(commandBuffer, 0); +``` + +`vkResetCommandBuffer`의 두 번째 파라미터는 `VkCommandBufferResetFlagBits` 플래그입니다. 특별한 작업을 원하지 않으므로 0으로 둡니다. + +이제 `recordCommandBuffer` 함수를 호출하여 원하는 커맨드를 기록합니다. + +```c++ +recordCommandBuffer(commandBuffer, imageIndex); +``` + +완전히 기록된 커맨드 버퍼가 있으므로 이제 제출할 수 있습니다. + +## 커맨드 버퍼 제출하기 + +큐 제출 및 동기화는 `VkSubmitInfo` 구조체의 파라미터를 통해 구성됩니다. + +```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; +``` + +처음 세 파라미터는 실행이 시작되기 전에 대기할 세마포어와 파이프라인의 어느 단계에서 대기할지를 지정합니다. 우리는 이미지가 사용 가능해질 때까지 이미지에 색상을 쓰는 것을 기다리고 싶으므로, 컬러 어태치먼트에 쓰는 그래픽스 파이프라인 단계를 지정합니다. 이론적으로 이는 구현이 이미지가 아직 사용 가능하지 않은 동안에도 버텍스 셰이더 등을 실행 시작할 수 있음을 의미합니다. `waitStages` 배열의 각 항목은 `pWaitSemaphores`의 동일한 인덱스를 가진 세마포어에 해당합니다. + +```c++ +submitInfo.commandBufferCount = 1; +submitInfo.pCommandBuffers = &commandBuffer; +``` + +다음 두 파라미터는 실제로 실행을 위해 제출할 커맨드 버퍼를 지정합니다. 우리는 가지고 있는 단일 커맨드 버퍼를 간단히 제출합니다. + +```c++ +VkSemaphore signalSemaphores[] = {renderFinishedSemaphore}; +submitInfo.signalSemaphoreCount = 1; +submitInfo.pSignalSemaphores = signalSemaphores; +``` + +`signalSemaphoreCount`와 `pSignalSemaphores` 파라미터는 커맨드 버퍼(들)의 실행이 완료되면 신호를 보낼 세마포어를 지정합니다. 우리의 경우 `renderFinishedSemaphore`를 그 목적으로 사용합니다. + +```c++ +if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFence) != VK_SUCCESS) { + throw std::runtime_error("failed to submit draw command buffer!"); +} +``` + +이제 `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{}; +dependency.srcSubpass = VK_SUBPASS_EXTERNAL; +dependency.dstSubpass = 0; +``` + +처음 두 필드는 종속성과 종속되는 서브패스의 인덱스를 지정합니다. 특별한 값 `VK_SUBPASS_EXTERNAL`은 `srcSubpass`에 지정되었는지 `dstSubpass`에 지정되었는지에 따라 렌더 패스 전후의 암시적 서브패스를 나타냅니다. 인덱스 `0`은 우리의 서브패스를 가리키며, 이는 첫 번째이자 유일한 서브패스입니다. `dstSubpass`는 종속성 그래프에서 순환을 방지하기 위해 항상 `srcSubpass`보다 높아야 합니다(서브패스 중 하나가 `VK_SUBPASS_EXTERNAL`이 아닌 경우). + +```c++ +dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; +dependency.srcAccessMask = 0; +``` + +다음 두 필드는 대기할 작업과 이러한 작업이 발생하는 단계를 지정합니다. 우리가 이미지에 접근하기 전에 스왑 체인이 이미지 읽기를 마칠 때까지 기다려야 합니다. 이는 컬러 어태치먼트 출력 단계 자체를 기다림으로써 달성할 수 있습니다. + +```c++ +dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; +dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; +``` + +이것을 기다려야 하는 작업은 컬러 어태치먼트 단계에 있으며, 컬러 어태치먼트 쓰기를 포함합니다. 이러한 설정은 실제로 필요하고 허용될 때까지(즉, 우리가 색상을 쓰기 시작하고 싶을 때까지) 전환이 일어나지 않도록 막아줍니다. + +```c++ +renderPassInfo.dependencyCount = 1; +renderPassInfo.pDependencies = &dependency; +``` + +`VkRenderPassCreateInfo` 구조체에는 종속성 배열을 지정하는 두 개의 필드가 있습니다. + +## 프레젠테이션(Presentation) + +프레임을 그리는 마지막 단계는 결과를 다시 스왑 체인에 제출하여最终적으로 화면에 나타나게 하는 것입니다. 프레젠테이션은 `drawFrame` 함수 끝에서 `VkPresentInfoKHR` 구조체를 통해 구성됩니다. + +```c++ +VkPresentInfoKHR presentInfo{}; +presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + +presentInfo.waitSemaphoreCount = 1; +presentInfo.pWaitSemaphores = signalSemaphores; +``` + +처음 두 파라미터는 `VkSubmitInfo`와 마찬가지로 프레젠테이션이 일어나기 전에 대기할 세마포어를 지정합니다. 우리는 커맨드 버퍼의 실행이 끝나기를, 즉 삼각형이 그려지기를 기다리고 싶으므로 신호를 받을 세마포어를 가져와서 그것들을 기다립니다. 따라서 `signalSemaphores`를 사용합니다. + +```c++ +VkSwapchainKHR swapChains[] = {swapChain}; +presentInfo.swapchainCount = 1; +presentInfo.pSwapchains = swapChains; +presentInfo.pImageIndices = &imageIndex; +``` + +다음 두 파라미터는 이미지를 제시할 스왑 체인과 각 스왑 체인에 대한 이미지의 인덱스를 지정합니다. 이것은 거의 항상 단일 스왑 체인일 것입니다. + +```c++ +presentInfo.pResults = nullptr; // Optional +``` + +`pResults`라는 마지막 선택적 파라미터가 있습니다. 이를 통해 각 개별 스왑 체인에 대해 프레젠테이션이 성공했는지 확인할 `VkResult` 값의 배열을 지정할 수 있습니다. 단일 스왑 체인만 사용하는 경우에는 필요하지 않습니다. 왜냐하면 프레젠테이션 함수의 반환 값을 간단히 사용할 수 있기 때문입니다. + +```c++ +vkQueuePresentKHR(presentQueue, &presentInfo); +``` + +`vkQueuePresentKHR` 함수는 스왑 체인에 이미지를 제시하라는 요청을 제출합니다. 다음 장에서는 `vkAcquireNextImageKHR`와 `vkQueuePresentKHR`에 대한 오류 처리를 추가할 것입니다. 왜냐하면 지금까지 본 함수들과 달리 이 함수들의 실패가 반드시 프로그램 종료를 의미하지는 않기 때문입니다. + +지금까지 모든 것을 올바르게 했다면, 프로그램을 실행했을 때 다음과 유사한 것을 보게 될 것입니다. + +![](/images/triangle.png) + +> 이 색상의 삼각형은 그래픽스 튜토리얼에서 흔히 보던 것과 약간 다를 수 있습니다. 이 튜토리얼은 셰이더가 선형(linear) 색 공간에서 보간하고 나중에 sRGB 색 공간으로 변환하기 때문입니다. 차이점에 대한 논의는 [이 블로그 포스트](https://medium.com/@heypete/hello-triangle-meet-swift-and-wide-color-6f9e246616d9)를 참조하세요. + +만세! 안타깝게도, 유효성 검사 레이어를 활성화하면 프로그램을 닫자마자 충돌하는 것을 보게 될 것입니다. `debugCallback`에서 터미널에 출력된 메시지가 그 이유를 알려줍니다. + +![](/images/semaphore_in_use.png) + +`drawFrame`의 모든 작업은 비동기적이라는 것을 기억하세요. 즉, `mainLoop`에서 루프를 빠져나올 때 드로잉 및 프레젠테이션 작업이 여전히 진행 중일 수 있습니다. 그 와중에 리소스를 정리하는 것은 좋지 않은 생각입니다. + +이 문제를 해결하려면 `mainLoop`를 종료하고 창을 파괴하기 전에 로지컬 디바이스가 작업을 마칠 때까지 기다려야 합니다. + +```c++ +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + drawFrame(); + } + + vkDeviceWaitIdle(device); +} +``` + +특정 커맨드 큐의 작업이 완료될 때까지 `vkQueueWaitIdle`로 기다릴 수도 있습니다. 이 함수들은 매우 초보적인 동기화 방법으로 사용될 수 있습니다. 이제 창을 닫을 때 프로그램이 문제없이 종료되는 것을 볼 수 있을 것입니다. + +## 결론 + +900줄이 넘는 코드를 작성한 끝에 마침내 화면에 무언가 나타나는 단계를 마쳤습니다! Vulkan 프로그램을 부트스트래핑하는 것은 확실히 많은 작업이지만, 여기서 얻을 수 있는 교훈은 Vulkan이 명시성을 통해 엄청난 제어권을 제공한다는 것입니다. 지금 시간을 내어 코드를 다시 읽어보고 프로그램의 모든 Vulkan 객체의 목적과 서로 어떻게 관련되어 있는지에 대한 정신 모델을 구축하는 것을 추천합니다. 이제부터 이 지식을 바탕으로 프로그램의 기능을 확장해 나갈 것입니다. + +다음 장에서는 여러 프레임을 동시에 처리하도록 렌더링 루프를 확장할 것입니다. + +[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 new file mode 100644 index 00000000..54b95e3d --- /dev/null +++ b/ko/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md @@ -0,0 +1,146 @@ +## 동시에 여러 프레임 렌더링하기 (Frames in flight) + +현재 우리의 렌더 루프에는 한 가지 명백한 결함이 있습니다. 이전 프레임의 렌더링이 끝나기를 기다려야만 다음 프레임의 렌더링을 시작할 수 있다는 점인데, 이는 호스트(CPU)의 불필요한 유휴 상태를 유발합니다. + + + +이 문제를 해결하는 방법은 여러 프레임을 동시에 *작업 중(in-flight)* 상태로 두는 것입니다. 즉, 한 프레임의 렌더링이 다음 프레임의 기록을 방해하지 않도록 하는 것입니다. 어떻게 이렇게 할 수 있을까요? 렌더링 중에 접근하고 수정하는 모든 리소스를 복제해야 합니다. 따라서 여러 개의 커맨드 버퍼, 세마포어, 펜스가 필요합니다. 이후 챕터에서는 다른 리소스들의 여러 인스턴스도 추가할 것이므로, 이 개념은 다시 등장하게 될 것입니다. + +먼저 프로그램 상단에 동시에 처리할 프레임 수를 정의하는 상수를 추가합니다. + +```c++ +const int MAX_FRAMES_IN_FLIGHT = 2; +``` + +2를 선택한 이유는 CPU가 GPU보다 *너무* 앞서 나가는 것을 원치 않기 때문입니다. 2개의 프레임이 동시 실행되면, CPU와 GPU가 동시에 각자의 작업을 처리할 수 있습니다. 만약 CPU가 먼저 작업을 마치면, GPU가 렌더링을 마칠 때까지 기다렸다가 다음 작업을 제출합니다. 3개 이상의 프레임을 사용하면 CPU가 GPU를 앞질러 지연 시간(latency)을 추가할 수 있습니다. 일반적으로 추가적인 지연 시간은 바람직하지 않습니다. 하지만 애플리케이션에 동시 실행 프레임 수를 제어할 수 있는 권한을 주는 것은 Vulkan의 명시적인(explicit) 특성을 보여주는 또 다른 예시입니다. + +각 프레임은 자체적인 커맨드 버퍼, 세마포어 집합, 펜스를 가져야 합니다. 기존 객체들의 이름을 바꾸고 `std::vector`로 변경합니다. + +```c++ +std::vector commandBuffers; + +... + +std::vector imageAvailableSemaphores; +std::vector renderFinishedSemaphores; +std::vector inFlightFences; +``` + +다음으로 여러 개의 커맨드 버퍼를 생성해야 합니다. `createCommandBuffer`를 `createCommandBuffers`로 이름을 바꿉니다. 그리고 커맨드 버퍼 벡터의 크기를 `MAX_FRAMES_IN_FLIGHT`로 조절하고, `VkCommandBufferAllocateInfo`가 해당 개수만큼의 커맨드 버퍼를 담도록 수정하며, 할당 대상을 우리의 커맨드 버퍼 벡터로 변경해야 합니다. + +```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!"); + } +} +``` + +`createSyncObjects` 함수는 모든 동기화 객체들을 생성하도록 변경해야 합니다. + +```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!"); + } + } +} +``` + +마찬가지로, 이 객체들도 모두 정리(cleanup)되어야 합니다. + +```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); + } + + ... +} +``` + +커맨드 버퍼는 커맨드 풀이 해제될 때 자동으로 해제되므로, 커맨드 버퍼 정리를 위해 추가로 할 일은 없다는 점을 기억하세요. + +매 프레임마다 올바른 객체를 사용하기 위해, 현재 프레임을 추적해야 합니다. 이를 위해 프레임 인덱스를 사용하겠습니다. + +```c++ +uint32_t currentFrame = 0; +``` + +이제 `drawFrame` 함수를 올바른 객체들을 사용하도록 수정할 수 있습니다. + +```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) { +} +``` + +물론, 매번 다음 프레임으로 넘어가는 것을 잊지 말아야 합니다. + +```c++ +void drawFrame() { + ... + + currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +} +``` + +나머지(%) 연산자를 사용함으로써, 프레임 인덱스는 `MAX_FRAMES_IN_FLIGHT` 만큼의 프레임이 큐에 쌓인 후 다시 순환하게 됩니다. + + + +이제 우리는 최대 `MAX_FRAMES_IN_FLIGHT`개의 프레임만 작업 큐에 쌓이도록 하고, 이 프레임들이 서로를 침범하지 않도록 하는 데 필요한 모든 동기화를 구현했습니다. 최종 정리(cleanup)와 같은 코드의 다른 부분에서는 `vkDeviceWaitIdle`처럼 더 단순한 동기화에 의존해도 괜찮다는 점에 유의하세요. 성능 요구사항에 따라 어떤 접근 방식을 사용할지 결정해야 합니다. + +동기화에 대해 예제를 통해 더 배우고 싶다면, Khronos의 [이 광범위한 개요](https://github.com/KhronosGroup/Vulkan-Docs/wiki/Synchronization-Examples#swapchain-image-acquire-and-present)를 살펴보세요. + +다음 챕터에서는 잘 동작하는 Vulkan 프로그램을 위해 필요한 또 다른 작은 사항을 다룰 것입니다. + +[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 new file mode 100644 index 00000000..54f5aa5b --- /dev/null +++ b/ko/03_Drawing_a_triangle/04_Swap_chain_recreation.md @@ -0,0 +1,235 @@ +## 소개 + +지금까지 우리가 만든 애플리케이션은 성공적으로 삼각형을 그리지만, 아직 제대로 처리하지 못하는 몇 가지 상황이 있습니다. 윈도우 서피스가 변경되어 스왑 체인이 더 이상 호환되지 않는 경우가 발생할 수 있습니다. 이런 상황이 발생하는 원인 중 하나는 윈도우 크기가 변경되는 것입니다. 우리는 이러한 이벤트를 감지하고 스왑 체인을 다시 만들어야 합니다. + +## 스왑 체인 재구성 + +`recreateSwapChain`이라는 새로운 함수를 만들어, `createSwapChain`과 스왑 체인 또는 윈도우 크기에 의존하는 모든 객체들의 생성 함수를 호출하도록 합시다. + +```c++ +void recreateSwapChain() { + vkDeviceWaitIdle(device); + + createSwapChain(); + createImageViews(); + createFramebuffers(); +} +``` + +먼저 `vkDeviceWaitIdle`을 호출하는데, 이는 이전 장에서와 마찬가지로 아직 사용 중일 수 있는 리소스에 접근해서는 안 되기 때문입니다. 당연히 스왑 체인 자체를 다시 만들어야 합니다. 이미지 뷰는 스왑 체인 이미지에 직접 기반하므로 다시 만들어야 합니다. 마지막으로, 프레임버퍼는 스왑 체인 이미지에 직접 의존하므로 다시 만들어야 합니다. + +이러한 객체들의 이전 버전이 재생성되기 전에 확실히 정리되도록, 일부 정리 코드를 별도의 함수로 옮겨 `recreateSwapChain` 함수에서 호출하도록 합시다. 이 함수를 `cleanupSwapChain`이라고 부르겠습니다. + +```c++ +void cleanupSwapChain() { + +} + +void recreateSwapChain() { + vkDeviceWaitIdle(device); + + cleanupSwapChain(); + + createSwapChain(); + createImageViews(); + createFramebuffers(); +} +``` + +여기서는 간단하게 하기 위해 렌더 패스를 다시 만들지 않는다는 점에 유의하세요. 이론적으로는 애플리케이션 실행 중에 스왑 체인 이미지 포맷이 변경될 수 있습니다. 예를 들어, 표준 다이나믹 레인지(SDR) 모니터에서 하이 다이나믹 레인지(HDR) 모니터로 창을 옮기는 경우가 그렇습니다. 이 경우 다이나믹 레인지 간의 변경이 올바르게 반영되도록 애플리케이션이 렌더 패스를 다시 만들어야 할 수도 있습니다. + +스왑 체인 갱신의 일부로 재생성되는 모든 객체들의 정리 코드를 `cleanup`에서 `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(); +} +``` + +`chooseSwapExtent`에서는 이미 새로운 윈도우 해상도를 조회하여 스왑 체인 이미지가 (새로운) 올바른 크기를 갖도록 하고 있으므로, `chooseSwapExtent`를 수정할 필요는 없습니다 (스왑 체인을 만들 때 이미 서피스의 해상도를 픽셀 단위로 얻기 위해 `glfwGetFramebufferSize`를 사용해야 했던 것을 기억하세요). + +이것만으로도 스왑 체인을 재구성할 수 있습니다! 하지만 이 방법의 단점은 새로운 스왑 체인을 만들기 전에 모든 렌더링을 중단해야 한다는 것입니다. 이전 스왑 체인의 이미지에 대한 그리기 명령이 아직 실행 중인 상태에서 새로운 스왑 체인을 만드는 것도 가능합니다. 그러려면 `VkSwapchainCreateInfoKHR` 구조체의 `oldSwapChain` 필드에 이전 스왑 체인을 전달하고, 이전 스왑 체인 사용이 끝나는 즉시 파괴해야 합니다. + +## 준최적(Suboptimal) 또는 오래된(out-of-date) 스왑 체인 + +이제 스왑 체인 재구성이 언제 필요한지 파악하고 새로운 `recreateSwapChain` 함수를 호출하기만 하면 됩니다. 다행히도 Vulkan은 보통 프레젠테이션 중에 스왑 체인이 더 이상 적합하지 않다고 알려줍니다. `vkAcquireNextImageKHR`와 `vkQueuePresentKHR` 함수는 이를 나타내기 위해 다음과 같은 특별한 값들을 반환할 수 있습니다. + +* `VK_ERROR_OUT_OF_DATE_KHR`: 스왑 체인이 서피스와 호환되지 않게 되어 더 이상 렌더링에 사용할 수 없습니다. 보통 윈도우 리사이즈 후에 발생합니다. +* `VK_SUBOPTIMAL_KHR`: 스왑 체인을 여전히 성공적으로 서피스에 표시할 수는 있지만, 서피스의 속성이 더 이상 정확하게 일치하지 않습니다. + +```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!"); +} +``` + +이미지를 가져오려고 할 때 스왑 체인이 오래된(out-of-date) 것으로 판명되면, 더 이상 프레젠테이션을 할 수 없습니다. 따라서 즉시 스왑 체인을 재구성하고 다음 `drawFrame` 호출에서 다시 시도해야 합니다. + +스왑 체인이 준최적(suboptimal)일 때도 재구성을 결정할 수 있지만, 여기서는 이미 이미지를 획득했기 때문에 그대로 진행하기로 했습니다. `VK_SUCCESS`와 `VK_SUBOPTIMAL_KHR` 모두 "성공" 반환 코드로 간주됩니다. + +```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; +``` + +`vkQueuePresentKHR` 함수도 같은 의미로 동일한 값들을 반환합니다. 이 경우에는 최상의 결과를 원하기 때문에 준최적일 때도 스왑 체인을 재구성할 것입니다. + +## 데드락 해결하기 + +지금 코드를 실행하면 데드락이 발생할 수 있습니다. 코드를 디버깅해보면, 애플리케이션이 `vkWaitForFences`에 도달한 후 더 이상 진행하지 못하고 멈추는 것을 발견할 수 있습니다. 이는 `vkAcquireNextImageKHR`가 `VK_ERROR_OUT_OF_DATE_KHR`를 반환할 때, 우리가 스왑 체인을 재구성한 후 `drawFrame`에서 즉시 반환하기 때문입니다. 하지만 그 전에, 현재 프레임의 펜스는 대기 상태에 들어간 후 리셋되었습니다. 우리가 즉시 반환하므로 아무 작업도 제출되지 않고, 따라서 펜스는 절대 신호를 받지 못하게 되어 `vkWaitForFences`가 영원히 멈추게 됩니다. + +다행히 간단한 해결책이 있습니다. 펜스를 리셋하는 것을, 우리가 확실히 작업을 제출할 것이라는 것을 안 이후로 미루는 것입니다. 이렇게 하면, 우리가 일찍 반환하더라도 펜스는 여전히 신호를 받은 상태(signaled)로 남아있어, 다음에 같은 펜스 객체를 사용할 때 `vkWaitForFences`가 데드락을 일으키지 않을 것입니다. + +이제 `drawFrame` 함수의 시작 부분은 다음과 같아야 합니다: +```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!"); +} + +// 작업을 제출할 때만 펜스를 리셋합니다. +vkResetFences(device, 1, &inFlightFences[currentFrame]); +``` + +## 명시적으로 리사이즈 처리하기 + +많은 드라이버와 플랫폼이 윈도우 리사이즈 후 자동으로 `VK_ERROR_OUT_OF_DATE_KHR`를 발생시키지만, 이것이 보장되지는 않습니다. 그래서 우리는 리사이즈를 명시적으로 처리하는 코드를 추가할 것입니다. 먼저 리사이즈가 발생했음을 알리는 플래그 멤버 변수를 추가합니다: + +```c++ +std::vector inFlightFences; + +bool framebufferResized = false; +``` + +그런 다음 `drawFrame` 함수를 이 플래그도 확인하도록 수정해야 합니다: + +```c++ +if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) { + framebufferResized = false; + recreateSwapChain(); +} else if (result != VK_SUCCESS) { + ... +} +``` + +세마포어들이 일관된 상태를 유지하도록 `vkQueuePresentKHR` 이후에 이 작업을 수행하는 것이 중요합니다. 그렇지 않으면 신호를 받은 세마포어가 제대로 대기 상태에 들어가지 못할 수 있습니다. 이제 실제로 리사이즈를 감지하기 위해 GLFW 프레임워크의 `glfwSetFramebufferSizeCallback` 함수를 사용하여 콜백을 설정할 수 있습니다: + +```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) { + +} +``` + +콜백을 `static` 함수로 만드는 이유는 GLFW가 `HelloTriangleApplication` 인스턴스에 대한 올바른 `this` 포인터를 가지고 멤버 함수를 호출하는 방법을 모르기 때문입니다. + +하지만 콜백에서 `GLFWwindow`에 대한 참조를 얻을 수 있으며, 임의의 포인터를 저장할 수 있는 다른 GLFW 함수가 있습니다: `glfwSetWindowUserPointer`: + +```c++ +window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); +glfwSetWindowUserPointer(window, this); +glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); +``` + +이제 이 값은 `glfwGetWindowUserPointer`를 사용하여 콜백 내에서 가져와 플래그를 올바르게 설정할 수 있습니다: + +```c++ +static void framebufferResizeCallback(GLFWwindow* window, int width, int height) { + auto app = reinterpret_cast(glfwGetWindowUserPointer(window)); + app->framebufferResized = true; +} +``` + +이제 프로그램을 실행하고 윈도우 크기를 조절하여 프레임버퍼가 윈도우에 맞게 올바르게 리사이즈되는지 확인해 보세요. + +## 창 최소화 처리하기 + +스왑 체인이 오래될 수 있는 또 다른 경우는 특별한 종류의 윈도우 리사이즈인 창 최소화입니다. 이 경우는 프레임버퍼 크기가 `0`이 되기 때문에 특별합니다. 이 튜토리얼에서는 윈도우가 다시 전경에 올 때까지 일시 중지하는 방식으로 이 문제를 처리할 것입니다. `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); + + ... +} +``` + +초기 `glfwGetFramebufferSize` 호출은 이미 크기가 올바르고 `glfwWaitEvents`가 기다릴 것이 없는 경우를 처리합니다. + +축하합니다, 여러분은 이제 최초의 잘 동작하는(well-behaved) Vulkan 프로그램을 완성했습니다! 다음 장에서는 버텍스 셰이더에 하드코딩된 정점들을 제거하고 실제로 정점 버퍼(vertex buffer)를 사용할 것입니다. + +[C++ 코드](/code/17_swap_chain_recreation.cpp) / +[버텍스 셰이더](/code/09_shader_base.vert) / +[프래그먼트 셰이더](/code/09_shader_base.frag) \ 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 new file mode 100644 index 00000000..69cdefcd --- /dev/null +++ b/ko/04_Vertex_buffers/00_Vertex_input_description.md @@ -0,0 +1,167 @@ +## 소개 + +다음 몇 개의 챕터에서는 버텍스 셰이더에 하드코딩된 정점 데이터를 메모리의 버텍스 버퍼로 교체할 것입니다. 가장 쉬운 접근법으로 시작하여, CPU에서 볼 수 있는(visible) 버퍼를 만들고 `memcpy`를 사용해 정점 데이터를 직접 복사하는 방법을 알아볼 것입니다. 그 후에는 스테이징 버퍼(staging buffer)를 사용해 정점 데이터를 고성능 메모리로 복사하는 방법도 살펴볼 것입니다. + +## 버텍스 셰이더 + +먼저 버텍스 셰이더를 변경하여, 셰이더 코드 자체에 더 이상 정점 데이터를 포함하지 않도록 합니다. 버텍스 셰이더는 `in` 키워드를 사용하여 버텍스 버퍼로부터 입력을 받습니다. + +```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; +} +``` + +`inPosition`과 `inColor` 변수는 **정점 속성(vertex attributes)**입니다. 이들은 우리가 이전에 두 배열을 사용해 수동으로 위치와 색상을 지정했던 것처럼, 버텍스 버퍼에서 정점 단위로 지정되는 속성입니다. 버텍스 셰이더를 다시 컴파일하는 것을 잊지 마세요! + +`fragColor`와 마찬가지로, `layout(location = x)` 어노테이션은 나중에 참조할 수 있도록 입력에 인덱스를 할당합니다. 64비트 벡터인 `dvec3` 같은 일부 타입은 여러 개의 **슬롯(slot)**을 사용한다는 점을 아는 것이 중요합니다. 즉, 그 다음의 인덱스는 최소 2 이상 커야 합니다. + +```glsl +layout(location = 0) in dvec3 inPosition; +layout(location = 2) in vec3 inColor; +``` + +레이아웃 한정자(layout qualifier)에 대한 더 많은 정보는 [OpenGL 위키](https://www.khronos.org/opengl/wiki/Layout_Qualifier_(GLSL))에서 찾을 수 있습니다. + +## 정점 데이터 + +이제 정점 데이터를 셰이더 코드에서 우리 프로그램 코드의 배열로 옮길 것입니다. 먼저 벡터나 행렬 같은 선형대수 관련 타입을 제공하는 GLM 라이브러리를 포함합니다. 이 타입들을 사용하여 위치와 색상 벡터를 지정할 것입니다. + +```c++ +#include +``` + +`Vertex`라는 새로운 구조체를 만들고, 그 안에 버텍스 셰이더에서 사용할 두 속성을 넣습니다. + +```c++ +struct Vertex { + glm::vec2 pos; + glm::vec3 color; +}; +``` + +GLM은 셰이더 언어에서 사용되는 벡터 타입과 정확히 일치하는 C++ 타입을 편리하게 제공합니다. + +```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}} +}; +``` + +이제 `Vertex` 구조체를 사용해 정점 데이터의 배열을 지정합니다. 이전과 정확히 같은 위치와 색상 값을 사용하지만, 이제는 하나의 정점 배열로 결합되었습니다. 이를 **인터리빙(interleaving)** 정점 속성이라고 합니다. + +## 바인딩 서술 (Binding descriptions) + +다음 단계는 이 데이터 포맷이 GPU 메모리에 업로드된 후, 버텍스 셰이더로 어떻게 전달될지를 Vulkan에게 알려주는 것입니다. 이 정보를 전달하기 위해서는 두 가지 종류의 구조체가 필요합니다. + +첫 번째 구조체는 `VkVertexInputBindingDescription`이며, `Vertex` 구조체에 멤버 함수를 추가하여 올바른 데이터로 채우도록 할 것입니다. + +```c++ +struct Vertex { + glm::vec2 pos; + glm::vec3 color; + + static VkVertexInputBindingDescription getBindingDescription() { + VkVertexInputBindingDescription bindingDescription{}; + + return bindingDescription; + } +}; +``` + +정점 바인딩(vertex binding)은 정점들 전체에서 메모리로부터 데이터를 어떤 속도(rate)로 로드할지 서술합니다. 이는 데이터 항목 사이의 바이트 수와 각 정점 또는 각 인스턴스 이후에 다음 데이터 항목으로 이동할지 여부를 지정합니다. + +```c++ +VkVertexInputBindingDescription bindingDescription{}; +bindingDescription.binding = 0; +bindingDescription.stride = sizeof(Vertex); +bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; +``` + +우리의 모든 정점별 데이터는 하나의 배열에 함께 묶여 있으므로, 우리는 하나의 바인딩만 가질 것입니다. `binding` 파라미터는 바인딩 배열에서의 인덱스를 지정합니다. `stride` 파라미터는 한 항목에서 다음 항목까지의 바이트 수를 지정하며, `inputRate` 파라미터는 다음 값 중 하나를 가질 수 있습니다. + +* `VK_VERTEX_INPUT_RATE_VERTEX`: 각 정점마다 다음 데이터 항목으로 이동 +* `VK_VERTEX_INPUT_RATE_INSTANCE`: 각 인스턴스마다 다음 데이터 항목으로 이동 + +우리는 인스턴스 렌더링을 사용하지 않을 것이므로, 정점별(per-vertex) 데이터를 고수할 것입니다. + +## 속성 서술 (Attribute descriptions) + +정점 입력을 처리하는 방법을 서술하는 두 번째 구조체는 `VkVertexInputAttributeDescription`입니다. 이 구조체들을 채우기 위해 `Vertex`에 또 다른 헬퍼 함수를 추가할 것입니다. + +```c++ +#include + +... + +static std::array getAttributeDescriptions() { + std::array attributeDescriptions{}; + + return attributeDescriptions; +} +``` + +함수 프로토타입에서 알 수 있듯이, 이 구조체는 두 개가 될 것입니다. 속성 서술(attribute description) 구조체는 바인딩 서술에서 비롯된 정점 데이터 덩어리(chunk)로부터 어떻게 정점 속성을 추출할지를 서술합니다. 우리는 위치와 색상이라는 두 가지 속성을 가지고 있으므로, 두 개의 속성 서술 구조체가 필요합니다. + +```c++ +attributeDescriptions[0].binding = 0; +attributeDescriptions[0].location = 0; +attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; +attributeDescriptions[0].offset = offsetof(Vertex, pos); +``` + +`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; +attributeDescriptions[1].location = 1; +attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; +attributeDescriptions[1].offset = offsetof(Vertex, color); +``` + +색상 속성도 거의 같은 방식으로 서술됩니다. + +## 파이프라인 정점 입력 + +이제 `createGraphicsPipeline`에서 구조체들을 참조하여, 이 포맷의 정점 데이터를 받도록 그래픽 파이프라인을 설정해야 합니다. `vertexInputInfo` 구조체를 찾아 두 서술을 참조하도록 수정합니다: + +```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(); +``` + +이제 파이프라인은 `vertices` 컨테이너 포맷의 정점 데이터를 받아들여 우리 버텍스 셰이더로 전달할 준비가 되었습니다. 만약 지금 검증 레이어를 활성화한 상태로 프로그램을 실행하면, 바인딩에 연결된 버텍스 버퍼가 없다고 불평하는 것을 볼 수 있을 것입니다. 다음 단계는 버텍스 버퍼를 생성하고 정점 데이터를 그곳으로 옮겨 GPU가 접근할 수 있도록 하는 것입니다. + +[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 new file mode 100644 index 00000000..6afd2c9b --- /dev/null +++ b/ko/04_Vertex_buffers/01_Vertex_buffer_creation.md @@ -0,0 +1,257 @@ +## 소개 + +Vulkan에서 버퍼(Buffer)는 그래픽 카드가 읽을 수 있는 임의의 데이터를 저장하는 데 사용되는 메모리 영역입니다. 이번 장에서 다룰 정점 데이터(vertex data)를 저장하는 데 사용할 수도 있지만, 앞으로의 장에서 살펴볼 다른 많은 목적으로도 사용될 수 있습니다. 지금까지 다뤄온 다른 Vulkan 객체들과는 달리, 버퍼는 스스로 메모리를 할당하지 않습니다. 이전 장들에서 보았듯이 Vulkan API는 프로그래머가 거의 모든 것을 직접 제어하도록 하며, 메모리 관리도 그중 하나입니다. + +## 버퍼 생성 + +`createVertexBuffer`라는 새 함수를 만들고, `initVulkan` 함수에서 `createCommandBuffers` 바로 전에 호출하도록 합시다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); + createVertexBuffer(); + createCommandBuffers(); + createSyncObjects(); +} + +... + +void createVertexBuffer() { + +} +``` + +버퍼를 생성하려면 `VkBufferCreateInfo` 구조체를 채워야 합니다. + +```c++ +VkBufferCreateInfo bufferInfo{}; +bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; +bufferInfo.size = sizeof(vertices[0]) * vertices.size(); +``` + +구조체의 첫 번째 필드는 `size`로, 버퍼의 크기를 바이트 단위로 지정합니다. 정점 데이터의 바이트 크기는 `sizeof`를 사용해 간단하게 계산할 수 있습니다. + +```c++ +bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; +``` + +두 번째 필드는 `usage`로, 버퍼에 있는 데이터가 어떤 목적으로 사용될지를 나타냅니다. 비트 OR 연산을 사용하여 여러 목적을 동시에 지정할 수도 있습니다. 우리의 사용 사례는 정점 버퍼이므로, 이 플래그를 사용합니다. 다른 사용 유형은 향후 장에서 살펴보겠습니다. + +```c++ +bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; +``` + +스왑 체인의 이미지처럼, 버퍼도 특정 큐 패밀리가 소유하거나 여러 큐 패밀리 간에 공유될 수 있습니다. 이 버퍼는 그래픽스 큐에서만 사용할 것이므로, 배타적(exclusive) 접근 방식이면 충분합니다. + +`flags` 매개변수는 희소 버퍼 메모리(sparse buffer memory)를 설정하는 데 사용되며, 지금은 중요하지 않습니다. 기본값인 `0`으로 두겠습니다. + +이제 `vkCreateBuffer`를 사용해 버퍼를 생성할 수 있습니다. 버퍼 핸들을 저장할 클래스 멤버 `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!"); + } +} +``` + +버퍼는 프로그램이 끝날 때까지 렌더링 명령어에서 사용할 수 있어야 하며, 스왑 체인에 종속되지 않으므로 원래의 `cleanup` 함수에서 정리합니다. + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyBuffer(device, vertexBuffer, nullptr); + + ... +} +``` + +## 메모리 요구사항 + +버퍼는 생성되었지만 아직 메모리가 할당되지 않았습니다. 버퍼에 메모리를 할당하는 첫 단계는 이름 그대로인 `vkGetBufferMemoryRequirements` 함수를 사용하여 메모리 요구사항을 쿼리하는 것입니다. + +```c++ +VkMemoryRequirements memRequirements; +vkGetBufferMemoryRequirements(device, vertexBuffer, &memRequirements); +``` + +`VkMemoryRequirements` 구조체에는 세 가지 필드가 있습니다. + +* `size`: 필요한 메모리의 크기(바이트 단위)이며, `bufferInfo.size`와 다를 수 있습니다. +* `alignment`: 할당된 메모리 영역 내에서 버퍼가 시작되는 오프셋(offset)을 바이트 단위로 나타냅니다. `bufferInfo.usage`와 `bufferInfo.flags`에 따라 달라집니다. +* `memoryTypeBits`: 버퍼에 적합한 메모리 타입들의 비트 필드입니다. + +그래픽 카드는 할당할 수 있는 여러 다른 종류의 메모리를 제공합니다. 각 메모리 타입은 허용되는 연산과 성능 특성 면에서 다릅니다. 우리는 버퍼의 요구사항과 우리 애플리케이션의 요구사항을 결합하여 사용할 올바른 메모리 타입을 찾아야 합니다. 이를 위해 `findMemoryType`이라는 새 함수를 만들어 봅시다. + +```c++ +uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) { + +} +``` + +먼저 `vkGetPhysicalDeviceMemoryProperties`를 사용하여 사용 가능한 메모리 타입에 대한 정보를 쿼리해야 합니다. + +```c++ +VkPhysicalDeviceMemoryProperties memProperties; +vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties); +``` + +`VkPhysicalDeviceMemoryProperties` 구조체에는 `memoryTypes`와 `memoryHeaps` 두 개의 배열이 있습니다. 메모리 힙(Memory heap)은 전용 VRAM이나 VRAM이 부족할 때 사용되는 RAM의 스왑 공간과 같은 개별적인 메모리 자원입니다. 다양한 종류의 메모리 타입이 이 힙들 안에 존재합니다. 지금은 메모리 타입 자체에만 신경 쓰고 힙은 신경 쓰지 않겠지만, 이 선택이 성능에 영향을 미칠 수 있다는 점은 상상할 수 있을 것입니다. + +먼저 버퍼 자체에 적합한 메모리 타입을 찾아봅시다. + +```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!"); +``` + +`typeFilter` 매개변수는 적합한 메모리 타입들의 비트 필드를 지정하는 데 사용됩니다. 즉, 단순히 모든 메모리 타입을 순회하며 해당하는 비트가 `1`로 설정되어 있는지 확인하면 적합한 메모리 타입의 인덱스를 찾을 수 있습니다. + +하지만 우리는 정점 버퍼에 적합한 메모리 타입에만 관심 있는 것이 아닙니다. 또한 그 메모리에 정점 데이터를 쓸 수 있어야 합니다. `memoryTypes` 배열은 각 메모리 타입의 힙과 속성을 지정하는 `VkMemoryType` 구조체로 구성됩니다. 이 속성들은 메모리의 특별한 기능들을 정의하는데, 예를 들어 CPU에서 메모리에 쓰기 위해 맵핑(map)할 수 있는 기능이 있습니다. 이 속성은 `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT`으로 표시되며, `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` 속성도 필요합니다. 메모리를 맵핑할 때 그 이유를 알게 될 것입니다. + +이제 이 속성들의 지원 여부도 확인하도록 루프를 수정할 수 있습니다. + +```c++ +for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } +} +``` + +우리가 원하는 속성이 여러 개일 수 있으므로, 비트 AND 연산의 결과가 0이 아닌지 확인하는 것뿐만 아니라, 원하는 속성 비트 필드와 정확히 일치하는지 확인해야 합니다. 버퍼에 적합하면서 우리가 필요로 하는 모든 속성을 가진 메모리 타입이 있다면 그 인덱스를 반환하고, 그렇지 않으면 예외를 던집니다. + +## 메모리 할당 + +이제 올바른 메모리 타입을 결정할 방법을 알았으니, `VkMemoryAllocateInfo` 구조체를 채워 실제로 메모리를 할당할 수 있습니다. + +```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); +``` + +이제 메모리 할당은 크기와 타입을 지정하는 것만큼 간단해졌습니다. 두 값 모두 정점 버퍼의 메모리 요구사항과 원하는 속성으로부터 파생됩니다. 메모리 핸들을 저장할 클래스 멤버를 만들고 `vkAllocateMemory`로 할당합니다. + +```c++ +VkBuffer vertexBuffer; +VkDeviceMemory vertexBufferMemory; + +... + +if (vkAllocateMemory(device, &allocInfo, nullptr, &vertexBufferMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate vertex buffer memory!"); +} +``` + +메모리 할당에 성공했다면, 이제 `vkBindBufferMemory`를 사용하여 이 메모리를 버퍼와 연결할 수 있습니다. + +```c++ +vkBindBufferMemory(device, vertexBuffer, vertexBufferMemory, 0); +``` + +처음 세 매개변수는 이름에서 알 수 있듯이 명확하며, 네 번째 매개변수는 메모리 영역 내의 오프셋입니다. 이 메모리는 이 정점 버퍼를 위해 특별히 할당되었으므로, 오프셋은 간단히 `0`입니다. 만약 오프셋이 0이 아니라면, `memRequirements.alignment`로 나누어떨어져야 합니다. + +물론, C++의 동적 메모리 할당처럼, 이 메모리도 언젠가는 해제되어야 합니다. 버퍼 객체에 바인딩된 메모리는 버퍼가 더 이상 사용되지 않을 때 해제할 수 있으므로, 버퍼가 파괴된 후에 해제하도록 합시다. + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyBuffer(device, vertexBuffer, nullptr); + vkFreeMemory(device, vertexBufferMemory, nullptr); +``` + +## 정점 버퍼 채우기 + +이제 정점 데이터를 버퍼에 복사할 차례입니다. 이는 `vkMapMemory`를 사용하여 버퍼 메모리를 CPU에서 접근 가능한 메모리로 맵핑(mapping)하여 수행됩니다. + +```c++ +void* data; +vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data); +``` + +이 함수를 사용하면 오프셋과 크기로 정의된 지정된 메모리 자원의 영역에 접근할 수 있습니다. 여기서 오프셋과 크기는 각각 `0`과 `bufferInfo.size`입니다. 모든 메모리를 맵핑하기 위해 특별한 값 `VK_WHOLE_SIZE`를 지정할 수도 있습니다. 뒤에서 두 번째 매개변수는 플래그를 지정하는 데 사용될 수 있지만, 현재 API에는 아직 사용 가능한 플래그가 없습니다. 반드시 `0`으로 설정해야 합니다. 마지막 매개변수는 맵핑된 메모리에 대한 포인터의 출력을 지정합니다. + +```c++ +void* data; +vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data); + memcpy(data, vertices.data(), (size_t) bufferInfo.size); +vkUnmapMemory(device, vertexBufferMemory); +``` + +이제 `memcpy`를 사용하여 정점 데이터를 맵핑된 메모리에 복사하고, `vkUnmapMemory`를 사용하여 다시 언맵핑하면 됩니다. 안타깝게도 드라이버가 데이터를 버퍼 메모리로 즉시 복사하지 않을 수 있습니다 (예: 캐싱 때문). 또한 버퍼에 대한 쓰기가 맵핑된 메모리에서 아직 보이지 않을 수도 있습니다. 이 문제를 해결하는 두 가지 방법이 있습니다. + +* `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT`로 표시된 호스트 일관성(host coherent) 메모리 힙을 사용합니다. +* 맵핑된 메모리에 쓴 후 `vkFlushMappedMemoryRanges`를 호출하고, 맵핑된 메모리에서 읽기 전에 `vkInvalidateMappedMemoryRanges`를 호출합니다. + +우리는 첫 번째 접근 방식, 즉 맵핑된 메모리가 항상 할당된 메모리의 내용과 일치하도록 보장하는 방식을 선택했습니다. 이 방식이 명시적인 플러싱(flushing)보다 성능이 약간 저하될 수 있지만, 다음 장에서 왜 이것이 중요하지 않은지 알게 될 것입니다. + +메모리 범위를 플러싱하거나 일관성 있는 메모리 힙을 사용하는 것은 드라이버가 우리의 버퍼 쓰기를 인지한다는 것을 의미하지만, 이것이 GPU에서 실제로 보인다는 것을 의미하지는 않습니다. GPU로의 데이터 전송은 백그라운드에서 발생하는 작업이며, 사양에서는 단순히 [다음 `vkQueueSubmit` 호출 시점에 완료되는 것이 보장된다](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-submission-host-writes)고 명시합니다. + +## 정점 버퍼 바인딩 + +이제 남은 일은 렌더링 작업 중에 정점 버퍼를 바인딩하는 것뿐입니다. 이를 위해 `recordCommandBuffer` 함수를 확장하겠습니다. + +```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); +``` + +`vkCmdBindVertexBuffers` 함수는 이전 장에서 설정한 것과 같은 바인딩에 정점 버퍼를 바인딩하는 데 사용됩니다. 명령어 버퍼 다음의 첫 두 매개변수는 우리가 정점 버퍼를 지정할 바인딩의 시작 오프셋과 개수를 지정합니다. 마지막 두 매개변수는 바인딩할 정점 버퍼의 배열과 정점 데이터를 읽기 시작할 바이트 오프셋을 지정합니다. 또한 `vkCmdDraw` 호출을 수정하여 하드코딩된 숫자 `3` 대신 버퍼에 있는 정점의 수를 전달하도록 해야 합니다. + +이제 프로그램을 실행하면 익숙한 삼각형이 다시 나타날 것입니다. + +![](/images/triangle.png) + +`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}} +}; +``` + +프로그램을 다시 실행하면 다음과 같이 보일 것입니다. + +![](/images/triangle_white.png) + +다음 장에서는 더 나은 성능을 제공하지만 약간의 추가 작업이 필요한, 정점 데이터를 정점 버퍼로 복사하는 다른 방법을 살펴보겠습니다. + +[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 new file mode 100644 index 00000000..3c02bf27 --- /dev/null +++ b/ko/04_Vertex_buffers/02_Staging_buffer.md @@ -0,0 +1,196 @@ +## 소개 + +지금 우리가 사용하는 정점 버퍼는 올바르게 작동하지만, 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) { + 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); +} +``` + +다양한 종류의 버퍼를 생성하는 데 이 함수를 사용할 수 있도록 버퍼 크기, 메모리 속성, 사용 목적을 매개변수로 추가해야 합니다. 마지막 두 매개변수는 핸들을 기록하기 위한 출력 변수입니다. + +이제 `createVertexBuffer`에서 버퍼 생성 및 메모리 할당 코드를 제거하고, 대신 `createBuffer`를 호출할 수 있습니다. + +```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); +} +``` + +프로그램을 실행하여 정점 버퍼가 여전히 제대로 작동하는지 확인하세요. + +## 스테이징 버퍼 사용하기 + +이제 `createVertexBuffer` 함수를 수정하여, 호스트 가시성(host visible) 버퍼는 임시 버퍼로만 사용하고, 디바이스 로컬(device local) 버퍼를 실제 정점 버퍼로 사용하도록 변경하겠습니다. + +```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); +} +``` + +이제 정점 데이터를 매핑하고 복사하기 위해 `stagingBuffer`와 `stagingBufferMemory`를 사용합니다. 이번 장에서는 두 개의 새로운 버퍼 사용 플래그를 사용합니다: + +* `VK_BUFFER_USAGE_TRANSFER_SRC_BIT`: 버퍼가 메모리 전송 연산의 원본(source)으로 사용될 수 있습니다. +* `VK_BUFFER_USAGE_TRANSFER_DST_BIT`: 버퍼가 메모리 전송 연산의 대상(destination)으로 사용될 수 있습니다. + +이제 `vertexBuffer`는 디바이스 로컬 메모리 타입으로 할당됩니다. 이는 일반적으로 우리가 `vkMapMemory`를 사용할 수 없다는 것을 의미합니다. 하지만 `stagingBuffer`에서 `vertexBuffer`로 데이터를 복사할 수는 있습니다. 이를 위해 `stagingBuffer`에는 전송 원본 플래그를, `vertexBuffer`에는 정점 버퍼 사용 플래그와 함께 전송 대상 플래그를 지정해야 합니다. + +이제 한 버퍼의 내용을 다른 버퍼로 복사하는 `copyBuffer` 함수를 작성하겠습니다. + +```c++ +void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { + +} +``` + +메모리 전송 연산은 그리기 명령과 마찬가지로 커맨드 버퍼를 사용하여 실행됩니다. 따라서 먼저 임시 커맨드 버퍼를 할당해야 합니다. 이런 종류의 단기(short-lived) 버퍼를 위해 별도의 커맨드 풀을 만드는 것을 고려할 수 있습니다. 왜냐하면 구현체가 메모리 할당 최적화를 적용할 수 있기 때문입니다. 그 경우 커맨드 풀 생성 시 `VK_COMMAND_POOL_CREATE_TRANSIENT_BIT` 플래그를 사용해야 합니다. + +```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); +} +``` + +그리고 즉시 커맨드 버퍼 기록을 시작합니다. + +```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); +``` + +우리는 이 커맨드 버퍼를 한 번만 사용할 것이며, 복사 작업이 실행 완료될 때까지 함수에서 반환하지 않고 기다릴 것입니다. 드라이버에게 우리의 의도를 `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); +``` + +버퍼의 내용은 `vkCmdCopyBuffer` 명령을 통해 전송됩니다. 이 함수는 원본과 대상 버퍼를 인자로 받고, 복사할 영역의 배열을 받습니다. 이 영역들은 `VkBufferCopy` 구조체로 정의되며, 원본 버퍼 오프셋, 대상 버퍼 오프셋, 그리고 크기로 구성됩니다. `vkMapMemory` 명령과 달리 여기서는 `VK_WHOLE_SIZE`를 지정할 수 없습니다. + +```c++ +vkEndCommandBuffer(commandBuffer); +``` + +이 커맨드 버퍼는 복사 명령만 포함하므로, 바로 기록을 중단할 수 있습니다. 이제 커맨드 버퍼를 실행하여 전송을 완료합니다. + +```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); +``` + +그리기 명령과 달리 이번에는 기다려야 할 이벤트가 없습니다. 단지 버퍼에 대한 전송을 즉시 실행하기만 하면 됩니다. 이 전송이 완료되기를 기다리는 방법에는 다시 두 가지가 있습니다. 펜스(fence)를 사용하고 `vkWaitForFences`로 기다리거나, 단순히 `vkQueueWaitIdle`로 전송 큐가 유휴(idle) 상태가 될 때까지 기다릴 수 있습니다. 펜스를 사용하면 여러 전송을 동시에 스케줄링하고 모든 작업이 완료될 때까지 기다릴 수 있어, 드라이버가 최적화할 더 많은 기회를 가질 수 있습니다. + +```c++ +vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); +``` + +전송 작업에 사용된 커맨드 버퍼를 정리하는 것을 잊지 마세요. + +이제 `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); + +copyBuffer(stagingBuffer, vertexBuffer, bufferSize); +``` + +스테이징 버퍼의 데이터를 디바이스 버퍼로 복사한 후에는 스테이징 버퍼를 정리해야 합니다. + +```c++ + ... + + copyBuffer(stagingBuffer, vertexBuffer, bufferSize); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingBufferMemory, nullptr); +} +``` + +프로그램을 실행하여 익숙한 삼각형이 다시 보이는지 확인하세요. 성능 향상이 지금 당장은 눈에 보이지 않을 수 있지만, 이제 정점 데이터는 고성능 메모리에서 로드되고 있습니다. 이는 앞으로 더 복잡한 지오메트리를 렌더링하기 시작할 때 중요해질 것입니다. + +## 결론 + +실제 애플리케이션에서는 모든 개별 버퍼에 대해 `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 new file mode 100644 index 00000000..87f2f144 --- /dev/null +++ b/ko/04_Vertex_buffers/03_Index_buffer.md @@ -0,0 +1,122 @@ +## 소개 + +실제 애플리케이션에서 렌더링할 3D 메시는 여러 삼각형 간에 정점을 공유하는 경우가 많습니다. 이는 사각형을 그리는 것처럼 간단한 작업에서도 이미 발생합니다. + +![](/images/vertex_vs_index.svg) + +사각형을 그리려면 두 개의 삼각형이 필요하며, 이는 6개의 정점으로 구성된 정점 버퍼가 필요하다는 것을 의미합니다. 문제는 두 정점의 데이터가 중복되어 50%의 중복이 발생한다는 것입니다. 더 복잡한 메시에서는 정점이 평균 3개의 삼각형에서 재사용되므로 이 문제는 더욱 심각해집니다. 이 문제에 대한 해결책은 *인덱스 버퍼(index buffer)*를 사용하는 것입니다. + +인덱스 버퍼는 본질적으로 정점 버퍼에 대한 포인터 배열입니다. 인덱스 버퍼를 사용하면 정점 데이터의 순서를 바꾸고, 여러 정점에 대해 기존 데이터를 재사용할 수 있습니다. 위 그림은 4개의 고유한 정점을 포함하는 정점 버퍼가 있을 때, 사각형을 위한 인덱스 버퍼가 어떻게 보일지를 보여줍니다. 처음 세 개의 인덱스는 오른쪽 위 삼각형을 정의하고, 마지막 세 개의 인덱스는 왼쪽 아래 삼각형의 정점을 정의합니다. + +## 인덱스 버퍼 생성 + +이번 장에서는 정점 데이터를 수정하고 인덱스 데이터를 추가하여 그림과 같은 사각형을 그려보겠습니다. 네 개의 모서리를 나타내도록 정점 데이터를 수정합니다. + +```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}} +}; +``` + +왼쪽 아래 모서리는 빨간색, 오른쪽 아래는 녹색, 오른쪽 위는 파란색, 왼쪽 위는 흰색입니다. `indices`라는 새 배열을 추가하여 인덱스 버퍼의 내용을 나타냅니다. 이 배열은 그림의 인덱스와 일치시켜 오른쪽 위 삼각형과 왼쪽 아래 삼각형을 그려야 합니다. + +```c++ +const std::vector indices = { + 0, 1, 2, 2, 3, 0 +}; +``` + +`vertices`의 항목 수에 따라 인덱스 버퍼에 `uint16_t` 또는 `uint32_t`를 사용할 수 있습니다. 65535개 미만의 고유 정점을 사용하므로 지금은 `uint16_t`를 사용하겠습니다. + +정점 데이터와 마찬가지로, 인덱스도 GPU가 접근할 수 있도록 `VkBuffer`에 업로드해야 합니다. 인덱스 버퍼의 리소스를 저장하기 위해 두 개의 새로운 클래스 멤버를 정의합니다. + +```c++ +VkBuffer vertexBuffer; +VkDeviceMemory vertexBufferMemory; +VkBuffer indexBuffer; +VkDeviceMemory indexBufferMemory; +``` + +이제 추가할 `createIndexBuffer` 함수는 `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); +} +``` + +주목할 만한 차이점은 두 가지뿐입니다. `bufferSize`는 이제 인덱스 수에 인덱스 타입(`uint16_t` 또는 `uint32_t`)의 크기를 곱한 값과 같습니다. `indexBuffer`의 usage는 `VK_BUFFER_USAGE_VERTEX_BUFFER_BIT` 대신 `VK_BUFFER_USAGE_INDEX_BUFFER_BIT`이어야 하는데, 이는 타당한 설정입니다. 그 외의 과정은 정확히 동일합니다. `indices`의 내용을 복사하기 위한 스테이징 버퍼를 만들고, 그 내용을 최종 장치 로컬 인덱스 버퍼로 복사합니다. + +인덱스 버퍼는 정점 버퍼와 마찬가지로 프로그램이 끝날 때 정리해야 합니다. + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyBuffer(device, indexBuffer, nullptr); + vkFreeMemory(device, indexBufferMemory, nullptr); + + vkDestroyBuffer(device, vertexBuffer, nullptr); + vkFreeMemory(device, vertexBufferMemory, nullptr); + + ... +} +``` + +## 인덱스 버퍼 사용하기 + +그리기에 인덱스 버퍼를 사용하는 것은 `recordCommandBuffer`에 두 가지 변경 사항을 수반합니다. 먼저 정점 버퍼에 했던 것처럼 인덱스 버퍼를 바인딩해야 합니다. 차이점은 인덱스 버퍼는 하나만 가질 수 있다는 것입니다. 아쉽게도 각 정점 속성에 대해 서로 다른 인덱스를 사용하는 것은 불가능하므로, 속성 하나만 다르더라도 정점 데이터를 완전히 복제해야 합니다. + +```c++ +vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets); + +vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT16); +``` + +`vkCmdBindIndexBuffer`를 사용하여 인덱스 버퍼를 바인딩하며, 이 함수는 인덱스 버퍼, 버퍼 내의 바이트 오프셋, 그리고 인덱스 데이터의 타입을 매개변수로 받습니다. 앞서 언급했듯이, 가능한 타입은 `VK_INDEX_TYPE_UINT16`과 `VK_INDEX_TYPE_UINT32`입니다. + +인덱스 버퍼를 바인딩하는 것만으로는 아직 아무것도 바뀌지 않으며, Vulkan에 인덱스 버퍼를 사용하도록 지시하기 위해 그리기 명령도 변경해야 합니다. `vkCmdDraw` 라인을 제거하고 `vkCmdDrawIndexed`로 교체합니다. + +```c++ +vkCmdDrawIndexed(commandBuffer, static_cast(indices.size()), 1, 0, 0, 0); +``` + +이 함수 호출은 `vkCmdDraw`와 매우 유사합니다. 처음 두 매개변수는 인덱스의 수와 인스턴스의 수를 지정합니다. 인스턴싱은 사용하지 않으므로 인스턴스는 `1`로 지정합니다. 인덱스의 수는 정점 셰이더로 전달될 정점의 수를 나타냅니다. 다음 매개변수는 인덱스 버퍼로의 오프셋을 지정하며, 값으로 `1`을 사용하면 그래픽 카드가 두 번째 인덱스부터 읽기 시작합니다. 끝에서 두 번째 매개변수는 인덱스 버퍼의 인덱스에 더할 오프셋을 지정합니다. 마지막 매개변수는 인스턴싱을 위한 오프셋을 지정하는데, 우리는 사용하지 않습니다. + +이제 프로그램을 실행하면 다음과 같은 결과가 나타나야 합니다. + +![](/images/indexed_rectangle.png) + +이제 인덱스 버퍼를 사용하여 정점을 재사용함으로써 메모리를 절약하는 방법을 알게 되었습니다. 이는 나중에 복잡한 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 new file mode 100644 index 00000000..6d1e6349 --- /dev/null +++ b/ko/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md @@ -0,0 +1,319 @@ +## 서론 + +이제 우리는 각 정점마다 임의의 어트리뷰트를 버텍스 셰이더로 전달할 수 있게 되었습니다. 하지만 전역 변수는 어떨까요? 이번 장부터는 3D 그래픽스로 넘어가게 되는데, 여기에는 모델-뷰-프로젝션(MVP) 행렬이 필요합니다. 이 행렬을 정점 데이터에 포함시킬 수도 있겠지만, 이는 메모리 낭비이며 변환이 변경될 때마다 정점 버퍼를 업데이트해야 합니다. 변환은 매 프레임마다 쉽게 바뀔 수 있습니다. + +Vulkan에서 이 문제를 해결하는 올바른 방법은 *리소스 디스크립터(resource descriptor)*를 사용하는 것입니다. 디스크립터는 셰이더가 버퍼나 이미지 같은 리소스에 자유롭게 접근할 수 있게 해주는 방법입니다. 우리는 변환 행렬들을 담고 있는 버퍼를 설정하고, 버텍스 셰이더가 디스크립터를 통해 이들에 접근하도록 할 것입니다. 디스크립터 사용은 세 부분으로 구성됩니다: + +* 파이프라인 생성 시 디스크립터 셋 레이아웃 명시 +* 디스크립터 풀에서 디스크립터 셋 할당 +* 렌더링 시 디스크립터 셋 바인딩 + +*디스크립터 셋 레이아웃*은 렌더 패스가 접근할 어태치먼트의 타입을 명시하는 것과 유사하게, 파이프라인이 접근할 리소스의 타입을 명시합니다. *디스크립터 셋*은 프레임버퍼가 렌더 패스 어태치먼트에 바인딩할 실제 이미지 뷰를 지정하는 것과 유사하게, 디스크립터에 바인딩될 실제 버퍼나 이미지 리소스를 지정합니다. 그 후 디스크립터 셋은 정점 버퍼나 프레임버퍼처럼 드로우 커맨드를 위해 바인딩됩니다. + +많은 종류의 디스크립터가 있지만, 이번 장에서는 uniform buffer object (UBO)를 다룰 것입니다. 다른 종류의 디스크립터는 다음 장들에서 살펴보겠지만, 기본적인 과정은 동일합니다. 버텍스 셰이더가 사용하길 원하는 데이터를 다음과 같은 C++ 구조체에 담는다고 가정해 봅시다: + +```c++ +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; +``` + +그러면 우리는 이 데이터를 `VkBuffer`로 복사하고, 버텍스 셰이더에서 uniform buffer object 디스크립터를 통해 다음과 같이 접근할 수 있습니다: + +```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; +} +``` + +우리는 이전 장의 사각형을 3D 공간에서 회전시키기 위해 매 프레임마다 모델, 뷰, 프로젝션 행렬을 업데이트할 것입니다. + +## 버텍스 셰이더 + +위에서 명시된 것처럼 uniform buffer object를 포함하도록 버텍스 셰이더를 수정하세요. MVP 변환에 대해서는 이미 익숙하다고 가정하겠습니다. 만약 익숙하지 않다면, 첫 장에서 언급된 [참고 자료](https://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/)를 확인하세요. + +```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; +} +``` + +`uniform`, `in`, `out` 선언 순서는 중요하지 않습니다. `binding` 지시어는 어트리뷰트의 `location` 지시어와 유사합니다. 우리는 디스크립터 셋 레이아웃에서 이 바인딩을 참조할 것입니다. `gl_Position`을 계산하는 줄은 변환 행렬들을 사용하여 최종 클립 좌표(clip coordinates)를 계산하도록 변경되었습니다. 2D 삼각형과 달리, 클립 좌표의 마지막 성분은 `1`이 아닐 수 있으며, 이는 화면의 최종 정규화된 장치 좌표(normalized device coordinates)로 변환될 때 나눗셈을 유발합니다. 이것은 원근 투영에서 *원근 분할(perspective division)*로 사용되며, 가까운 물체가 멀리 있는 물체보다 더 크게 보이게 하는 데 필수적입니다. + +## 디스크립터 셋 레이아웃 + +다음 단계는 C++ 측에서 UBO를 정의하고, 버텍스 셰이더의 이 디스크립터에 대해 Vulkan에 알려주는 것입니다. + +```c++ +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; +``` + +GLM의 데이터 타입을 사용하면 셰이더의 정의와 정확히 일치시킬 수 있습니다. 행렬의 데이터는 셰이더가 기대하는 방식과 바이너리 호환되므로, 나중에 `UniformBufferObject`를 `VkBuffer`에 `memcpy`하기만 하면 됩니다. + +모든 정점 어트리뷰트와 그 `location` 인덱스에 대해 했던 것처럼, 파이프라인 생성을 위해 셰이더에서 사용되는 모든 디스크립터 바인딩에 대한 세부 정보를 제공해야 합니다. 이 모든 정보를 정의하기 위해 `createDescriptorSetLayout`이라는 새 함수를 설정할 것입니다. 이 함수는 파이프라인 생성 직전에 호출되어야 합니다. 왜냐하면 거기서 필요하기 때문입니다. + +```c++ +void initVulkan() { + ... + createDescriptorSetLayout(); + createGraphicsPipeline(); + ... +} + +... + +void createDescriptorSetLayout() { + +} +``` + +모든 바인딩은 `VkDescriptorSetLayoutBinding` 구조체를 통해 기술되어야 합니다. + +```c++ +void createDescriptorSetLayout() { + VkDescriptorSetLayoutBinding uboLayoutBinding{}; + uboLayoutBinding.binding = 0; + uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + uboLayoutBinding.descriptorCount = 1; +} +``` + +첫 두 필드는 셰이더에서 사용되는 `binding`과 디스크립터의 타입, 즉 uniform buffer object를 명시합니다. 셰이더 변수가 uniform buffer object의 배열을 나타내는 것도 가능하며, `descriptorCount`는 배열에 있는 값의 수를 지정합니다. 예를 들어, 이는 스켈레탈 애니메이션에서 각 뼈에 대한 변환을 지정하는 데 사용될 수 있습니다. 우리의 MVP 변환은 단일 uniform buffer object에 있으므로, `descriptorCount`는 `1`을 사용합니다. + +```c++ +uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; +``` + +우리는 또한 디스크립터가 어느 셰이더 단계에서 참조될 것인지 명시해야 합니다. `stageFlags` 필드는 `VkShaderStageFlagBits` 값들의 조합이거나 `VK_SHADER_STAGE_ALL_GRAPHICS` 값일 수 있습니다. 우리의 경우, 버텍스 셰이더에서만 디스크립터를 참조합니다. + +```c++ +uboLayoutBinding.pImmutableSamplers = nullptr; // Optional +``` + +`pImmutableSamplers` 필드는 이미지 샘플링 관련 디스크립터에만 관련이 있으며, 이는 나중에 살펴볼 것입니다. 이 값은 기본값으로 남겨둘 수 있습니다. + +모든 디스크립터 바인딩은 단일 `VkDescriptorSetLayout` 객체로 결합됩니다. `pipelineLayout` 위에 새로운 클래스 멤버를 정의하세요: + +```c++ +VkDescriptorSetLayout descriptorSetLayout; +VkPipelineLayout pipelineLayout; +``` + +그런 다음 `vkCreateDescriptorSetLayout`을 사용하여 이를 생성할 수 있습니다. 이 함수는 바인딩 배열을 포함하는 간단한 `VkDescriptorSetLayoutCreateInfo`를 받습니다: + +```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!"); +} +``` + +셰이더가 어떤 디스크립터를 사용할지 Vulkan에 알리기 위해 파이프라인 생성 중에 디스크립터 셋 레이아웃을 지정해야 합니다. 디스크립터 셋 레이아웃은 파이프라인 레이아웃 객체에 지정됩니다. 레이아웃 객체를 참조하도록 `VkPipelineLayoutCreateInfo`를 수정하세요: + +```c++ +VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; +pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; +pipelineLayoutInfo.setLayoutCount = 1; +pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout; +``` + +왜 하나의 레이아웃에 모든 바인딩이 포함되어 있는데도 여러 디스크립터 셋 레이아웃을 지정할 수 있는지 궁금할 수 있습니다. 이에 대해서는 다음 장에서 디스크립터 풀과 디스크립터 셋을 다룰 때 다시 돌아올 것입니다. + +디스크립터 셋 레이아웃은 우리가 새로운 그래픽스 파이프라인을 생성할 수 있는 동안, 즉 프로그램이 끝날 때까지 유지되어야 합니다: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); + + ... +} +``` + +## Uniform 버퍼 + +다음 장에서는 셰이더를 위한 UBO 데이터를 담고 있는 버퍼를 명시할 것이지만, 먼저 이 버퍼를 생성해야 합니다. 우리는 매 프레임마다 uniform 버퍼에 새로운 데이터를 복사할 것이므로, 스테이징 버퍼를 사용하는 것은 별 의미가 없습니다. 이 경우 추가적인 오버헤드만 발생시키고 성능을 향상시키기보다는 저하시킬 가능성이 높습니다. + +우리는 여러 개의 버퍼를 가져야 합니다. 왜냐하면 여러 프레임이 동시에 처리 중(in flight)일 수 있고, 이전 프레임이 여전히 버퍼에서 읽고 있는 동안 다음 프레임을 준비하기 위해 버퍼를 업데이트하고 싶지 않기 때문입니다! 따라서, 우리는 동시에 처리 중인 프레임 수만큼 uniform 버퍼를 가져야 하며, 현재 GPU가 읽고 있지 않은 uniform 버퍼에 써야 합니다. + +이를 위해 `uniformBuffers`와 `uniformBuffersMemory`, `uniformBuffersMapped`를 위한 새로운 클래스 멤버를 추가하세요: + +```c++ +VkBuffer indexBuffer; +VkDeviceMemory indexBufferMemory; + +std::vector uniformBuffers; +std::vector uniformBuffersMemory; +std::vector uniformBuffersMapped; +``` + +유사하게, `createIndexBuffer` 다음에 호출되어 버퍼들을 할당하는 `createUniformBuffers`라는 새 함수를 생성하세요: + +```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]); + } +} +``` + +`vkMapMemory`를 사용하여 버퍼를 생성한 직후 매핑하여, 나중에 데이터를 쓸 수 있는 포인터를 얻습니다. 버퍼는 애플리케이션의 전체 수명 동안 이 포인터에 매핑된 상태로 유지됩니다. 이 기법은 **"영구적 매핑(persistent mapping)"**이라고 불리며 모든 Vulkan 구현에서 작동합니다. 업데이트가 필요할 때마다 버퍼를 매핑할 필요가 없으므로 성능이 향상됩니다. 매핑은 공짜가 아니기 때문입니다. + +uniform 데이터는 모든 드로우 콜에 사용되므로, 이를 담고 있는 버퍼는 렌더링을 멈출 때만 파괴되어야 합니다. + +```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); + + ... + +} +``` + +## Uniform 데이터 업데이트 + +`updateUniformBuffer`라는 새 함수를 만들고, 다음 프레임을 제출하기 전에 `drawFrame` 함수에서 이 함수를 호출하도록 추가하세요: + +```c++ +void drawFrame() { + ... + + updateUniformBuffer(currentFrame); + + ... + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + + ... +} + +... + +void updateUniformBuffer(uint32_t currentImage) { + +} +``` + +이 함수는 지오메트리가 회전하도록 매 프레임 새로운 변환을 생성할 것입니다. 이 기능을 구현하기 위해 두 개의 새로운 헤더를 포함해야 합니다: + +```c++ +#define GLM_FORCE_RADIANS +#include +#include + +#include +``` + +`glm/gtc/matrix_transform.hpp` 헤더는 `glm::rotate`와 같은 모델 변환, `glm::lookAt`과 같은 뷰 변환, `glm::perspective`와 같은 프로젝션 변환을 생성하는 데 사용할 수 있는 함수들을 제공합니다. `GLM_FORCE_RADIANS` 정의는 `glm::rotate`와 같은 함수들이 라디안을 인자로 사용하도록 하여 혼동의 여지를 없애는 데 필요합니다. + +`chrono` 표준 라이브러리 헤더는 정밀한 시간 측정을 위한 함수들을 제공합니다. 우리는 이를 사용하여 프레임 속도에 관계없이 지오메트리가 초당 90도 회전하도록 할 것입니다. + +```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(); +} +``` + +`updateUniformBuffer` 함수는 렌더링 시작 후 경과 시간을 부동소수점 정밀도의 초 단위로 계산하는 로직으로 시작합니다. + +이제 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)); +``` + +`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)); +``` + +뷰 변환의 경우, 45도 각도에서 위에서 지오메트리를 바라보도록 결정했습니다. `glm::lookAt` 함수는 눈의 위치, 바라보는 중심점, 위쪽 축을 매개변수로 받습니다. + +```c++ +ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f); +``` + +45도의 수직 시야각을 가진 원근 투영을 사용하기로 결정했습니다. 다른 매개변수는 종횡비(aspect ratio), 근평면(near plane), 원평면(far plane)입니다. 창 크기 조정 후의 새로운 너비와 높이를 고려하여 종횡비를 계산하기 위해 현재 스왑 체인의 크기(`swapChainExtent`)를 사용하는 것이 중요합니다. + +```c++ +ubo.proj[1][1] *= -1; +``` + +GLM은 원래 Y 좌표가 반전된 OpenGL을 위해 설계되었습니다. 이를 보정하는 가장 쉬운 방법은 투영 행렬에서 Y축의 스케일링 팩터 부호를 뒤집는 것입니다. 이렇게 하지 않으면 이미지가 거꾸로 렌더링됩니다. + +이제 모든 변환이 정의되었으므로, uniform buffer object의 데이터를 현재 uniform 버퍼로 복사할 수 있습니다. 이것은 스테이징 버퍼 없이 정점 버퍼에 대해 했던 것과 정확히 같은 방식으로 일어납니다. 앞서 언급했듯이, uniform 버퍼는 한 번만 매핑하므로, 다시 매핑할 필요 없이 직접 쓸 수 있습니다: + +```c++ +memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); +``` + +이런 식으로 UBO를 사용하는 것은 자주 변경되는 값을 셰이더에 전달하는 가장 효율적인 방법은 아닙니다. 작은 데이터 버퍼를 셰이더에 전달하는 더 효율적인 방법은 *푸시 상수(push constants)*입니다. 이에 대해서는 향후 챕터에서 살펴볼 수 있습니다. + +다음 장에서는 디스크립터 셋에 대해 살펴보고, 실제로 `VkBuffer`들을 uniform 버퍼 디스크립터에 바인딩하여 셰이더가 이 변환 데이터에 접근할 수 있도록 할 것입니다. + +[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 new file mode 100644 index 00000000..c0039c07 --- /dev/null +++ b/ko/05_Uniform_buffers/01_Descriptor_pool_and_sets.md @@ -0,0 +1,327 @@ +## 소개 + +이전 장에서 다룬 디스크립터 셋 레이아웃은 바인딩할 수 있는 디스크립터의 유형을 설명합니다. 이번 장에서는 각 `VkBuffer` 리소스마다 디스크립터 셋을 만들어서 유니폼 버퍼 디스크립터에 바인딩할 것입니다. + +## 디스크립터 풀 (Descriptor pool) + +디스크립터 셋은 직접 생성할 수 없으며, 커맨드 버퍼처럼 풀(pool)에서 할당해야 합니다. 디스크립터 셋을 위한 이러한 풀은 놀랍지 않게도 *디스크립터 풀(descriptor pool)*이라고 불립니다. 이를 설정하기 위해 새로운 함수 `createDescriptorPool`을 작성하겠습니다. + +```c++ +void initVulkan() { + ... + createUniformBuffers(); + createDescriptorPool(); + ... +} + +... + +void createDescriptorPool() { + +} +``` + +우선 `VkDescriptorPoolSize` 구조체를 사용해 우리 디스크립터 셋이 어떤 유형의 디스크립터를 얼마나 포함할지 기술해야 합니다. + +```c++ +VkDescriptorPoolSize poolSize{}; +poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +poolSize.descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); +``` + +우리는 프레임마다 하나씩 이 디스크립터를 할당할 것입니다. 이 풀 크기 구조체는 메인 `VkDescriptorPoolCreateInfo`에서 참조됩니다. + +```c++ +VkDescriptorPoolCreateInfo poolInfo{}; +poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; +poolInfo.poolSizeCount = 1; +poolInfo.pPoolSizes = &poolSize; +``` + +개별 디스크립터의 최대 개수 외에도, 할당될 수 있는 디스크립터 셋의 최대 개수도 지정해야 합니다. + +```c++ +poolInfo.maxSets = static_cast(MAX_FRAMES_IN_FLIGHT); +``` + +이 구조체는 커맨드 풀과 유사한 선택적 플래그 `VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT`를 가집니다. 이 플래그는 개별 디스크립터 셋을 해제할 수 있는지 여부를 결정합니다. 우리는 디스크립터 셋을 생성한 후에는 수정하지 않을 것이므로 이 플래그는 필요 없습니다. `flags`는 기본값인 `0`으로 둘 수 있습니다. + +```c++ +VkDescriptorPool descriptorPool; + +... + +if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) { + throw std::runtime_error("failed to create descriptor pool!"); +} +``` + +디스크립터 풀 핸들을 저장할 새 클래스 멤버를 추가하고 `vkCreateDescriptorPool`을 호출하여 생성합니다. + +## 디스크립터 셋 (Descriptor set) + +이제 디스크립터 셋 자체를 할당할 수 있습니다. 이를 위해 `createDescriptorSets` 함수를 추가합시다. + +```c++ +void initVulkan() { + ... + createDescriptorPool(); + createDescriptorSets(); + ... +} + +... + +void createDescriptorSets() { + +} +``` + +`VkDescriptorSetAllocateInfo` 구조체로 디스크립터 셋 할당을 기술합니다. 할당할 디스크립터 풀, 할당할 디스크립터 셋의 개수, 그리고 기반으로 할 디스크립터 셋 레이아웃을 지정해야 합니다. + +```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(); +``` + +우리의 경우, 각 프레임마다 하나의 디스크립터 셋을 생성하며, 모두 동일한 레이아웃을 가집니다. 안타깝게도 다음 함수가 셋의 개수와 일치하는 배열을 기대하기 때문에, 레이아웃의 모든 복사본이 필요합니다. + +디스크립터 셋 핸들을 담을 클래스 멤버를 추가하고 `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!"); +} +``` + +디스크립터 풀이 파괴될 때 자동으로 해제되므로, 디스크립터 셋을 명시적으로 정리할 필요는 없습니다. `vkAllocateDescriptorSets` 호출은 각각 하나의 유니폼 버퍼 디스크립터를 가진 디스크립터 셋들을 할당할 것입니다. + +```c++ +void cleanup() { + ... + vkDestroyDescriptorPool(device, descriptorPool, nullptr); + + vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); + ... +} +``` + +이제 디스크립터 셋은 할당되었지만, 그 안의 디스크립터들은 아직 설정이 필요합니다. 이제 모든 디스크립터를 채우기 위한 루프를 추가합니다. + +```c++ +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + +} +``` + +우리의 유니폼 버퍼 디스크립터처럼 버퍼를 참조하는 디스크립터는 `VkDescriptorBufferInfo` 구조체로 설정합니다. 이 구조체는 버퍼와 디스크립터 데이터를 포함하는 버퍼 내의 영역을 지정합니다. + +```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); +} +``` + +우리처럼 버퍼 전체를 덮어쓰는 경우, `range`에 `VK_WHOLE_SIZE` 값을 사용하는 것도 가능합니다. 디스크립터 설정은 `VkWriteDescriptorSet` 구조체의 배열을 파라미터로 받는 `vkUpdateDescriptorSets` 함수를 사용하여 업데이트됩니다. + +```c++ +VkWriteDescriptorSet descriptorWrite{}; +descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; +descriptorWrite.dstSet = descriptorSets[i]; +descriptorWrite.dstBinding = 0; +descriptorWrite.dstArrayElement = 0; +``` + +첫 두 필드는 업데이트할 디스크립터 셋과 바인딩을 지정합니다. 우리는 유니폼 버퍼 바인딩 인덱스를 `0`으로 지정했습니다. 디스크립터는 배열이 될 수 있으므로, 업데이트를 시작할 배열의 첫 번째 인덱스도 지정해야 합니다. 우리는 배열을 사용하지 않으므로 인덱스는 단순히 `0`입니다. + +```c++ +descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +descriptorWrite.descriptorCount = 1; +``` + +디스크립터 유형을 다시 지정해야 합니다. `dstArrayElement` 인덱스에서 시작하여 배열 내의 여러 디스크립터를 한 번에 업데이트하는 것이 가능합니다. `descriptorCount` 필드는 업데이트하려는 배열 요소의 수를 지정합니다. + +```c++ +descriptorWrite.pBufferInfo = &bufferInfo; +descriptorWrite.pImageInfo = nullptr; // Optional +descriptorWrite.pTexelBufferView = nullptr; // Optional +``` + +마지막 필드는 실제로 디스크립터를 설정하는 `descriptorCount` 개의 구조체 배열을 참조합니다. 세 필드 중 어느 것을 사용해야 하는지는 디스크립터의 유형에 따라 다릅니다. `pBufferInfo` 필드는 버퍼 데이터를 참조하는 디스크립터에, `pImageInfo`는 이미지 데이터를 참조하는 디스크립터에, `pTexelBufferView`는 버퍼 뷰를 참조하는 디스크립터에 사용됩니다. 우리 디스크립터는 버퍼 기반이므로 `pBufferInfo`를 사용합니다. + +```c++ +vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr); +``` + +업데이트는 `vkUpdateDescriptorSets`를 사용하여 적용됩니다. 이 함수는 두 종류의 배열을 파라미터로 받습니다: `VkWriteDescriptorSet` 배열과 `VkCopyDescriptorSet` 배열입니다. 후자는 이름에서 알 수 있듯이 디스크립터를 서로 복사하는 데 사용할 수 있습니다. + +## 디스크립터 셋 사용하기 + +이제 `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); +``` + +정점 및 인덱스 버퍼와 달리, 디스크립터 셋은 그래픽스 파이프라인에만 국한되지 않습니다. 따라서 디스크립터 셋을 그래픽스 파이프라인에 바인딩할지, 컴퓨트 파이프라인에 바인딩할지 지정해야 합니다. 다음 파라미터는 디스크립터가 기반으로 하는 레이아웃입니다. 그 다음 세 파라미터는 첫 번째 디스크립터 셋의 인덱스, 바인딩할 셋의 개수, 그리고 바인딩할 셋의 배열을 지정합니다. 이 부분은 잠시 후에 다시 다루겠습니다. 마지막 두 파라미터는 동적 디스크립터에 사용되는 오프셋 배열을 지정하며, 이는 다음 장에서 살펴보겠습니다. + +지금 프로그램을 실행해보면 안타깝게도 아무것도 보이지 않는 것을 알 수 있습니다. 문제는 투영 행렬에서 Y축을 뒤집었기 때문에, 정점들이 시계 방향 대신 반시계 방향으로 그려진다는 것입니다. 이로 인해 후면 컬링(backface culling)이 작동하여 지오메트리가 그려지지 않게 됩니다. `createGraphicsPipeline` 함수로 가서 `VkPipelineRasterizationStateCreateInfo`의 `frontFace`를 수정하여 이를 바로잡습니다. + +```c++ +rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; +rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; +``` + +프로그램을 다시 실행하면 다음과 같은 화면을 볼 수 있습니다. + +![](/images/spinning_quad.png) + +투영 행렬이 이제 화면 비율을 보정하기 때문에 직사각형이 정사각형으로 변경되었습니다. `updateUniformBuffer`가 화면 크기 조정을 처리하므로 `recreateSwapChain`에서 디스크립터 셋을 다시 만들 필요는 없습니다. + +## 정렬 요구사항 (Alignment requirements) + +지금까지 간과한 한 가지는 C++ 구조체의 데이터가 셰이더의 유니폼 정의와 정확히 어떻게 일치해야 하는가입니다. 단순히 양쪽에서 같은 타입을 사용하는 것으로 충분해 보입니다. + +```c++ +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; + +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; +``` + +하지만 이게 전부가 아닙니다. 예를 들어, 구조체와 셰이더를 다음과 같이 수정해보세요. + +```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; +``` + +셰이더와 프로그램을 다시 컴파일하고 실행하면, 지금까지 작업한 다채로운 사각형이 사라진 것을 발견할 것입니다! 이는 우리가 *정렬 요구사항(alignment requirements)*을 고려하지 않았기 때문입니다. + +Vulkan은 구조체의 데이터가 메모리에서 특정 방식으로 정렬되기를 기대합니다. 예를 들면 다음과 같습니다: + +* 스칼라는 N(32비트 부동소수점의 경우 4바이트)으로 정렬되어야 합니다. +* `vec2`는 2N(8바이트)으로 정렬되어야 합니다. +* `vec3` 또는 `vec4`는 4N(16바이트)으로 정렬되어야 합니다. +* 중첩 구조체는 멤버의 기본 정렬을 16의 배수로 올림한 값으로 정렬되어야 합니다. +* `mat4` 행렬은 `vec4`와 동일한 정렬을 가져야 합니다. + +전체 정렬 요구사항 목록은 [사양서](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap15.html#interfaces-resources-layout)에서 찾을 수 있습니다. + +세 개의 `mat4` 필드만 있던 원래 셰이더는 이미 정렬 요구사항을 충족했습니다. 각 `mat4`는 4 x 4 x 4 = 64바이트 크기이므로 `model`의 오프셋은 0, `view`의 오프셋은 64, `proj`의 오프셋은 128입니다. 이들은 모두 16의 배수이므로 문제가 없었습니다. + +새로운 구조체는 크기가 8바이트뿐인 `vec2`로 시작하여 모든 오프셋을 망가뜨립니다. 이제 `model`의 오프셋은 `8`, `view`는 `72`, `proj`는 `136`이 되어, 어느 것도 16의 배수가 아닙니다. 이 문제를 해결하기 위해 C++11에 도입된 [`alignas`](https://en.cppreference.com/w/cpp/language/alignas) 지정자를 사용할 수 있습니다. + +```c++ +struct UniformBufferObject { + glm::vec2 foo; + alignas(16) glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; +``` + +이제 프로그램을 다시 컴파일하고 실행하면 셰이더가 행렬 값을 다시 올바르게 받는 것을 볼 수 있습니다. + +다행히도 *대부분의* 경우 이러한 정렬 요구사항에 대해 생각하지 않아도 되는 방법이 있습니다. GLM을 포함하기 직전에 `GLM_FORCE_DEFAULT_ALIGNED_GENTYPES`를 정의할 수 있습니다. + +```c++ +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES +#include +``` + +이렇게 하면 GLM이 `vec2`와 `mat4`에 대해 이미 정렬 요구사항이 지정된 버전을 사용하도록 강제합니다. 이 정의를 추가하면 `alignas` 지정자를 제거해도 프로그램이 여전히 작동해야 합니다. + +안타깝게도 이 방법은 중첩 구조체를 사용하기 시작하면 문제가 될 수 있습니다. C++ 코드에서 다음과 같은 정의를 생각해보세요. + +```c++ +struct Foo { + glm::vec2 v; +}; + +struct UniformBufferObject { + Foo f1; + Foo f2; +}; +``` + +그리고 다음 셰이더 정의를 생각해보세요. + +```c++ +struct Foo { + vec2 v; +}; + +layout(binding = 0) uniform UniformBufferObject { + Foo f1; + Foo f2; +} ubo; +``` + +이 경우 `f2`는 중첩 구조체이므로 오프셋이 `16`이어야 하지만, 실제로는 `8`의 오프셋을 갖게 됩니다. 이런 경우에는 정렬을 직접 지정해야 합니다. + +```c++ +struct UniformBufferObject { + Foo f1; + alignas(16) Foo f2; +}; +``` + +이러한 함정들은 항상 정렬을 명시적으로 하는 것이 좋은 이유입니다. 그렇게 하면 정렬 오류의 이상한 증상에 당황하지 않을 것입니다. + +```c++ +struct UniformBufferObject { + alignas(16) glm::mat4 model; + alignas(16) glm::mat4 view; + alignas(16) glm::mat4 proj; +}; +``` + +`foo` 필드를 제거한 후 셰이더를 다시 컴파일하는 것을 잊지 마세요. + +## 여러 개의 디스크립터 셋 (Multiple descriptor sets) + +일부 구조체와 함수 호출에서 암시되었듯이, 여러 디스크립터 셋을 동시에 바인딩하는 것도 가능합니다. 파이프라인 레이아웃을 생성할 때 각 디스크립터 셋에 대한 디스크립터 셋 레이아웃을 지정해야 합니다. 그러면 셰이더는 다음과 같이 특정 디스크립터 셋을 참조할 수 있습니다. + +```c++ +layout(set = 0, binding = 0) uniform UniformBufferObject { ... } +``` + +이 기능을 사용하면 객체별로 다른 디스크립터와 공유되는 디스크립터를 별도의 디스크립터 셋에 넣을 수 있습니다. 이 경우 드로우 콜 간에 대부분의 디스크립터를 다시 바인딩하는 것을 피할 수 있어 잠재적으로 더 효율적입니다. + +[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 new file mode 100644 index 00000000..ae845ea1 --- /dev/null +++ b/ko/06_Texture_mapping/00_Images.md @@ -0,0 +1,561 @@ +## 서론 + +지금까지는 정점별(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** + +`stb_image.h`가 있는 디렉토리를 `Additional Include Directories` 경로에 추가합니다. + +![](/images/include_dirs_stb.png) + +**Makefile** + +`stb_image.h`가 있는 디렉토리를 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) +``` + +## 이미지 로딩하기 + +다음과 같이 이미지 라이브러리를 포함시킵니다: + +```c++ +#define STB_IMAGE_IMPLEMENTATION +#include +``` + +헤더는 기본적으로 함수의 프로토타입만 정의합니다. 하나의 코드 파일에서 `STB_IMAGE_IMPLEMENTATION` 정의와 함께 헤더를 포함해야 함수 본문이 포함되어 링킹 오류가 발생하지 않습니다. + +```c++ +void initVulkan() { + ... + createCommandPool(); + createTextureImage(); + createVertexBuffer(); + ... +} + +... + +void createTextureImage() { + +} +``` + +`createTextureImage`라는 새 함수를 만들어 이미지를 로드하고 Vulkan 이미지 객체에 업로드할 것입니다. 커맨드 버퍼를 사용할 것이므로 `createCommandPool` 이후에 호출되어야 합니다. + +`shaders` 디렉토리 옆에 `textures`라는 새 디렉토리를 만들어 텍스처 이미지를 저장합니다. 해당 디렉토리에서 `texture.jpg`라는 이미지를 로드할 것입니다. 저는 512 x 512 픽셀로 리사이즈된 다음 [CC0 라이선스 이미지](https://pixabay.com/en/statue-sculpture-fig-historically-1275469/)를 사용하기로 했지만, 원하는 이미지를 자유롭게 선택해도 좋습니다. 이 라이브러리는 JPEG, PNG, BMP, GIF와 같은 대부분의 일반적인 이미지 파일 형식을 지원합니다. + +![](/images/texture.jpg) + +이 라이브러리로 이미지를 로드하는 것은 정말 쉽습니다: + +```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!"); + } +} +``` + +`stbi_load` 함수는 파일 경로와 로드할 채널 수를 인자로 받습니다. `STBI_rgb_alpha` 값은 이미지가 알파 채널을 가지고 있지 않더라도 강제로 알파 채널과 함께 로드하도록 하여, 나중에 다른 텍스처와의 일관성을 유지하는 데 좋습니다. 가운데 세 개의 매개변수는 이미지의 너비, 높이, 실제 채널 수에 대한 출력입니다. 반환되는 포인터는 픽셀 값 배열의 첫 번째 요소입니다. 픽셀은 `STBI_rgb_alpha`의 경우 픽셀당 4바이트로 행 단위로 배치되어 총 `texWidth * texHeight * 4`개의 값을 가집니다. + +## 스테이징 버퍼 + +이제 호스트 가시성(host visible) 메모리에 버퍼를 만들어 `vkMapMemory`를 사용하고 픽셀을 복사할 수 있도록 하겠습니다. 이 임시 버퍼를 위한 변수들을 `createTextureImage` 함수에 추가합니다: + +```c++ +VkBuffer stagingBuffer; +VkDeviceMemory stagingBufferMemory; +``` + +버퍼는 매핑할 수 있도록 호스트 가시성 메모리에 있어야 하며, 나중에 이미지로 복사할 수 있도록 전송 소스로 사용될 수 있어야 합니다: + +```c++ +createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); +``` + +그런 다음 이미지 로딩 라이브러리에서 얻은 픽셀 값을 버퍼에 직접 복사할 수 있습니다: + +```c++ +void* data; +vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data); + memcpy(data, pixels, static_cast(imageSize)); +vkUnmapMemory(device, stagingBufferMemory); +``` + +이제 원본 픽셀 배열을 정리하는 것을 잊지 마세요: + +```c++ +stbi_image_free(pixels); +``` + +## 텍스처 이미지 + +셰이더가 버퍼의 픽셀 값에 접근하도록 설정할 수도 있지만, Vulkan에서는 이 목적으로 이미지 객체를 사용하는 것이 더 좋습니다. 이미지 객체는 2D 좌표를 사용할 수 있게 하여 색상을 더 쉽고 빠르게 가져올 수 있게 해줍니다. 이미지 객체 내의 픽셀은 텍셀(texel)이라고 하며, 지금부터 이 용어를 사용하겠습니다. 다음의 새 클래스 멤버를 추가합니다: + +```c++ +VkImage textureImage; +VkDeviceMemory textureImageMemory; +``` + +이미지의 매개변수는 `VkImageCreateInfo` 구조체에 지정됩니다: + +```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; +``` + +`imageType` 필드에 지정된 이미지 타입은 Vulkan에게 이미지의 텍셀이 어떤 종류의 좌표계로 주소 지정될 것인지를 알려줍니다. 1D, 2D, 3D 이미지를 생성할 수 있습니다. 1차원 이미지는 데이터 배열이나 그래디언트를 저장하는 데 사용될 수 있고, 2차원 이미지는 주로 텍스처에 사용되며, 3차원 이미지는 복셀 볼륨 등을 저장하는 데 사용될 수 있습니다. `extent` 필드는 이미지의 차원, 즉 각 축에 얼마나 많은 텍셀이 있는지를 지정합니다. 이것이 `depth`가 `0`이 아닌 `1`이어야 하는 이유입니다. 우리 텍스처는 배열이 아니며, 지금은 밉매핑을 사용하지 않을 것입니다. + +```c++ +imageInfo.format = VK_FORMAT_R8G8B8A8_SRGB; +``` + +Vulkan은 다양한 이미지 형식을 지원하지만, 버퍼의 픽셀과 동일한 형식을 텍셀에 사용해야 합니다. 그렇지 않으면 복사 작업이 실패합니다. + +```c++ +imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; +``` + +`tiling` 필드는 다음 두 가지 값 중 하나를 가질 수 있습니다: + +* `VK_IMAGE_TILING_LINEAR`: 텍셀이 우리의 `pixels` 배열처럼 행 우선 순서(row-major order)로 배치됩니다. +* `VK_IMAGE_TILING_OPTIMAL`: 최적의 접근을 위해 구현에 따라 정의된 순서로 텍셀이 배치됩니다. + +이미지의 레이아웃과 달리 타일링 모드는 나중에 변경할 수 없습니다. 이미지 메모리에서 텍셀에 직접 접근하고 싶다면 `VK_IMAGE_TILING_LINEAR`를 사용해야 합니다. 우리는 스테이징 이미지 대신 스테이징 버퍼를 사용할 것이므로 이것은 필요하지 않습니다. 셰이더에서의 효율적인 접근을 위해 `VK_IMAGE_TILING_OPTIMAL`을 사용할 것입니다. + +```c++ +imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; +``` + +이미지의 `initialLayout`에는 두 가지 가능한 값만 있습니다: + +* `VK_IMAGE_LAYOUT_UNDEFINED`: GPU에서 사용할 수 없으며, 첫 번째 전환 시 텍셀이 폐기됩니다. +* `VK_IMAGE_LAYOUT_PREINITIALIZED`: GPU에서 사용할 수 없지만, 첫 번째 전환 시 텍셀이 보존됩니다. + +첫 번째 전환 중에 텍셀이 보존되어야 하는 경우는 거의 없습니다. 그러나 한 가지 예는 `VK_IMAGE_TILING_LINEAR` 레이아웃과 함께 이미지를 스테이징 이미지로 사용하려는 경우입니다. 이 경우 텍셀 데이터를 업로드한 다음 데이터를 잃지 않고 이미지를 전송 소스로 전환하고 싶을 것입니다. 하지만 우리의 경우에는 먼저 이미지를 전송 대상으로 전환한 다음 버퍼 객체에서 텍셀 데이터를 복사할 것이므로 이 속성이 필요 없으며 `VK_IMAGE_LAYOUT_UNDEFINED`를 안전하게 사용할 수 있습니다. + +```c++ +imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; +``` + +`usage` 필드는 버퍼 생성 시와 동일한 의미를 갖습니다. 이미지는 버퍼 복사의 대상으로 사용될 것이므로 전송 대상으로 설정되어야 합니다. 또한 셰이더에서 이미지에 접근하여 메쉬를 색칠하고 싶으므로 `VK_IMAGE_USAGE_SAMPLED_BIT`를 포함해야 합니다. + +```c++ +imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; +``` + +이미지는 그래픽(따라서 전송도) 작업을 지원하는 단일 큐 패밀리에서만 사용될 것입니다. + +```c++ +imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; +imageInfo.flags = 0; // Optional +``` + +`samples` 플래그는 멀티샘플링과 관련이 있습니다. 이것은 어태치먼트로 사용될 이미지에만 관련이 있으므로 하나의 샘플로 고정합니다. 희소 이미지(sparse images)와 관련된 몇 가지 선택적 플래그가 있습니다. 희소 이미지는 특정 영역만 실제로 메모리에 의해 지원되는 이미지입니다. 예를 들어, 복셀 지형에 3D 텍스처를 사용하는 경우, 이를 사용하여 "공기" 값의 큰 볼륨을 저장하기 위한 메모리 할당을 피할 수 있습니다. 이 튜토리얼에서는 사용하지 않을 것이므로 기본값인 `0`으로 둡니다. + +```c++ +if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) { + throw std::runtime_error("failed to create image!"); +} +``` + +이미지는 `vkCreateImage`를 사용하여 생성되며, 특별히 주목할 만한 매개변수는 없습니다. `VK_FORMAT_R8G8B8A8_SRGB` 형식이 그래픽 하드웨어에서 지원되지 않을 수 있습니다. 수용 가능한 대안 목록을 가지고 지원되는 최상의 것을 선택해야 합니다. 그러나 이 특정 형식에 대한 지원은 매우 널리 퍼져 있으므로 이 단계를 건너뛰겠습니다. 다른 형식을 사용하려면 번거로운 변환이 필요할 수도 있습니다. 깊이 버퍼 장에서 이러한 시스템을 구현할 때 이 문제로 다시 돌아올 것입니다. + +```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); +``` + +이미지용 메모리 할당은 버퍼용 메모리 할당과 정확히 동일한 방식으로 작동합니다. `vkGetBufferMemoryRequirements` 대신 `vkGetImageMemoryRequirements`를 사용하고, `vkBindBufferMemory` 대신 `vkBindImageMemory`를 사용합니다. + +이 함수는 이미 상당히 커지고 있으며, 이후 장에서 더 많은 이미지를 생성할 필요가 있으므로, 버퍼에서 했던 것처럼 이미지 생성을 `createImage` 함수로 추상화해야 합니다. 함수를 만들고 이미지 객체 생성 및 메모리 할당을 그곳으로 옮깁니다: + +```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); +} +``` + +너비, 높이, 형식, 타일링 모드, 사용법 및 메모리 속성을 매개변수로 만들었는데, 이는 이 튜토리얼 전체에서 생성할 이미지마다 달라질 것이기 때문입니다. + +이제 `createTextureImage` 함수를 다음과 같이 단순화할 수 있습니다: + +```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); +} +``` + +## 레이아웃 전환 + +지금 작성할 함수는 커맨드 버퍼를 다시 기록하고 실행하는 것을 포함하므로, 이 로직을 한두 개의 헬퍼 함수로 옮기기에 좋은 시점입니다: + +```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); +} +``` + +이 함수들의 코드는 `copyBuffer`의 기존 코드를 기반으로 합니다. 이제 해당 함수를 다음과 같이 단순화할 수 있습니다: + +```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); +} +``` + +만약 여전히 버퍼를 사용하고 있었다면, `vkCmdCopyBufferToImage`를 기록하고 실행하는 함수를 작성하여 작업을 마칠 수 있었겠지만, 이 명령어는 이미지가 먼저 올바른 레이아웃에 있어야 합니다. 레이아웃 전환을 처리할 새 함수를 만듭니다: + +```c++ +void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout) { + VkCommandBuffer commandBuffer = beginSingleTimeCommands(); + + endSingleTimeCommands(commandBuffer); +} +``` + +레이아웃 전환을 수행하는 가장 일반적인 방법 중 하나는 *이미지 메모리 배리어(image memory barrier)*를 사용하는 것입니다. 이러한 파이프라인 배리어는 일반적으로 리소스 접근을 동기화하는 데 사용됩니다(예: 버퍼에서 읽기 전에 쓰기가 완료되었는지 확인). 하지만 `VK_SHARING_MODE_EXCLUSIVE`가 사용될 때 이미지 레이아웃을 전환하고 큐 패밀리 소유권을 이전하는 데에도 사용할 수 있습니다. 버퍼에 대해 이를 수행하기 위한 동등한 *버퍼 메모리 배리어*가 있습니다. + +```c++ +VkImageMemoryBarrier barrier{}; +barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; +barrier.oldLayout = oldLayout; +barrier.newLayout = newLayout; +``` + +첫 두 필드는 레이아웃 전환을 지정합니다. 이미지의 기존 내용에 신경 쓰지 않는다면 `oldLayout`으로 `VK_IMAGE_LAYOUT_UNDEFINED`를 사용할 수 있습니다. + +```c++ +barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; +barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; +``` + +큐 패밀리 소유권을 이전하기 위해 배리어를 사용하는 경우, 이 두 필드는 큐 패밀리의 인덱스가 되어야 합니다. 이를 원하지 않는 경우 (기본값이 아님!) `VK_QUEUE_FAMILY_IGNORED`로 설정해야 합니다. + +```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; +``` + +`image`와 `subresourceRange`는 영향을 받는 이미지와 이미지의 특정 부분을 지정합니다. 우리 이미지는 배열이 아니고 밉매핑 레벨도 없으므로, 하나의 레벨과 레이어만 지정됩니다. + +```c++ +barrier.srcAccessMask = 0; // TODO +barrier.dstAccessMask = 0; // TODO +``` + +배리어는 주로 동기화 목적으로 사용되므로, 배리어 이전에 발생해야 하는 리소스 관련 작업 유형과 배리어를 기다려야 하는 리소스 관련 작업 유형을 지정해야 합니다. 이미 `vkQueueWaitIdle`을 사용하여 수동으로 동기화하고 있음에도 불구하고 이를 수행해야 합니다. 올바른 값은 이전 및 새 레이아웃에 따라 달라지므로, 어떤 전환을 사용할지 파악한 후에 이 부분으로 돌아오겠습니다. + +```c++ +vkCmdPipelineBarrier( + commandBuffer, + 0 /* TODO */, 0 /* TODO */, + 0, + 0, nullptr, + 0, nullptr, + 1, &barrier +); +``` + +모든 유형의 파이프라인 배리어는 동일한 함수를 사용하여 제출됩니다. 커맨드 버퍼 다음의 첫 번째 매개변수는 배리어 이전에 발생해야 하는 작업이 일어나는 파이프라인 스테이지를 지정합니다. 두 번째 매개변수는 배리어를 기다릴 작업이 일어나는 파이프라인 스테이지를 지정합니다. 배리어 전후에 지정할 수 있는 파이프라인 스테이지는 배리어 전후에 리소스를 어떻게 사용하느냐에 따라 다릅니다. 허용되는 값은 사양의 [이 표](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`)를 파이프라인 스테이지로 지정합니다. 이 유형의 사용법에 대해 비-셰이더 파이프라인 스테이지를 지정하는 것은 의미가 없으며, 유효성 검사 계층은 사용 유형과 일치하지 않는 파이프라인 스테이지를 지정할 때 경고합니다. + +세 번째 매개변수는 `0` 또는 `VK_DEPENDENCY_BY_REGION_BIT`입니다. 후자는 배리어를 영역별 조건으로 바꿉니다. 이는 구현이 예를 들어, 이미 쓰여진 리소스의 부분부터 읽기 시작할 수 있음을 의미합니다. + +마지막 세 쌍의 매개변수는 사용 가능한 세 가지 유형의 파이프라인 배리어(메모리 배리어, 버퍼 메모리 배리어, 그리고 우리가 사용하는 이미지 메모리 배리어)의 배열을 참조합니다. 아직 `VkFormat` 매개변수를 사용하지 않았지만, 깊이 버퍼 장에서 특별한 전환을 위해 사용할 것입니다. + +## 버퍼를 이미지로 복사하기 + +`createTextureImage`로 돌아가기 전에, `copyBufferToImage`라는 헬퍼 함수를 하나 더 작성하겠습니다: + +```c++ +void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) { + VkCommandBuffer commandBuffer = beginSingleTimeCommands(); + + endSingleTimeCommands(commandBuffer); +} +``` + +버퍼 복사와 마찬가지로, 버퍼의 어느 부분을 이미지의 어느 부분으로 복사할지 지정해야 합니다. 이것은 `VkBufferImageCopy` 구조체를 통해 이루어집니다: + +```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 +}; +``` + +이 필드들의 대부분은 자명합니다. `bufferOffset`은 픽셀 값이 시작되는 버퍼의 바이트 오프셋을 지정합니다. `bufferRowLength`와 `bufferImageHeight` 필드는 픽셀이 메모리에 어떻게 배치되는지를 지정합니다. 예를 들어, 이미지의 행 사이에 패딩 바이트가 있을 수 있습니다. 두 필드 모두 `0`으로 지정하면 우리 경우처럼 픽셀이 단순히 빽빽하게 채워져 있음을 나타냅니다. `imageSubresource`, `imageOffset`, `imageExtent` 필드는 픽셀을 이미지의 어느 부분으로 복사할지를 나타냅니다. + +버퍼에서 이미지로의 복사 작업은 `vkCmdCopyBufferToImage` 함수를 사용하여 큐에 추가됩니다: + +```c++ +vkCmdCopyBufferToImage( + commandBuffer, + buffer, + image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, + ®ion +); +``` + +네 번째 매개변수는 이미지가 현재 사용하고 있는 레이아웃을 나타냅니다. 여기서는 이미지가 픽셀을 복사하기에 최적화된 레이아웃으로 이미 전환되었다고 가정합니다. 지금은 픽셀 덩어리 하나를 전체 이미지에 복사하고 있지만, `VkBufferImageCopy`의 배열을 지정하여 이 버퍼에서 이미지로 여러 다른 복사를 한 번의 작업으로 수행할 수도 있습니다. + +## 텍스처 이미지 준비하기 + +이제 텍스처 이미지 설정을 마치는 데 필요한 모든 도구를 갖추었으므로 `createTextureImage` 함수로 돌아갑니다. 거기서 마지막으로 한 일은 텍스처 이미지를 생성하는 것이었습니다. 다음 단계는 스테이징 버퍼를 텍스처 이미지로 복사하는 것입니다. 이는 두 단계로 이루어집니다: + +* 텍스처 이미지를 `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`로 전환 +* 버퍼에서 이미지로의 복사 작업 실행 + +방금 만든 함수들로 이 작업은 쉽게 할 수 있습니다: + +```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)); +``` + +이미지는 `VK_IMAGE_LAYOUT_UNDEFINED` 레이아웃으로 생성되었으므로, `textureImage`를 전환할 때 이전 레이아웃으로 지정되어야 합니다. 복사 작업을 수행하기 전에 그 내용에 신경 쓰지 않기 때문에 이렇게 할 수 있다는 것을 기억하세요. + +셰이더에서 텍스처 이미지로부터 샘플링을 시작하려면, 셰이더 접근을 위해 준비하는 마지막 전환이 한 번 더 필요합니다: + +```c++ +transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); +``` + +## 전환 배리어 마스크 + +이제 유효성 검사 계층을 활성화한 상태로 애플리케이션을 실행하면, `transitionImageLayout`의 접근 마스크와 파이프라인 스테이지가 유효하지 않다고 불평하는 것을 볼 수 있습니다. 전환의 레이아웃에 따라 이들을 아직 설정해야 합니다. + +처리해야 할 두 가지 전환이 있습니다: + +* 정의되지 않음 → 전송 대상: 어떤 것도 기다릴 필요 없는 전송 쓰기 +* 전송 대상 → 셰이더 읽기: 셰이더 읽기는 전송 쓰기를 기다려야 하며, 특히 프래그먼트 셰이더에서의 셰이더 읽기를 기다려야 합니다. 왜냐하면 우리가 텍스처를 사용할 곳이 거기이기 때문입니다. + +이러한 규칙은 다음 접근 마스크와 파이프라인 스테이지를 사용하여 지정됩니다: + +```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 +); +``` + +앞서 언급한 표에서 볼 수 있듯이, 전송 쓰기는 파이프라인 전송 스테이지에서 발생해야 합니다. 쓰기는 어떤 것도 기다릴 필요가 없으므로, 빈 접근 마스크와 배리어 이전 작업을 위한 가장 빠른 파이프라인 스테이지인 `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); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingBufferMemory, nullptr); +} +``` + +주 텍스처 이미지는 프로그램이 끝날 때까지 사용됩니다: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyImage(device, textureImage, nullptr); + vkFreeMemory(device, textureImageMemory, nullptr); + + ... +} +``` + +이제 이미지는 텍스처를 포함하고 있지만, 그래픽 파이프라인에서 접근할 방법이 아직 필요합니다. 다음 장에서 그 작업을 할 것입니다. + +[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 new file mode 100644 index 00000000..2f68f105 --- /dev/null +++ b/ko/06_Texture_mapping/01_Image_view_and_sampler.md @@ -0,0 +1,294 @@ +이번 장에서는 그래픽스 파이프라인이 이미지를 샘플링하는 데 필요한 두 가지 리소스를 더 만들 것입니다. 첫 번째 리소스는 스왑 체인 이미지에서 이미 다루었던 것이지만, 두 번째 리소스는 새로운 것으로 셰이더가 이미지에서 텍셀(texel)을 어떻게 읽을지와 관련이 있습니다. + +## 텍스처 이미지 뷰 + +우리는 이전에 스왑 체인 이미지와 프레임버퍼에서 이미지가 직접 접근되는 대신 이미지 뷰를 통해 접근된다는 것을 보았습니다. 텍스처 이미지에 대해서도 이러한 이미지 뷰를 만들어야 합니다. + +텍스처 이미지의 `VkImageView`를 저장할 클래스 멤버를 추가하고, 이를 생성할 `createTextureImageView` 함수를 새로 만듭니다. + +```c++ +VkImageView textureImageView; + +... + +void initVulkan() { + ... + createTextureImage(); + createTextureImageView(); + createVertexBuffer(); + ... +} + +... + +void createTextureImageView() { + +} +``` + +이 함수의 코드는 `createImageViews` 함수를 거의 그대로 가져와서 만들 수 있습니다. 변경해야 할 부분은 `format`과 `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; +``` + +`VK_COMPONENT_SWIZZLE_IDENTITY`가 어차피 `0`으로 정의되어 있으므로, `viewInfo.components`의 명시적인 초기화는 생략했습니다. `vkCreateImageView`를 호출하여 이미지 뷰 생성을 마칩니다. + +```c++ +if (vkCreateImageView(device, &viewInfo, nullptr, &textureImageView) != VK_SUCCESS) { + throw std::runtime_error("failed to create texture image view!"); +} +``` + +`createImageViews`와 많은 로직이 중복되므로, 이를 새로운 `createImageView` 함수로 추상화할 수 있습니다. + +```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; +} +``` + +이제 `createTextureImageView` 함수는 다음과 같이 단순화할 수 있습니다. + +```c++ +void createTextureImageView() { + textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_SRGB); +} +``` + +그리고 `createImageViews`도 다음과 같이 단순화됩니다. + +```c++ +void createImageViews() { + swapChainImageViews.resize(swapChainImages.size()); + + for (uint32_t i = 0; i < swapChainImages.size(); i++) { + swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat); + } +} +``` + +프로그램이 끝날 때, 이미지 자체를 파괴하기 직전에 이미지 뷰를 파괴하도록 해야 합니다. + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyImageView(device, textureImageView, nullptr); + + vkDestroyImage(device, textureImage, nullptr); + vkFreeMemory(device, textureImageMemory, nullptr); + ... +} +``` + +## 샘플러 + +셰이더가 이미지에서 직접 텍셀을 읽는 것도 가능하지만, 이미지가 텍스처로 사용될 때는 흔한 방식이 아닙니다. 텍스처는 보통 샘플러를 통해 접근되며, 샘플러는 최종적으로 검색될 색상을 계산하기 위해 필터링과 변환을 적용합니다. + +이러한 필터들은 오버샘플링(oversampling) 같은 문제를 해결하는 데 유용합니다. 텍셀보다 더 많은 프래그먼트가 있는 지오메트리에 텍스처가 매핑되는 경우를 생각해보세요. 만약 각 프래그먼트의 텍스처 좌표에 가장 가까운 텍셀을 단순히 가져온다면, 아래 첫 번째 이미지와 같은 결과를 얻게 될 것입니다. + +![](/images/texture_filtering.png) + +만약 가장 가까운 4개의 텍셀을 선형 보간(linear interpolation)으로 혼합한다면, 오른쪽 이미지처럼 더 부드러운 결과를 얻을 수 있습니다. 물론 애플리케이션의 아트 스타일에 따라 왼쪽 스타일(마인크래프트처럼)이 더 적합할 수도 있지만, 일반적인 그래픽스 애플리케이션에서는 오른쪽 방식이 선호됩니다. 샘플러 객체는 텍스처에서 색상을 읽을 때 이 필터링을 자동으로 적용해줍니다. + +언더샘플링(undersampling)은 그 반대의 문제로, 프래그먼트보다 텍셀이 더 많은 경우입니다. 이는 체커보드 텍스처처럼 고주파 패턴을 예리한 각도에서 샘플링할 때 아티팩트를 유발합니다. + +![](/images/anisotropic_filtering.png) + +왼쪽 이미지에서 보듯이, 텍스처가 멀어질수록 흐릿한 덩어리로 변합니다. 이에 대한 해결책은 [비등방성 필터링(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)이며, 이 또한 샘플러에 의해 자동으로 적용될 수 있습니다. + +이러한 필터 외에도, 샘플러는 변환도 처리할 수 있습니다. 샘플러는 *주소 지정 모드(addressing mode)*를 통해 이미지 외부의 텍셀을 읽으려고 할 때 어떤 일이 일어날지를 결정합니다. 아래 이미지는 몇 가지 가능한 옵션을 보여줍니다. + +![](/images/texture_addressing.png) + +이제 이러한 샘플러 객체를 설정하기 위해 `createTextureSampler` 함수를 만들 것입니다. 나중에 셰이더에서 이 샘플러를 사용해 텍스처로부터 색상을 읽게 됩니다. + +```c++ +void initVulkan() { + ... + createTextureImage(); + createTextureImageView(); + createTextureSampler(); + ... +} + +... + +void createTextureSampler() { + +} +``` + +샘플러는 `VkSamplerCreateInfo` 구조체를 통해 구성되며, 이 구조체는 샘플러가 적용해야 할 모든 필터와 변환을 명시합니다. + +```c++ +VkSamplerCreateInfo samplerInfo{}; +samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; +samplerInfo.magFilter = VK_FILTER_LINEAR; +samplerInfo.minFilter = VK_FILTER_LINEAR; +``` + +`magFilter`와 `minFilter` 필드는 텍셀이 확대(magnified)되거나 축소(minified)될 때 어떻게 보간할지를 지정합니다. 확대는 위에서 설명한 오버샘플링 문제와 관련이 있고, 축소는 언더샘플링 문제와 관련이 있습니다. 선택지는 `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; +``` + +주소 지정 모드는 `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 = ???; +``` + +이 두 필드는 비등방성 필터링을 사용할지 여부를 지정합니다. 성능이 우려되는 경우가 아니라면 사용하지 않을 이유가 없습니다. `maxAnisotropy` 필드는 최종 색상을 계산하는 데 사용될 수 있는 텍셀 샘플의 양을 제한합니다. 값이 낮을수록 성능은 좋아지지만 결과물의 품질은 떨어집니다. 우리가 사용할 수 있는 값을 알아내려면, 다음과 같이 물리 장치의 속성을 가져와야 합니다. + +```c++ +VkPhysicalDeviceProperties properties{}; +vkGetPhysicalDeviceProperties(physicalDevice, &properties); +``` + +`VkPhysicalDeviceProperties` 구조체의 문서를 보면 `limits`라는 이름의 `VkPhysicalDeviceLimits` 멤버가 포함되어 있습니다. 이 구조체는 다시 `maxSamplerAnisotropy`라는 멤버를 가지고 있으며, 이것이 `maxAnisotropy`에 지정할 수 있는 최대값입니다. 최고의 품질을 원한다면 이 값을 직접 사용하면 됩니다. + +```c++ +samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy; +``` + +프로그램 시작 시에 속성을 조회하여 필요한 함수에 전달하거나, `createTextureSampler` 함수 내에서 직접 조회할 수 있습니다. + +```c++ +samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; +``` + +`borderColor` 필드는 `clamp to border` 주소 지정 모드로 이미지 외부를 샘플링할 때 반환될 색상을 지정합니다. float 또는 int 형식으로 검은색, 흰색 또는 투명색을 반환할 수 있습니다. 임의의 색상을 지정할 수는 없습니다. + +```c++ +samplerInfo.unnormalizedCoordinates = VK_FALSE; +``` + +`unnormalizedCoordinates` 필드는 이미지의 텍셀 주소를 지정하는 데 사용할 좌표계를 지정합니다. 이 필드가 `VK_TRUE`이면 `[0, texWidth)`와 `[0, texHeight)` 범위 내의 좌표를 그대로 사용할 수 있습니다. `VK_FALSE`이면 텍셀은 모든 축에서 `[0, 1)` 범위의 정규화된 좌표를 사용하여 주소 지정됩니다. 실제 애플리케이션에서는 다양한 해상도의 텍스처를 동일한 좌표로 사용할 수 있기 때문에 거의 항상 정규화된 좌표를 사용합니다. + +```c++ +samplerInfo.compareEnable = VK_FALSE; +samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS; +``` + +비교 함수가 활성화되면, 텍셀은 먼저 특정 값과 비교되고 그 비교 결과가 필터링 연산에 사용됩니다. 이는 주로 섀도 맵의 [백분율 근접 필터링(percentage-closer filtering)](https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch11.html)에 사용됩니다. 이 내용은 나중 장에서 다룰 것입니다. + +```c++ +samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; +samplerInfo.mipLodBias = 0.0f; +samplerInfo.minLod = 0.0f; +samplerInfo.maxLod = 0.0f; +``` + +이 필드들은 모두 밉매핑(mipmapping)에 적용됩니다. 밉매핑은 [다음 장](/Generating_Mipmaps)에서 다룰 것이지만, 기본적으로 적용할 수 있는 또 다른 유형의 필터입니다. + +이제 샘플러의 작동 방식이 완전히 정의되었습니다. 샘플러 객체의 핸들을 저장할 클래스 멤버를 추가하고 `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!"); + } +} +``` + +샘플러는 어디에도 `VkImage`를 참조하지 않는다는 점에 유의하세요. 샘플러는 텍스처에서 색상을 추출하는 인터페이스를 제공하는 별개의 객체입니다. 1D, 2D, 3D 등 원하는 어떤 이미지에도 적용할 수 있습니다. 이는 텍스처 이미지와 필터링을 단일 상태로 결합했던 많은 구형 API와는 다른 점입니다. + +더 이상 이미지를 접근하지 않을 프로그램의 끝에서 샘플러를 파괴합니다. + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroySampler(device, textureSampler, nullptr); + vkDestroyImageView(device, textureImageView, nullptr); + + ... +} +``` + +## 비등방성 장치 기능 + +지금 프로그램을 실행하면 다음과 같은 검증 레이어 메시지를 볼 수 있습니다. + +![](/images/validation_layer_anisotropy.png) + +이는 비등방성 필터링이 사실 선택적(optional) 장치 기능이기 때문입니다. 이를 요청하도록 `createLogicalDevice` 함수를 업데이트해야 합니다. + +```c++ +VkPhysicalDeviceFeatures deviceFeatures{}; +deviceFeatures.samplerAnisotropy = VK_TRUE; +``` + +그리고 최신 그래픽 카드가 이를 지원하지 않을 가능성은 매우 낮지만, `isDeviceSuitable` 함수를 업데이트하여 사용 가능한지 확인해야 합니다. + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + ... + + VkPhysicalDeviceFeatures supportedFeatures; + vkGetPhysicalDeviceFeatures(device, &supportedFeatures); + + return indices.isComplete() && extensionsSupported && swapChainAdequate && supportedFeatures.samplerAnisotropy; +} +``` + +`vkGetPhysicalDeviceFeatures`는 `VkPhysicalDeviceFeatures` 구조체를 재사용하여, 요청된 기능이 아닌 지원되는 기능을 불리언 값으로 설정하여 나타냅니다. + +비등방성 필터링의 사용 가능성을 강제하는 대신, 조건부로 사용하지 않도록 설정할 수도 있습니다. + +```c++ +samplerInfo.anisotropyEnable = VK_FALSE; +samplerInfo.maxAnisotropy = 1.0f; +``` + +다음 장에서는 이미지와 샘플러 객체를 셰이더에 노출하여 사각형에 텍스처를 그릴 것입니다. + +[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 new file mode 100644 index 00000000..2d84eb7e --- /dev/null +++ b/ko/06_Texture_mapping/02_Combined_image_sampler.md @@ -0,0 +1,231 @@ +## 소개 + +우리는 유니폼 버퍼(uniform buffers) 파트에서 처음으로 디스크립터(descriptor)를 살펴보았습니다. 이번 챕터에서는 새로운 유형의 디스크립터인 **결합 이미지 샘플러(combined image sampler)**에 대해 알아보겠습니다. 이 디스크립터를 사용하면 셰이더가 이전 챕터에서 생성한 것과 같은 샘플러 객체를 통해 이미지 리소스에 접근할 수 있습니다. + +우선 디스크립터 셋 레이아웃, 디스크립터 풀, 디스크립터 셋을 수정하여 결합 이미지 샘플러 디스크립터를 포함하는 것부터 시작하겠습니다. 그 후, `Vertex`에 텍스처 좌표를 추가하고 프래그먼트 셰이더를 수정하여 단순히 정점 색상을 보간하는 대신 텍스처에서 색상을 읽도록 할 것입니다. + +## 디스크립터 업데이트하기 + +`createDescriptorSetLayout` 함수로 가서 결합 이미지 샘플러 디스크립터를 위한 `VkDescriptorSetLayoutBinding`을 추가합니다. 단순히 유니폼 버퍼 다음 바인딩에 추가하겠습니다. + +```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(); +``` + +`stageFlags`를 설정하여 프래그먼트 셰이더에서 결합 이미지 샘플러 디스크립터를 사용하려는 의도를 나타내야 합니다. 프래그먼트의 색상이 결정되는 곳이 바로 여기입니다. 버텍스 셰이더에서 텍스처 샘플링을 사용하는 것도 가능합니다. 예를 들어, [하이트맵(heightmap)](https://en.wikipedia.org/wiki/Heightmap)을 사용하여 정점 그리드를 동적으로 변형시킬 수 있습니다. + +또한 `VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER` 타입의 `VkPoolSize`를 `VkDescriptorPoolCreateInfo`에 추가하여 결합 이미지 샘플러 할당을 위한 공간을 만들기 위해 더 큰 디스크립터 풀을 생성해야 합니다. `createDescriptorPool` 함수로 가서 이 디스크립터를 위한 `VkDescriptorPoolSize`를 포함하도록 수정합니다. + +```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); +``` + +부적절한 디스크립터 풀은 검증 레이어가 잡아내지 못하는 문제의 좋은 예입니다. 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++) { + 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; + + ... +} +``` + +결합 이미지 샘플러 구조를 위한 리소스는 유니폼 버퍼 디스크립터의 버퍼 리소스가 `VkDescriptorBufferInfo` 구조체에 지정되는 것과 마찬가지로, `VkDescriptorImageInfo` 구조체에 지정되어야 합니다. 여기서 이전 챕터의 객체들이 함께 사용됩니다. + +```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); +``` + +버퍼와 마찬가지로 이 이미지 정보로 디스크립터를 업데이트해야 합니다. 이번에는 `pBufferInfo` 대신 `pImageInfo` 배열을 사용합니다. 이제 디스크립터는 셰이더에서 사용할 준비가 되었습니다! + +## 텍스처 좌표 + +텍스처 매핑에 있어 아직 빠진 중요한 요소가 하나 있는데, 바로 각 정점에 대한 실제 텍스처 좌표입니다. 텍스처 좌표는 이미지가 지오메트리에 실제로 어떻게 매핑될지 결정합니다. + +```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; + } +}; +``` + +`Vertex` 구조체를 수정하여 텍스처 좌표를 위한 `vec2`를 포함시킵니다. 또한 버텍스 셰이더에서 텍스처 좌표를 입력으로 접근할 수 있도록 `VkVertexInputAttributeDescription`을 추가해야 합니다. 이는 사각형 표면 전체에 걸쳐 보간을 위해 프래그먼트 셰이더로 전달하는 데 필요합니다. + +```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}} +}; +``` + +이 튜토리얼에서는 왼쪽 위 모서리의 `0, 0`에서 오른쪽 아래 모서리의 `1, 1`까지의 좌표를 사용하여 텍스처로 사각형을 채울 것입니다. 자유롭게 다른 좌표로 실험해보세요. `0` 미만 또는 `1` 초과의 좌표를 사용하여 주소 지정 모드가 실제로 어떻게 작동하는지 확인해보세요! + +## 셰이더 + +마지막 단계는 셰이더를 수정하여 텍스처에서 색상을 샘플링하는 것입니다. 먼저 버텍스 셰이더를 수정하여 텍스처 좌표를 프래그먼트 셰이더로 전달해야 합니다. + +```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; +} +``` + +정점별 색상과 마찬가지로 `fragTexCoord` 값은 래스터라이저에 의해 사각형 영역 전체에 걸쳐 부드럽게 보간됩니다. 프래그먼트 셰이더가 텍스처 좌표를 색상으로 출력하게 하여 이를 시각화할 수 있습니다. + +```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); +} +``` + +셰이더를 다시 컴파일하는 것을 잊지 마세요! 아래와 같은 이미지를 보게 될 것입니다. + +![](/images/texcoord_visualization.png) + +녹색 채널은 수평 좌표를, 적색 채널은 수직 좌표를 나타냅니다. 검은색과 노란색 모서리는 텍스처 좌표가 사각형에 걸쳐 `0, 0`에서 `1, 1`까지 올바르게 보간되었음을 확인시켜 줍니다. 색상을 사용한 데이터 시각화는 셰이더 프로그래밍에서 더 나은 대안이 없을 때 사용하는 `printf` 디버깅과 같습니다! + +결합 이미지 샘플러 디스크립터는 GLSL에서 샘플러 유니폼으로 표현됩니다. 프래그먼트 셰이더에 이에 대한 참조를 추가하세요. + +```glsl +layout(binding = 1) uniform sampler2D texSampler; +``` + +다른 유형의 이미지를 위한 `sampler1D` 및 `sampler3D`와 같은 타입도 있습니다. 여기서 올바른 바인딩을 사용해야 합니다. + +```glsl +void main() { + outColor = texture(texSampler, fragTexCoord); +} +``` + +텍스처는 내장 함수 `texture`를 사용하여 샘플링됩니다. 이 함수는 `sampler`와 좌표를 인수로 받습니다. 샘플러는 백그라운드에서 필터링과 변환을 자동으로 처리합니다. 이제 애플리케이션을 실행하면 사각형 위에 텍스처가 표시될 것입니다. + +![](/images/texture_on_square.png) + +텍스처 좌표를 `1`보다 큰 값으로 조정하여 주소 지정 모드를 실험해보세요. 예를 들어, 다음 프래그먼트 셰이더는 `VK_SAMPLER_ADDRESS_MODE_REPEAT`를 사용할 때 아래 이미지와 같은 결과를 생성합니다. + +```glsl +void main() { + outColor = texture(texSampler, fragTexCoord * 2.0); +} +``` + +![](/images/texture_on_square_repeated.png) + +정점 색상을 사용하여 텍스처 색상을 조작할 수도 있습니다. + +```glsl +void main() { + outColor = vec4(fragColor * texture(texSampler, fragTexCoord).rgb, 1.0); +} +``` + +알파 채널이 변하지 않도록 RGB와 알파 채널을 분리했습니다. + +![](/images/texture_on_square_colorized.png) + +이제 셰이더에서 이미지에 접근하는 방법을 알게 되었습니다! 이 기술은 프레임버퍼에 쓰여지기도 하는 이미지와 결합될 때 매우 강력합니다. 이러한 이미지를 입력으로 사용하여 후처리(post-processing)나 3D 세계 내 카메라 디스플레이와 같은 멋진 효과를 구현할 수 있습니다. + +[C++ 코드](/code/26_texture_mapping.cpp) / +[버텍스 셰이더](/code/26_shader_textures.vert) / +[프래그먼트 셰이더](/code/26_shader_textures.frag) \ No newline at end of file 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