Post Lists

레이블이 [Programming] LearnVulkan인 게시물을 표시합니다. 모든 게시물 표시
레이블이 [Programming] LearnVulkan인 게시물을 표시합니다. 모든 게시물 표시

2019년 8월 1일 목요일

Vulkan Tutorial (5)

https://vulkan-tutorial.com/Texture_mapping/Images

Introduction
~
우리의 어플리케이션에 텍스쳐를 추가하는 것은 다음의 스텝을 포함할 것이다:

  •  device memory에 의해 지원되는 image object 생성하기.
  • 한 image file로부터 pixels로 그것을 채우기
  • 한 image sampler 생성하기
  • texture의 colors를 sample하기 위해 combined image sampler  추가

~ Vulkan은 또한 너가 VkBuffer에서 한 이미지로 pixels을 copy하는 것을 허용하고, 이것에 대한 그 API는 실제로 어떤 하드웨어에서는 더 빠르다. 우리는 처음에 이 버퍼를 만들 것이고, 그것을 pixel values로 채울 것이고, 그러고나서 그 pixels을 복사할 image를 생성할 것이다. 한 image를 생성하는 것은 buffers를 생성하는 것과 매우 다르지 않다. 그것은 memory requirements를 query하고, device memory를 할당하고, 그것을 bind하는 것을 포함한다. 이전에 우리가 보았던 것 처럼.

~ Images는 그 pixels이 메모리에서 어떻게 구성되는지에 영향을 미치는 different layout을 가지고 있을 수 있다. 그래픽스 하드웨어가 작동하는 방식에 의해, 간다히 그 pixels을 row마다 저장하는 것은 최상의 성능을 이끌어 내지 않을지도 모른다. images에 대해 어떤 연산을 수행할 때, 너는 그것들이 그 연산에서 최적의 사용의 layout을 가지도록 해야만 한다. 우리는 실제로 이미 render pass를 명시할 떄 이러한 layouts의 몇가지를 보았다:
  • VK_IMAGE_LAYOUT_PRESENT_SRC_KHR : presentation을 위한 optimal
  • VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL : fragment shader로부터 colors를 writing하기위한 attachment로서 Optimal
  • VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : vkCmdCopyImageToBuffer같은, transfer 연산에서 source로서 Optimal
  • VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL : vkCmdCopyBufferToImage같은 transfer operation에서 destination으로서 Optimal
  • VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL : shader로부터 sampling하기 위한 Optimal
한 이미지의 layout을 변환하는 가장 공통된 방법들 중 하나는 pipeline barrier이다. Pipeline barriers는 주로, 한 이미지가 read되기 전에 쓰여지도록 보장하는 것 같은 resources에 대한 접근을 동기화하는데 사용되지만, 그것들은 또한 layouts을 변환하는데 사용될 수 있다. Barriers는 추가적으로 VK_SHARING_MODE_EXCLUSIVE를 사용할 때 queue family ownership을 transfer하는데 사용될 수 있다.

2019년 4월 22일 월요일

Vulkan Tutorial (2)

https://vulkan-tutorial.com/Drawing_a_triangle

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을 사용한다.



















2019년 4월 17일 수요일

Vulkan Tutorial (1)

https://vulkan-tutorial.com/Introduction
https://vulkan-tutorial.com/Overview

공부하면서 기록할 것들을 적어보자.. 완전 번역하기엔 시간이 없으니, 빠르게 요점만 정리하자. Introduction 부분은 그냥 쉽게 그냥 읽으면 된다.

Overview

Origin of Vulkan
- 벌칸은 GPU에 대한 크로스 플랫폼 추상화로서 설계되었다.
- 벌칸은 프로그래머가 좀 더 장황한 API를 사용하여 그것들의 의도를 명확하게 명시하도록 하여 드라이버 오버헤드를 줄이고, 여러 쓰레드들이 병렬로 명령어를 생성하고 제출하도록 한다.
- 벌칸은 단일 컴파일러로 표준화된 바이트 코드 포맷으로 바꾸어서 쉐이더 컴파일에서 비일관성을 줄인다.
- 마지막으로, 벌칸은 그래픽스 와 연산 기능을 단일의 API로 통합하여 현대 그래픽 카드의 일반 목적 처리 능력을 수용한다.

