Drawing a triangle
Base code
General structure
Resource management
malloc으로 할당된 메모리 청크가 free에 대한 호출을 요구하듯이, 우리가 생성하는 모든 Vulkan object는 그것이 더 이상 필요하지 않을 때 explicitly하게 destroyed 되어질 필요가 있다. 현대 C++ 코드에서, <memory> 헤더에서 utilities를 통해 automatic resource management를 하는 것이 가능하다. 그러나 나는 이 튜토리얼에서 Vulkan objects의 할당과 해제에 대해 explicit하는 것을 선택했다. 결국 Vulkan의 niche는 실수를 피하기 위해 모든 연산에 대해 explicit하는 것이다. 그래서 그 API가 어떻게 작동하는지를 배우기 위해 objects의 lifetime에 대해 explicit하는 것은 좋다.
이 튜토리얼을 따르고나서, 예를들어 std::shared_ptr을 오버로드하여, 자동적인 resource management를 구현할 수 있다. 너의 이득을 위해 RAII를 사용하는 것은 더 큰 Vulkan programs을 위해 추천되는 접근법이지만, 학습 목적을 위해서, scenes 뒤에서 무엇이 일어나고 있는지를 아는 것은 항상 좋다.
Vulkan objects는 vkCreateXX같은 함수들로 직접 생성되거나, vkAllocateXXX같은 함수들로 또 다른 오브젝트를 통해서 할당된다. 한 오브젝트가 어느 곳에서 든지 더 이상 사용되지 않는 것을 확시랗게 한 후에,너는 그것에 대응되는 vkDestroyXXX와 vkFreeXXX로 그것을 destroy할 필요가 있다. 이러한 함수들에 대한 파라미터들은 일반적으로 오브젝트들의 유형에 따라 다양하지만,그것들은 모두 하나의 파라미터를 공유한다 : pAllocator. 이것은 너가 custom memory allocator에 대해 callbacks을 명시하게 해주는 optioanl parameter이다. 우리는 튜토리얼에서 이 파라미터를 무시하고, 항상 인자로 null로 넘길 것이다.
Instance
Creating an instance
- 너가 처음 해야 할 것은 instance를 만들어서 Vulkan 라이브러리를 초기화하는 것이다. 그 인스턴스는 너의 어플리케이션과 벌칸 라이브러리 사이의 연결이고, 그것을 생성하는 것은 너의 어플리케이션에 대해 드라이버에게 몇 가지 세부사항을 명시하는 것을 포함한다.
- 인스턴스를 생성하기 위해, 우리의 어플리케이션에 관한 몇 가지 정보로 struct를 채워야만 한다. 이 데이터는 기술적으로 부가적이지만, 그것은 우리의 구체적인 어플리케이션을 위해 최적화하기 위해 드라이버에게 몇 가지 유용한 정보를 제공할지도 모른다. 예를들어, 그 프로그램이 어떤 특별한 행동을 가진 잘 알려진 그래픽스 엔진을 사용하기 때문이다. 이 구조체는 VkApplicationInfo 라고 불려진다.
- 이전에 언급되었듯이, 벌칸에 있는 많은 구조체들은 너가 sType 멤버에서 그 type을 explicitly하게 명시하는 것을 요구한다. 이것은 또한 나중에 extension 정보를 가리킬 수 있는 pNext 멤버를 가진 많은 구조체중의 하나이다. 우리는 여기에서 그것을 여기에서 nullptr로 두어서 기본 초기화를 할 것이다.
- 벌칸에서 많은 정보는 함수 파라미터 대신에 구조체를 통해서 전달된다. 그래서 우리는 한 instance를 생성하는데 있어 충분한 정보를 제공하기 위해, 한 가지 더 구조체를 채워야만 할 것이다. 이 다음 구조체는 부가적이지 않고, 그 벌칸 드라이버에게 어떤 global extensions과 validation layers를 우리가 사용하고 싶은지를 말한다. Global 이란 것은 여기서 그것들이 특정 디바이스가 아닌 전체 프로그램에 적용될 것이라는 것을 의미하고, 이것들은 다음의 챕터들에서 명확해질 것이다.
- 너가 보게 되듯이, 벌칸에서 오브젝트 생성 함수 파라미터들이 따르는 일반적인 패턴은
- creation info를 가진 구조체에 대한 포인터
- custom allocator callbacks에 대한 포인터, 이 튜토리얼에서는 항상 nullptr
- 그 새로운 오브젝트에 대한 handle를 저장하는 변수에 대한 포인터
- 만약 모든 것이 잘되었다면, instance에 대한 handle이 VkInstance에 저장된다.
Checking for extension support
- 한 instance를 생성하기전에, 지원되는extensions의리스트를 얻기 위해, vkEnumerateInstanceExtensionProperties함수가 있다. 그것은 extensions의 개수를 저장하는 변수에 대한 포인터와, extensions의 세부사항을 저장하는 vkExtensionsProperties의 배열엗 ㅐ한 포인터를 필요한다. 그것은 또한 우리가 특정한 validation layer에 의해 extensions을 필터링하게 해주는 한 가지 부가 파라미터를 필요한다. 그리고 우리는 지금은 이것을 무시한다.
Validation Layers
What are validation layers
- Validation layers는 부가적인 연산을 적용하기 위해 Vulkan 함수 호출에 걸려있는 부가적인 컴포넌트들이다. validation layers에서의 흔한 연산들은:
- 잘못된 사용을 탐지하기 위해 명세에 대해 파라미터의 값을 체크
- resource leaks를 찾기위해 오브젝트들의 생성과 파괴를 추적
- 호출이 출발하는 쓰레드들을 추적하여 thread safety를 체크
- standard output으로 모든 호출과 그것의 파라미터들을 로그화
- 프로파일링과 replaying을 위한 Vulkan calls을 추적
이것은 diagnostics validation layer에서 한 함수의 구현이 어떻게 생겼는지에 대한 예시이다.
VkResult vkCreateInstance( const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkInstance* instance) { if (pCreateInfo == nullptr || instance == nullptr) { log("Null pointer passed to required parameter!"); return VK_ERROR_INITIALIZATION_FAILED; } return real_vkCreateInstance(pCreateInfo, pAllocator, instance); }
- 이러한 validation layers는 너가 관심있어 하는 모든 디버깅 기능들을 포함하도록 자유롭게 쌓아질 수 있다. 너는 간단히 디버그 빌드를 위해 validation layers를 활성화할 수 있고, release builds를 위해 그것들을 완전히 비활성화 할 수 있다. 그리고 이것은 너에게 두 세계에 대해 최상의 것을 준다.
- 벌칸에서 이전에 두 개의 다른 validation layers가 있었다 : instance and device specific. 그 아이디어는 instance layers는 오직 instances같은 global Vulkan objects와 관련된 호출만을 체크하고, device specific layers는 특정한 GPU와 관련된 호출들만을 체크하는 것이다. Device specific layers는 이제 구식이 되었고, instance validation layers가 모든 Vulkan 호출에 대해 적용된다는 것을 의미한다. 명세 문서는 여전히 너가 호환성을 위해 또한 device level에서 validation layers를 활성화 할 것을 추천한다. 그리고 이것은 어떤 구현에 의해 요구된다. 우리는 간단히 logical device level에서의 instance로서 같은 layers를 명시할 것이다. 이것은 나중에 우리가 볼 것이다.
Using validation layers
- explicitly하게 모든 유용한 layers를 명시하는 대신에, 그 SDK는 너가 implicitly하게 유용한 diagnostics layers의 전체를 활성화하는 VK_LAYER_LUNARG_standard_validation layer를 요청하도록 한다.
============================================
Swapchain
~
다음으로, 우리는 여러 개의 queue families에 걸쳐 사용될 swap chain images를 어떻게 처리해야할지를 명시할 필요가 있따. 만약 graphics queue family가 presentation queue와 다르다면 우리의 어플리케이션이 그러한 케이스가 될 것이다. 우리는 graphics queue로부터 swap chain에 있는 이미지에 그릴 것이고, 그러고나서 그것들을 presentation queue로 제출할 것이다. 여러 개의 queues로부터 접근ㄷ뇌는 이미지들을 처리하는 데에 두 가지 방법들이 있다:
- VK_SHARING_MODE_EXCLUSIVE : 한 이미지는 한 time에 한 queue family에 의해 소유되고, 소유권은 다른 queue family에서 그것을 사용하기 전에 explicitly하게 전달되어야 한다. 이 옵션은 최상의 성능을 제공한다.
- VK_SHARING_MODE_CONCURRENT : 이미지들은 explicit ownership transfers 없이 여러 개의 queue families에 걸쳐서 사용되어 질 수 있다.
만약 queue families가 다르다면, 그러면 우리는 이 튜토리얼에서 concurrent mode를 사용할 것인데, ownership chapters를 해야만 하는 것을 피하기 위해서이다. 왜냐하면 이러한 것들은 나중에 더 잘 설명되어지는 몇 가지 개념들을 포함하기 때문이다. Concurrent mode는 어떤 queue families가 ownership을 공유할 것인지를 미리 명시하는것을 요구하는데, queueFamilyIndexCount와 pQueueFamilyIndices 파라미터를 사용한다. 만약 graphics queue family와 presentation queue family가 같다면, 대부분의 하드웨어가 이 겨우일 것인데, 그러면 우리는 exclusive mode를 고수해야만한다. 왜냐하면 concurrent mode는 너가 적어도 두 개의 다른 queue families를 명시하기를 요구하기 때문이다.
우리는 어떤 transform이 swap chain의 이미지들에 적용되어야 하는지를 명시할 수 있는데, 그것이 지원된다면 가능하다 (capabilities에 있는 supportedTransforms). 이것들은 90 degree clockwise rotation 또는 horizontal flip같은 것을 말한다. 어떠한 transformation을 원하지 않는다는 것을 명시하기위해, 간단히 current transformation을 명시해라.
compositeAlpha field는 alpha channel이 window system에 있는 다른 windows와 함께 blending을 위해 사용되어야 하는지를 명시한다. 너는 거의 항상 alpha channel을 무시하기를 원할 것이고,그러므로 VK_COMPOSITE_ALPHA_OPQUE_BIT_KHR 를 써라.
마지막 한 field oldSwapChain이 남았다. Vulkan으로, 어플리케이션이 작동하는 동안 너의 swap chain이 무효해지고, 최적화되지 않는것이 가능한데, 예를들어서 그 window가 resized 되었기 때문이다. 그 경우에, 그 swap chain은 실제로 처음부터 재생성 될 필요가 있따. 그리고 오래된 것에 대한 참조가 이 필드에 명시되어야 한다. 이것은 나중의 챕터에서 우리가 배울 복잡한토픽이다. 지금은, 우리는 오직 하나의 swap chain만을 만들 것이라고 가정할 것이다.
=========================================
Image views
swap chain에 있는 것들을 포함해서, 어떤 VkImage든 사용하기 위해, 우리는 render pipeline에 VkImageView 오브젝트를 생성해야만 한다. 한 image view는 거의 글자 그대로 한 이미지에 대한 view이다. 그것은 그 이미지에 어떻게 접근하는지와, 그 이미지의 어떤 부분을 접근하는지를 설명한다. 예를들어, 만약 그것이 어떠한 mipmapping levels가 없는 2D texture depth texture로서 다뤄져야 한다면.
이 챕터에서, 우리는 swap chain에 있는 모든 이미지에 대해 basic image view를 만드는 createImageViews를 작성할 것인데, 우리는 그것들을 나중에 color targets으로서 사용할 수 있게 하기 위해서이다.
=========================================
Graphics Pipeline Basics - Introduction
다음 몇 챕터 동안, 우리는 우리의 첫 번째 삼각형을 그리기 위해 설정되는 그래픽스 파이프라인을 설정할 것이다. 그래픽스 파이프라인은 너의 meshes의 정점들과 텍스쳐들을 렌더 타겟에 있는 픽셀들로 가져오는 연산의 순서이다.
Input Assembler는 너가 명시한 버퍼로부터 raw vertex data를 모으고, 또한 vertex data 그 자체를 복사할 필요 없이 어떤 elements를 반복하기 위해 index buffer를 또한 사용할지도 모른다.
Vertex Shader는 모든 정점에 대해 작동하고, 일반적으로 model space에서 screen space로 정점 위치를 변환을 적용한다.그것은 또한 vertex마다의 data를 pipeline으로 보낸다.
tessellation shaders는 mesh quality를 증가시키기 위해 너가 어떤 규칙을 기반으로 geometry를 subdivide하게 해준다. 이것은 종종 brick walls같은 표면을 만들기 위해 사용되고, 계단들이 가까이 있을 때 덜 평평해 보인다.
geometry shader는 모든 primitive (triangle, line, point에 대해 작동하고, 그것을 버릴 수 있고, 들어오는 것보다 더 많은 primitives를 만들어 낼수 있다. 이것은 tessellation shader와 비슷하지만, 좀 더 유연성을가진다. 그러나, 오늘날의 어플리케이션에서 많이 사용되지 않는다. 왜냐하면 Intel의 내장 GPU를 제외하고 그 성능이 대부분의 그래픽 카드에서 좋지 않기 때문이다.
rasterization stage는 그 primitives를 fragments로 discretize한다. 이것들은 프레임버퍼에서 그것들이 채우는 pixel elements 이다. 스크린 바깥에 있는 어떤 fragments는 버려지고, vertex shader에 만들어지는 어떤 attributes는 fragments에 걸쳐서 interpolated된다. 보통 다른 primitive fragments 뒤에 있는 fragments는 또한 버려지는데, depth testing 때문이다.
fragment shader는 남아있는 모든 fragment에 대해 호출되고, 그리고 fragments가 어떤 framebuffer(s)에 쓰여졌는지를 결정하고, 어떤 color와 depth values가 쓰여졌는지를 결정한다. vertex shader로 부터의 interpolated data를 사용하여 이것을 할 수 있는데, 그 데이터는 텍스쳐좌표와 라이팅을 위한 normals같은 것들을 포함할 수 있다.
color blending stage는 framebuffer에 있는 같은 픽셀에 매핑되는 다른 fragments를 mix하는 연산을 적용한다. Fragments는 간단히 서로를 overwrite할 수 있고, 더해지거나 또는 transparency를 기반으로 섞여질 수 있다.
green color가 있는 stages들은 fixed-function stages라고 알려져있다. 이러한 단계들은 너가 파라미터를 사용하여 그것들의 연산을 조절할 수 있게 하지만, 그것들이 작동하는 방법은 이미 정의 되어 있다.
한편 orange color로 되어 있는 단계들은 programmable하다. 이것은 너가 너 자신의 코드를 너가 원하는 연산을 정확히 적용하기 위해 그래픽스 카드에 올릴 수 있다는 것을 의미한다. 이것은 예를들어 texturing과 lighting부터 ray tracers 까지의 것을 구현하도록, fragment shaders를사용하게 한다. 이러한 프로그램들은 정점과 fragments같은 많은 오브젝트들을 병렬로 처리하기 위해 많은 GPU cores에서 동시에 작동한다.
만약 너가 OpenGL과 Direct3D같은 더 오래된 API들을 전에 사용했다면, glBlendFunc와 OMSetBlendState 같은 호출들로 어떤 파이프라인 세팅을 변경할 수 있는 것에 이숙할 것이다. 벌칸에서의 그래픽스 파이프라인은 거의 완전히 변경될 수 없다. 그래서 만약 너가 쉐이더를 변경하고, 다른 프레임버퍼를 bind하고 또는 blend function을 바꾸기를 원한다면, 너는 파이프라인을 처음부터 다시 만들어야만 한다. 그 불이점은 너는 너의 렌더링 연산에서 너가 사용하길 원하는 다른 states의 조합 모두를 나타내는 많은 pipelines을 만ㄷ르어야만 할 것이다. 그러나, 너가 그 파이프라인에서 할 모든 연산들이 미리 알려져 있기 때문에, 그 드라이버는 그것에 대해 더 잘 최적화 할 수 있다.
프로그래밍 가능한 몇 단계들은 너가 의도하는 것에 따라 optional하다. 예를들어, tessellation과 geometry 단계들은 만약 너가 단순한 geometry를 그리려한다면 비활성화 될 수 있다. 만약너가 depth values에만 관심이 있다면, 너는 그 fragment shader stage를 비활성화 할 수 있다. 이것은 shadow map generation에 유용하다.
다음 챕터에서, 삼각형을 스크린에 넣는데 요구되는 두 개의 프로그래밍 간으한 단계를 만들 것이다: vertex shader and fragmenet shader. blending mode, viewport, rasterization같은 fixed function 설정은 그 이후의 챕터에서 설정될 것이다. Vulkan에서 그래픽스 파이프라인을 설정하는 최종 부분은 input과 output framebuffers의 명세를 포함한다.
=========================================
Graphics pipeline basics - Shader modules
이전 API들과 다르게, 벌칸의 쉐이더 코드는 GLSL와 HLSL같은 사람이 읽을 수 있는 문장과 반대로 bytecoe format으로 명시되어야만 한다. 이 bytecode format은 SPIR-V라고 불려지고, Vulkan과 OpenCL(둘 다 Khronos APIs이다)과 함께 사용되도록 설계되었다. 그것은 그래픽스 쉐이더와 컴퓨트 쉐이더를 쓰기 위해 사용될 수 있는 포맷이지만, 우리는 이 튜토리얼에서 Vulkan의 graphics pipelines에 사용되는 쉐이더에 집중할 것이다.
bytecode format을 사용하는 이점은, 쉐이더 코드를 native code로 변경하는 GPU vendors에 의해 쓰여진 컴파일러가 상당히 덜 복잡하다는 것이다. 과거에, GLSL 같은 사람이 읽을 수 있는 문장으로, 몇 가지 GPU 회사들은 표준에 대한 자신들만의 해석으로 유S연성을 가지고 있었다는 것을 보여주었다. 만약 너가 이러한 회사들중 하나의 GPU로 중요한 쉐이더를 잘성하려고 한다면, 그러면 너는 synta errors 때문에 너의 코드를 거부하는 다른 회사의 드라이버의 위험을 감수할 것이다. 또는 더 안좋게, 컴파일러 버그 때문에 너의 쉐이더가 다르게 작동할 것이다. SPIR-V 같은 간단한 bytecode format으로, 그것이 바라건대, 피해질 것이다.
그러나, 그것은 우리가 이 바이트 코드를 손으로 써야 할 필요가 있다는 것을의미하지 않는다. Khronos는 GLSL에서 SPIR-V로 컴파일하는 그들 자신의 vendor-independent compiler를 출시했다. 이 컴파일러는 너의 쉐이더 코드가 표준을 완전히 준수하고, 너가 너의 프로그램과 함께 보낼 수 있는 SPIR-V binary를 만들도록 설계되어 있다. 너는 또한 이 컴파일러를 라이브러리 로서, runtime에 SPIR-V를 만들어내도록 포함시킬 수 있지만, 우리는 이 튜토리얼에서 하지 않을 것이다. 그 컴파일러는 이미 LunarG SDK에 glslanValidator.exe로 포함되어 있다. 그래서 너는 추가적을 어떤 것을 다운로드 할 필요가 없다.
Vertex shader
~~ 만약 너가 이전에 OpenGL을 사용했더라면, 그러면 너는 Y 좌표의 기호가 (NDC Space) 이제 flipped 된 것을 눈치챌 것이다. 그 Z 좌표는 이제 Direct3D에서 그렇듯이 같은 범위인 0 ~ 1을 사용한다.
댓글 없음:
댓글 쓰기