[vk] Vulkan Tutorial: 삼각형을 그리기 위한 단계

2021. 8. 4. 14:06그래픽스/vk

Summary

실제 프로그램은 더 많은 단계를 수행해야함

- 정점 버퍼 할당 (allocating vertex buffers)

- 유니폼 버퍼 생성 (creating uniform buffers)

- 텍스처 이미지 업로드 (uploading texture images)

하지만 여기선 "삼각형"을 생성하기위해, 정점버퍼대신 정점 셰이더에 정점좌표를 포함시킴.

(정점 버퍼를 관리하기 위해선 먼저 명령버퍼부터 익숙해져야함)

 

아래는 vulkan에서 삼각형 만들기 과정.

  • Create a VkInstance
  • Select a supported graphics card (VkPhysicalDevice)
  • Create a VkDevice and VkQueue for drawing and presentation
  • Create a window, window surface and swap chain
  • Wrap the swap chain images into VkImageView
  • Create a render pass that specifies the render targets and usage
  • Create framebuffers for the render pass
  • Set up the graphics pipeline
  • Allocate and record a command buffer with the draw commands for every possible swap chain image
  • Draw frames by acquiring images, submitting the right draw command buffer and returning the images back to the swap chain

 

Step 1 - Instance and physical device selection

- vulkan app 은 VkInstance로부터 vulkan api를 세팅하며 시작함.

- An instance is created by describing your application and any API extensions you will be using.

- After creating the instance, can query for Vulkan supported hw & one or more VkphysicalDevices

   to use for operations

- query : properties (VRAM size, device capabilities) 

 

 

 

 

Step 2 - Logical device and queue families

- 물리적 hw device를 선택한 뒤 VkDevice (logical device) 를 생성해야함

- VkPhysicalDeviceFeatures : 구체적인 사항을 작성해야함(멀티 뷰포트렌더링이나 64bit floats 등등)

- queue families : 사용하고싶으면 작성해야함.

- 대부분 연산은 Vulkan (draw commands, memory operation)

   비동기적인 실행 : 명령들을 VkQueue 에 보냄

 

Queues

- queue families 로부터 할당받음

- 각각의 queue family 는 queues의 specific set의 연산을 서포트함.

   (separate queue families for graphics, compute and memory transfer operations)

- queue families 는 physical device selection에서 distinguishing factor 로 사용할 수 있음. 

- vulkan을 지원하는 장치가 이런 그래픽스 기능을 지원안할 수 있음,

- 하지만 모든 요즘 그래픽카드는 오늘날 대부분 queue 연산은 지원한다.

 

 

 

Step 3 - Window surface and swap chain

- offscreen rendering 뿐만이 아니라면 윈도우창을 만들고 이미지를 렌더해야함

- 기본 플랫폼 API 또는 GLFW, SDL 를 사용하여 띄울 수 있음

- 여기서는 GLFW를 사용하여 튜토리얼을 진행함.

 

window를 렌더링하기위해서 다음 두 요소가 필요

- window surface, swap chain : VkSurfaceKHR, VkSwapchainKHR 

   (khr 접미사 - vulkan extension의 일부분이라는의미)

- Vulkan API 는 그자체로 완전한 플랫폼 -> 표준인 WSI 를 사용해 window manager와 상호 작용

 

Surface

- 렌더링할 창에 대한 플랫폼 간 추상화, HWND windows 에서 기본창 핸들, 인스턴스화

-  GLFW라이브러리가 세부정보를 처리하는 기능이 내장

 

Swap chain

- render targets 의 집합

- 기본적인 목적 : 현재 렌더링하고자 하는것과 스크린에 띄워진거의 차이를 확인.

- 완전한 이미지만 보여주기위해서 중요함.

- 매번 프레임에 그릴때, swapchain에 렌더링할 이미지를 제공해야함.

- 프레임에 그리는것을 마치면, 이미지는 swapchain에 반환 (다시 그리기위해)

- render targets의 개수 와 스크린에 이미지를 표시하기위한 조건은  present mode 에 의존함.

- Common present modes : double buffering (vsync) and triple buffering.

- 일부 플랫폼에서는 VK_KHR_display및 VK_KHR_display_swapchain확장을 통해
- 창 관리자와 상호 작용하지 않고 디스플레이에 직접 렌더링할 수 있음. 
- 이를 통해 전체 화면을 나타내는 표면을 만들 수 있으며
- 예를 들어 고유한 창 관리자를 구현하는 데 사용

 

 

 

Step 4 - Image views and framebuffers

 

Swap chain에서 받은 이미지를 그리기위해 랩핑해야함.

- VkImageView and VkFramebuffer.

- image view : 사용할 이미지의 특정 부분을 참조, 

- framebuffer : color, depth, stencil targets같은 image views를 참조

- swap chain에는 다양한 이미지가 있을 수 있음

- 렌더 시간에 그릴 이미지를 찾기 위해 