What it takes to draw a triangle
Step1 - Instance and Physical Device Selection
- 벌칸 프로그램은 VkInstance를 통해 Vulkan API를 설정하여 시작한다.
- 한 instance는 너의 프로그램과, 너가 사용하려는 API Extensions을 설명하여 생성된다.
- 인스턴스를 생성한 후에, 너는 벌칸을 지원하는 하드웨어를 쿼리할 수 있고, 연산을 위해 사용될 한 개 이상의 VkPhysicalDevice를 선택할 수 있다.
- 너는 바람직한 장치를 선택하기 위해 VRAM size와 device capabilities같은 특성들을 쿼리할 수 있는데, 예를들어, 전용 그래픽스 카드를 사용하는 것을 선호하기 위해서이다.

Step2 - Logical device and queue families
- 사용할 올바른 하드웨어 장치를 선택한 후에, 너는 VkDevice(logical device)를 생성할 필요가 있다.
- VkDevice에서, 너는 너가 어떤 VkPhysicalDeviceFeatures를 사용할지를 좀 더 구체적으로 설명하게 된다. 그러한 특징들로는 multi viewport rendering과 64 bit floats 같은 것들이 있다.
- 또한 너는 어떤 queue families를 사용하고 싶은지를 명시할 필요가 있다.
- 왜냐하면, draw commands와 memory operations 같은 벌칸으로 수행되는 대부분의 연산들은 그것들을 VkQueue에 제출하여 비동기적으로 실행된다.
- Queues는 queue families로부터 할당되는데, 거기에서 각 queue family는 그것의 queues 안에서 특정한 연산들의 집합을 지원한다.
- 예를들어, 그래픽스, compute and memory transfer operations에 대해 별개의 queue families가 있을 수 있다.
- queue families의 이용가능성은 또한 physical device 선택시에 구분되는 요소로 사용될 수 있다.
- 벌칸 지원을 하는 한 장치가 어떠한 그래픽스 기능을 제공하지 않는 것이 가능하지만, 그러나 벌칸 지원을 가진 모든 그래픽 카드는 오늘날 일반적으로 우리가 관심있어 하는 모든 queue operations를 지원한다.

Step3 - Window surface and swap chain
-우리는 한 window에 실제로 렌더링할 두 개 이상의 컴포넌트들이 필요하다 : window surface(VkSurfaceKHR) 과 swap chain (VkSwapchainKHR). KHR 후위 표기에 주목해라. 그것은 이러한 오브젝트들이 Vulkan extension의 일부라는 것을 의미한다.
- 벌칸 API는 완전히 platform-agnostic인데, 우리가 표준화된 WSI(Window System Interface) extension을 window manager와 상호작용하기 우해 사용할 필요가 있는 이유이다. 그 surface는 렌더링할 windows에 대해 크로스 플랫폼 추상화이고, 일반적으로 native window handle에 대한 reference를 제공하여 instantiated 된다. 윈도우즈에서 예를들어 HWND이다. 운 좋게도, GLFW 라이브러리는 이 것에 대해 플랫폼 특정한 세부사항을 다루는 내장 함수를 가진다.
- swap chain은 렌더 타겟의 집합이다. 그것의 기본 목적은 우리가 현재 렌더링하고 있는 이미지를 혀냊 스크린에 있는 것과 다르게 하도록 하는 것이다.완전한 이미지들만 보이게 하는 것이 중요하다.
- 완성된 이미지를 스크린에 보이기 위해 렌더 타겟과 조건에 대한 개수는 present mode에 달려있다. 보통의 present modes는 double buffering (vsync)와 triple buffering이다.
- 어떤 플랫폼은 VH_KHR_display와 VK_KHR_display_swapchain을 통해 어떤 윈도우 매니저들과도 상호작용 하는 것 없이 디스플레이에 직접 렌더링하게 해준다.

Step4 - Image views and framebuffers
- swap chain으로부터 얻어진 이미지를 그리기 위해, 우리는 그것을 VkImageView와 VkFramebuffer에 넣어야 한다. 한 image view는 사용될 한 이미지의 특정한 부분을 참조하고, 한 프레임버퍼는 color, depth and stencil targets을 위해 사용될 image views를 참조한다. swap chain에 많은 다른 이미지들이 있을 수 있기 때문에, 우리는 우선적으로 한 image view와 그것들 각각에 대해 프레임버퍼를 만들 것이고, draw time에 올바른 것을 선택할 것이다.

Step5 - Render passes
벌칸에서의 Render passes는 렌더링 연산동안에 상요되는 이미지들의 유형들을 설명하고, 그것들이 어떻게 사용되는지와, 그것들의 내용이 어떻게 다뤄져야 하는지를 설명한다. 우리의 초기 삼각형 렌더링 어플리케이션에서, 우리는 벌카네엑 우리가 단일의 이미지를 color target으로 상요할 것이라고 말할 것이고, 우리가 그것이 drawing operation 직전에, solid color로 cleared되기를 원한다고 말할 것이다. 반면에 한 render pass는 오직 이미지들의 유형을 설명하지만, VkFramebuffer는 실제로 특정한 이미지를 이러한 slots에 바인드한다.

Step6 - Graphics pipeline
벌칸에서의 그래픽스 파이프라인은 VkPipeline object를 생성하여 설정된다. 그것은 그래픽카드의 설정가능한 상태를 설명하는데, viewport size와 depth buffer operation, VkShaderModule objects를 사용하여 programmable state 같은 상태들을 말한다. VkShaderModule objects는 shader byte code로부터 생성된다. 그 드라이버는 또한 어떤 렌더타겟들이 파이프라인에서 사용될지를 알 필요가 있다. 그래서 우리는 그 렌더패스를 참조하여 명시한다.

현존하는 API들과 비교해서 벌칸의 가장 구분되는 특징들 중 하나는 그래픽스 파이프라인의 거의 모든 환경설정이 미리 설정되어야 할 필요가 있다. 그것은 만약 너가 다른 쉐이더로 바꾸길 원하거나 약간 너의 vertex layout을 바꾸고 싶다면, 너는 전적으로 그래픽스 파이프라인을 재생성해야할 필요가 있다는 것을 의미한다.  그것은 너의 렌더링 연산을 위해 필요한 모드 ㄴ다른 조합을 위해 미리 많은 VkPipeline objects를 만들어야 한다는 것을 의미한다. viewport size와 clear color같은 오직 몇 가지 기본 설정만이 동적으로 바뀔 수 있다. 또한 모든 상태는 explicitly하게 설명되어질 필요가 있고, 예를들어, 어떠한 default color blend state가 없다.

좋은 소식은 너가 just in-time compilation에 대비하여 ahead-of-time compilation과 같은 것을 하기 떄문에, 드라이버에 대해 좀 더 최적화 기회가 있고, 런타임 성능은 좀 더 예측가능하다. 왜냐하면 다른 그래픽스 파이프라인으로 바꾼는 것 같은 많은 state changes가 매우 explicit하게 만들어지기 때문이다.

Step7 - Command pools and command buffers
이전에 언급되었듯이, drawing operations 같이 벌칸에서 우리가 실행하길 원하는 많은 연산들은 한 queue에 제출되어질 필요가 있다. 이러한 연산들은 그것들이 제출될 수 있기전에, VkCommandBuffer에 처음에 기록될 필요가 있다. 이러한 command buffers는 특정한 queue family와 연관된 VkCommonPool로 부터 할당된다. 간단한 삼각형을 그리기 위해, 우리는 다음의 연산들로 한 command buffer를 기록할 필요가 있다:

  • Begin render pass 
  • 그래픽스 파이프라인 바인드
  • 3개의 정점 그리기
  • End render pass
프레임버퍼에서의 이미지는  swap chain이 우리에게 어떤 특정한 이미지를 줄 지에 달려있기 때문에, 우리는 각 가능한 이미지에 대해 command buffer를 기록할 필요가 있고, draw time에 올바른 것을 선택할 필요가 있다. 대안은 매 프레임마다 그 command buffer를 다시 기록 하는 것인데, 효율적이지 않다.