- 각각에대해 이미지 뷰와 프레임 버퍼를 미리 생성해야함.

    (we'll preemptively create an image view and framebuffer)

 

 

 

 

Step 5 - Render passes

 

render passes : 렌더링 작업 중에 사용되는 이미지 유형, 이미지 사용방법, 콘테츠 처리 방법

(삼각형을 그릴때는 단일 이미지를 색상 대상으로 사용, draw직전에 단색으로 지운다고 vulkan에 알림)

- render type은 images type 만 기술하지만

- VkFramebuffer 는 실제로 특정 이미지를 이러한 slots에 bind

 

 

 

 

Step 6 - Graphics pipeline

 

Vulkan의 그래픽 파이프라인

- VkPipeline object 를 생성하여 설정

- 설정 가능한 state of the graphics card 를 작성해야함

  (viewport size, depth buffer operation, programmable state ->  VkShaderModule objects)

 

The VkShaderModule objects

- 쉐이더 바이트 코드에서 생성

- 드라이버는 또한 렌더 패스를 참조하여 지정하는 파이프라인에서 사용할 렌더 대상을 알아야함.

 

 

기존 API와 비교하여 가장 큰 특징

- 그래픽 파이프라인을 거의 모든 구성을 미리 설정해야함.

- 다른 셰이더로 전환하거나 정점 레이아웃을 약간 변경하려면

- 그래픽 파이프라인을 완전히 다시 만들어야함.

- 따라서 VkPipline object를 미리 많이 만들어야함.

- 뷰포트 크기, 및 선명한 색상같은 일부 기본 구성만 동적으로 변경할 수 있긴함

- 하지만 모든 상태도 명시적으로 작성해야함

   (no default color blend state)

 

 

 AOT(ahead-of-time) 컴파일, Just-in-time 컴파일에 해당하는 작업을 수행

=> 드라이버, 런타임 성능 최적화 기회가 더 많고

=> 더 예측 가능하다는 것

(because large state changes like switching to a different graphics pipeline are made very explicit.)

 

 

Step 7 - Command pools and command buffers

많은 operations (drawing 등) 은 queue에 submitt하는게 필요하다. 그 이전에

이러한 operations 은 첫번째로 VkCommandBuffer 에 record하는게 필요하다

-> 이러한 커맨드 버퍼들은 특정 queue family와 연관된 VkCommandPool 로부터 할당되어진다.

 

simple triangle  을 그리려면 다음과 같은 operation을 command buffer에 기록해야한다.

  • 렌더 패스 시작 (Begin the render pass)
  • 그래픽 파이프라인 바인딩 (Bind the graphics pipeline)
  • 3개의 꼭짓점 그리기 (Draw 3 vertices)
  • 랜더 패스 종료 (End the render pass)

 

the image in the framebuffer

- swap chain 이 제공하는 특정 이미지에 의존

- 가능한 각 이미지에 대한 command buffer를 기록하여

- 그릴 때 올바른것을 선택하게해야함.

-> 대안은 매 프레임 마다 명령 버퍼를 다시 기록하는것 (효율 하락)

 

Step 8 - Main loop

- step 7에서 drawing commands는 command buffer에 랩핑됨

- 메인 루프는 매우간단하게 작동한다.

 

1) swap chain에서 이미지를 받아오고 (vkAcquireNextImageKHR)

2) 해당 이미지에 적합한 명령 버퍼를 선택하고 실행한다. (vkQueueSubmit)

3) screen에 표시하기위해 이미지를 swap chain으로 반환 (vkQueuePresentKHR)

 

queues에 submit 된 operations은, 비동기적(asynchronously)으로 실행된다.

=> 그러므로 올바른 실행 순서를 보장하기위해 semaphores과 같은 object를 사용해야함.

     ( use synchronization objects )

=>  draw command buffer 는 이미지 획득이 완료될 때 까지 기다리도록 설정해야함.

=> 그렇지 않으면, 화면에 표시하기 위해 여전히 읽고 있는 이미지로 렌더링하게됨

=> vkQueuePresentKHR 호출은 렌더링이 완료될 때까지 기다려야함.

(The vkQueuePresentKHR call in turn needs to wait for rendering to be finished,

for which we'll use a second semaphore that is signaled after rendering completes.)

    

 

 

API concepts

coding conventions

함수에는 소문자 vk 접두사

열거형, 구조체 VK 접두사

열거형 변수에는 VK_ 접두사

 

API는 함수에 매개변수를 제공하기 위해 구조체를 많이 사용함.

아래는 객체 생성 패턴

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 << "failed to create object" << std::endl;
    return false;
}

1) sType멤버의 구조 유형을 명시적으로 지정해야한다.

2) pNext 멤버는 extension structure를 가리킬 수 있음 (튜토리얼에선 항상 nullptr)

3) 객체를 생성하거나 파괴하는 함수에는 VkAllocationCallbacks

    드라이버 메모리에 대한 사용자 지정할당자를 사용할 수 있는 매개변수가 있슴.(x튜토리얼에선 nullptr)

4) 거의 모든 함수는 VkResult 를 반환, VK_SUCCESS 이 아니면 에러 코드임.

 

 

Validation layers

- vk는 고성능과 낮은 드라이버 오버헤드를 위해 설계됨

- 기본적으로 매우 제한된 오류검사, 디버깅 기능이 포함됨

- 드라이버가 잘못되면 오류 코드를 반환하는 대신 충돌하는 경우가 많음

=>>> 다른 하드웨어일 경우 문제 가능성이 높다.

 

=> Vulkan을 사용하면, 유효성 검사 계층이라는 기능을 통해 광범위한 검사를 활성화 할 수 있음

=> 유효성 검사 계층은 API와 그래픽 드라이버 사이에 삽입하여 함수 매개 변수에 대한 추가 검사를 실행

=> 메모리 관리 문제를 추적하는 것과 같은 작업을 수행할 수 있는 코드

=> 개발중에 활성화한 다음 오버헤드 없이 앱을 릴리즈할때 비활성화 가능

=> (레이어에서 디버그 메시지를 수신하려면 콜백함수 등록해야함)

 

https://vulkan-tutorial.com/Overview