Step8 - Main loop
drawing commands가 한 command buffer에 들어갔으니, 그 main loop는 꽤 간단하다. 우리는 처음에 swap chain으로부터 vkAcquireNextImageKHR로 한 이미지를 얻는다. 우리는 그러고나서 그 이미지에 대해 적절한 command buffer를 선택할 수 있고, vkQueueSubmit 으로 그것을 실행한다. 마지막으로, 우리는 스크린에 보이기위해 swap chain에 그 이미지를 vkQueuePresentKHR로 반환한다.

queues에 제출되는 연산들은 비동기적으로 실행된다. 그러므로, 우리는 정확한 실행순서를 보장하기 위해 세마포어 같은 동기화 오브젝트들을 사용해야만 한다.  draw command buffer의 실행은 image 획득이 마무리 되는 것을 기다리도록 설정되어야 한다. 만약 그렇지 않다면, 우리가 스크린에 보이기위해 여전히 읽혀지고 있는 이미지를 렌더링하기 시작하는 것이 발생할지도 모른다. vkQueuePresentKHR 호출은 차례대로 렌더링이 끝나기를 기다릴 필요가 있다. 왜냐하면 우리가 렌더링이 완료된 후에 signaled 되는 두 번째 세마포어를 사용할 것이기 때문이다.

Summary
- 요악해서, 첫 번째 삼각형을 그리기 위해서, 우리는 다음을 할 필요가 있다:
  • Create a VkInstance
  • 지원되는 그래픽 카드 선택 (VkPhysicalDevice)
  • drawing과 presentation을 위해 VkDevice와 VkQueue 생성
  • window, window surface, swap chain 생성
  • swap chain 이미지들을 VkImageView에 넣기
  • 렌더 타겟과 사용법을 명시하는 render pass 생성
  • 그래픽스 파이프라인 설정
  • 커맨드 버퍼를 할당하고, 모든 가능한 swap chain image에 대해 draw commands로 기록
  • 이미지를 획득하여 프레임들을 그리기, 그리고 올바른 draw command buffer를 제출하고, 그 이미지를 다시 swap chain에 반환하기.


Coding Conventions
- 한 오브젝트를 생성하거나 파괴하는 함수들은 드라이버 메모리에 대해 custom allocator를 사용하게 해주는 VkAllocationCallbacks 파라미터를 가질 것이고, 이것은 또한 이 튜토리얼에서 nullptr로 남아질 것이다.
- 거의 모든 함수들은 VK_SUCCESS or error code 둘 중 하나를 반환하는 VkResult를 반환한다. 명세 문서가 각 함수가 어떤 에러코드를 반환하고 그것들이 무엇을 의미하는지를 설명한다.

Validation layers
- 이전에 언급했듯이, 벌칸은 고성능과 낮은 드라이버 오버헤드를 위해 설계되었다. 그러므로, 그것은 기본적으로 매우 제한된 에러 체크와 디버깅 능력을 퐇마할 것이다. 그 드라이버는 만약 너가 잘못한다면 종종 에러코드를 반환하는 대신에 충돌이 날 것이다. 그것은 너의 그래픽카드에서는 작동하는 것처럼 보일 것이지만 다른 것에서 완전히 실패할 것이다.

벌칸은 너가 validation layers라고 알려진 한 기능을 통해서 광범위한 체크를 할 수 있게 해준다. Validation layers는 API와 그래픽스 드라이버 사이에 삽입될 수 있는 코드 조각들인데, 이것은 함수 파라미터에 대해 추가 체크를 하고, 메모리 관리 문제를 추적하는 것 같은 것을 하기 위해서이다. 좋은 것은, 너가 개발하는 동안 그것들을 활성화할 수 있고, 그러므로 너의 어플리케이션을 zero overhead를 위해 출시할 때 완전히 그것들을 비활성화할 수 있다. 누구든지 자신만의 validation layers를 쓸 수 있지만, LunarG가 만든 Vulkan SDK는 우리가 이 튜토리얼에서 사용할 validation layers의 표준 세트를 제공한다. 너는 또한 그 layers로 부터 debug messages를 받는 callback function을 등록할 필요가 있다.

벌칸은 모든 연산에 대해 매우 explicit하고, validation layers가 매우 광범위하기 때문에, 너의 스크린이 OpenGL과 Direct3D에 비해 왜 검정색이 되었는지를 알아내는 것이 훨씬 더 쉽다.