본문 바로가기
Vulkan

5. Presentation And Synchronize ( Fence, Semaphore, Event )

by SimonLee 2024. 6. 27.

이번 챕터에서는 세마포어와 펜스를 살펴본다. 

세마포어와 펜스를 활용하는 화면출력 (Presentation) 부분을 먼저 확인합니다.

동기화를 적용할때, 더 큰 범위의 동기화를 사용하게 되면 최적화의 문제가 있다.

예를 들어 큐와 큐의 동기화 할수있는 부분을 펜스를 사용하게 되면, cpu time을 낭비할 수 있다.

그렇기에 적절한 동기화 방법을 선택해서 구현하자.

 

1. 표현 (Presentation)

화면 출력할때 사용하는 단계 입니다.

화면 출력은 3가지로 정의 된다.

1) 화면에 표시할 이미지 인덱스 얻어오기

2) 커맨드 버퍼 큐에 제출

3) 이미지 인덱스 참조하여 이미지 출력

 

 

다음 화면 해당하는 이미지 인덱스 가져오기

커맨드 버퍼가 색칠 작업에 사용할 프레젠테이션 이미지를 가져오고,

타깃 프레젠테이션 이미지가 변경이 됐음을 알려준다.

// Provided by VK_KHR_swapchain
VkResult vkAcquireNextImageKHR(
    VkDevice                                    device,
    VkSwapchainKHR                              swapchain,
    uint64_t                                    timeout,
    VkSemaphore                                 semaphore,
    VkFence                                     fence,
    uint32_t*                                   pImageIndex);

- device : 스와프 체인 개체와 연결된 논리장치

- swapchain : 스와프 체인 개체, 여기에서 이미지가 얻어진다.

- timeout : 타임 아웃 (nano초) 이 지정되면 가용한 이미지가 없을때 함수가 얼마나 오랫동안 기다릴 것인지 지정

  실패한 경우 정해진 시간 안에 소유권을 반환하는 것을 보장한다. (프레젠테이션이 반환 ? )

- semaphore : 프레젠테이션 엔진이 이미지에 대한 소유권을 반환하고 장치 내용을 변경할수 있을때 신호를 받게 된다.

- fence : 프레젠 테이션 엔진이 이미지의 대한 소유권을 반환하면 신호를 받게 된다.

- pImageIndex : 다음 프레젠테이션 가능한 이미지 인덱스.

 

 

이미지 화면에 출력하기

프페레젠테이션 엔진의 큐에 들어가게 되어 트레젠테이션 이미지가 디스플레이 출력으로 렌더링 된다.

// Provided by VK_KHR_swapchain
VkResult vkQueuePresentKHR(
    VkQueue                                     queue,
    const VkPresentInfoKHR*                     pPresentInfo);

- queue : 그래픽스 큐 기능과 프레젠테이션 기능을 갖은 큐, 이미지의 스와프 체인과 같은 장치에 포함 된다.

- pPresentInfo : VkPresentInfoKHR 포인터

 

 

이미지 출력 메타데이터

// Provided by VK_KHR_swapchain
typedef struct VkPresentInfoKHR {
    VkStructureType          sType;
    const void*              pNext;
    uint32_t                 waitSemaphoreCount;
    const VkSemaphore*       pWaitSemaphores;
    uint32_t                 swapchainCount;
    const VkSwapchainKHR*    pSwapchains;
    const uint32_t*          pImageIndices;
    VkResult*                pResults;
} VkPresentInfoKHR;

- sType : VK_STRUCTURE_TYPE_PRESENT_INFO_KHR

- pNext : 확장 포인터

- waitSemaphoreCount : 프레젠테이션 엔진이 이미지를 디스플레이 하기 전에 기다려야 하는 세마포어의 개수

- pWaitSemaphores : 현재 요청을 발행하기 전 기다려야하는 세마포어 지정.

- swapChainCount : 스왑체인 개수

- pSwapchains : 스왑체인 포인터

- pImageIndices : 표시 가능한 이미지 인덱스 배열

 

usage >>

커맨드 버퍼에 명령이 전부 수행이 되었으면

VkPresentInfoKHR presentInfo{
        .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
        .waitSemaphoreCount = 1,
        .pWaitSemaphores = &mSemaphore,
        .swapchainCount = 1,
        .pSwapchains = &mSwapchain,
        .pImageIndices = &swapchainImageIndex
};

VK_CHECK_ERROR(vkQueuePresentKHR(mGraphicQueue, &presentInfo));
VK_CHECK_ERROR(vkQueueWaitIdle(mGraphicQueue));

 

2.  동기화 기법

 

동기화는 비동기 시스템에 순서와 규칙을 저장하는 중요한 요소다.

리소스의 효율을 높여줄 뿐 아니라, CPU, GPU 유휴 시간을 줄여줌으로써 병렬 처리의 이점을 강화해준다.

 

벌칸은 병렬 처리를 위해 다음과 같은 네 가지 유형의 동기화 기법을 제공.

1) Fence : 호스트와 장치간 동기화를 담당한다.

2) 세마포어 : 큐 간 동기화

3) 이벤트 : 큐 제출 간의 동기화

4) 장벽(Barrier) : 커맨더 버퍼 내에서 명령 간 동기화

화면을 출력할때 중요한 동기화 포인트가 두 부분이 있다.

아래 내용 중 Wating 1, Wating 2로 쓰여진 부분이 그러하다.

 

 

1) 스왑체인 으로부터 화면에 표시할 이미지가 준비가 되면 이미지 인덱스를 얻어온다.  ( vkAcquireNextImageKHR )

< Wating 1 >

- 새롭게 렌더링할 이미지가 준비가 된 후, 커맨드 버퍼를 큐에 제출해야 한다.

- 렌더링 이미지가 준비될때까지 큐에 제출을 하지 말고 기다려야 한다.

 

2) 커맨드 버퍼를 그래픽스 큐에 제출. ( vkQueueSubmit )

< Wating 2 >

- 커맨드 버퍼의 커맨드가 전부 실행을 한 후 화면에 출력해야 한다.

- 그렇지 않으면, 일부 커맨드가 누락된 채로 화면에 출력이 되기 때문이다.

- 커맨드 버퍼 있는 내용이 전부 실행될때까지 기다려야 한다.

 

3) 이미지를 화면에 Present (출력) 한다. ( VkQueuePresentKHR )

- 프레젠테이션 엔진을 통해 이미지를 화면에 출력이 끝나게 되면

- 프레젠테이션 엔진의 대한 이미지 소유권을 해제하게 된다.

 

아래 예제에서는

Wating 1은 Fence를 사용하여 동기화를 하고,

Wating 2는 Semaphore를 사용하여 동기화를 해봅니다. 

 

2 - 1. Fence 란?

호스트(CPU) 와 장치(GPU) 간 동기화를 해주는 동기화 객체

GPU 작업의 완료를 HOST(CPU)에서 확인할 수 있게 하는 동기화 객체.

Fence는 두 가지 상태, Signal 상태, Unsignal 상태 중 하나를 가집니다.

 

Fence 용도

1) 제출한 커맨드 버퍼가 완료가 되었는지 확인 하는 경우,

2) 렌더링 작업이 완료되었는지 확인하고 그 결과를 복사할때 

3) SwapChain으로 부터 출력가능한 이미지를 얻어야 하는 경우.

4) 커맨드 큐에 제출된 작업이 완료 될때까지 호스트(cpu)가 대기하도록 지시. 이 방법으로 GPU는 커맨드 큐에 더 많이 작업이 쌓이는것을 방지.

 

펜스 생성 메타데이터

// Provided by VK_VERSION_1_0
typedef struct VkFenceCreateInfo {
    VkStructureType       sType;
    const void*           pNext;
    VkFenceCreateFlags    flags;
} VkFenceCreateInfo;

- sType : VK_STRUCTURE_TYPE_FENCE_CREATE_INFO

- pNext : 확장 기능 구조체 포인터

- flags : 초기상태, 기본적으로 Unsignal 상태

 

펜스 생성 플래그 비트

typedef enum VkFenceCreateFlagBits {
    VK_FENCE_CREATE_SIGNALED_BIT = 0x00000001,
    VK_FENCE_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkFenceCreateFlagBits;

 

Fence 생성

// Provided by VK_VERSION_1_0
VkResult vkCreateFence(
    VkDevice                                    device,
    const VkFenceCreateInfo*                    pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkFence*                                    pFence);

- device : 논리장치

- pCreateInfo : VkFenceCreateInfo 포인터

- vkFence : 생성된 vkFence 변수 포인터

 

Fence 객체가 signal 될때까지 기다리는 함수

// Provided by VK_VERSION_1_0
VkResult vkWaitForFences(
    VkDevice                                    device,
    uint32_t                                    fenceCount,
    const VkFence*                              pFences,
    VkBool32                                    waitAll,
    uint64_t                                    timeout);

- device : 논리장치

- fenceCount : 기다릴 vkFence 개수

- pFences : VkFence 배열의 포인터

- waitAll :

      VK_TRUE는 모든 VkFence가 Signal 될 때까지 기다림.

      VK_FALSE는 하나의 VkFence가 Signal 될 때까지 기다림.

- timeout : VkFence가 Signal 상태 될 때까지 대기하는 최대 시간.

 

Fence 상태 초기화

Fence는 Unsignal 상태를 가지고 생성된다.

VK_FENCE_CREATE_SIGNALED_BIT 사용하면 Signal 상태를 가지고 생성 된다.

// Provided by VK_VERSION_1_0
VkResult vkResetFences(
    VkDevice                                    device,
    uint32_t                                    fenceCount,
    const VkFence*                              pFences);

- VkFence : 논리 장치

- fenceCount : Unsignal 상태로 변경할 VkFence의 개수

- pFences : VkFence의 배열의 포인터

 

Fence 파괴

// Provided by VK_VERSION_1_0
void vkDestroyFence(
    VkDevice                                    device,
    VkFence                                     fence,
    const VkAllocationCallbacks*                pAllocator);

- device : 논리장치

- fence :  VkFence

 

Fence 상태 얻기

// Provided by VK_VERSION_1_0
VkResult vkGetFenceStatus(
    VkDevice                                    device,
    VkFence                                     fence);

- device : 논리장치

- fence : VkFence

VkResult 반환 값이 VK_SUCCESS 이면 Signal, VK_NOT_READY 이면 Unsignal.

 

 

Usage >

펜스 생성 코드

VkFenceCreateInfo fenceCreateInfo {
    .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
};
vkCreateFence(mDevice, &fenceCreateInfo, nullptr, &fence);

 

Waiting 1에 해당.프레젠테이션 엔진에서 이미지 인덱스를 얻어오게 되면,

fenceForAcquire가 시그널드 상태로 바뀌게 되어vkWaitForFences 에서 기다림을 멈추고 통과하게 된다.

그리고 펜스 객체를 초기화를 하며 언시그널드로 바꾸어,

다음 이미지 인덱스를 얻어올때 까지 기다리게 한다.

uint32_t swapchainImageIndex;
VK_CHECK_ERROR(vkAcquireNextImageKHR(mDevice,
                                     mSwapchain,
                                     UINT64_MAX,
                                     VK_NULL_HANDLE,
                                     fenceForAcquire,
                                     &swapchainImageIndex));
auto framebuffer = mFramebuffers[swapchainImageIndex];
vkWaitForFences(mDevice, 1, &fenceForAcquire, VK_TRUE, UINT64_MAX);
vkResetFences(mDevice, 1, &fenceForAcquire);
...
...
VkSubmitInfo submitInfo {
        .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
        .commandBufferCount = 1,
        .pCommandBuffers = &commandBuffer,
        .signalSemaphoreCount = 1,
        .pSignalSemaphores = &semaphore,
};
vkQueueSubmit(mGraphicQueue, 1, &submitInfo, fenceForSubmit);
VkPresentInfoKHR presentInfo{
        .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
        .waitSemaphoreCount = 1,
        .pWaitSemaphores = &semaphore,
        .swapchainCount = 1,
        .pSwapchains = &mSwapchain,
        .pImageIndices = &swapchainImageIndex
};
VK_CHECK_ERROR(vkQueuePresentKHR(mGraphicQueue, &presentInfo));

 

2 - 2. 세마포어 란?

세마포어는 큐 수준에서 동기화 할 수 있게 하며, 하나 이상의 큐들을 동기화 하는데 사용한다.

세마포어에는 신호 받음과 신호 없음 두 가지 상태가 있다.

신호 받음 세마포어는 큐 제출 명령 (VkQueueSubmit()) 에 지정된다.

세마포어는 장치에 의해 신호 없음 상태가 되지 않을 때까지 나머지 배치를 차단한다.

 

생성된 세마포어는 여러 큐에서 볼 수 있다.

두 개 이상의 큐 제출 명령이 동일한 세마포어를 대기 중이면 하나만 신호 상태를 수신하고, 다른 작업들은 계속 대기할 수 있다 (Atomic)

 

세마포어 생성 함수

// Provided by VK_VERSION_1_0
VkResult vkCreateSemaphore(
    VkDevice                                    device,
    const VkSemaphoreCreateInfo*                pCreateInfo,
    const VkAllocationCallbacks*                pAllocator,
    VkSemaphore*                                pSemaphore);

- device : 세마포어 개체를 생성하는데 사용할 논리 장치.

- pCreateInfo : VkSemaphoreCreateInfo 구조체 배열 포인터

- pAllocator : 호스트 메모리의 할당 제어.

- pSemaphore : 생성된 세마포어 개체의 핸들을 가리킨다.

 

세마포어 생성 메타데이터

// Provided by VK_VERSION_1_0
typedef struct VkSemaphoreCreateInfo {
    VkStructureType           sType;
    const void*               pNext;
    VkSemaphoreCreateFlags    flags;
} VkSemaphoreCreateInfo;

- sType : VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO

 

세마포어 삭제

// Provided by VK_VERSION_1_0
void vkDestroySemaphore(
    VkDevice                                    device,
    VkSemaphore                                 semaphore,
    const VkAllocationCallbacks*                pAllocator);

- device : 세마포어 개체를 생성하는데 사용할 논리 장치

- semaphore : 삭제할 세마포어 개체

- pAllocator : 호스트 메모리 할당 제어.

 

Usage >>

Wating 2에 해당한다.

VkPresentInfo 구조체 .pWaitSemaphores 변수에 세마포어를 넣어주게 되면, 해당 세마포어가 

시그널드 될때까지, 프레젠테이션 엔진에서 이미지를 출력하지 않는다.

시그널드 트리거링을 위해VkSubmitInfo 구조체 .pSignalSemaphores 변수에 세마포어를 넣어 준다.커맨드 버퍼 제출이 완료가 되면 , 해당 세마포어를 시그널드로 바꾸어 준다.

// 세마포어 생성 코드
VkSemaphoreCreateInfo semaphoreCreateInfo{
        .sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO,
};
vkCreateSemaphore(mDevice, &semaphoreCreateInfo, nullptr, &mSemaphore);


// 세마포어 사용 코드.
....
// 커맨드 버퍼 기록 종료
vkEndCommandBuffer(mCommandBuffer);
VkSubmitInfo submitInfo {
        .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
        .commandBufferCount = 1,
        .pCommandBuffers = &mCommandBuffer,
        .signalSemaphoreCount = 1,
        .pSignalSemaphores = &mSemaphore,
};

// Semaphores 시그널 동작, 큐에 제출 후 시그널 해제
vkQueueSubmit(mGraphicQueue, 1, &submitInfo, VK_NULL_HANDLE);
// vkQueueWaitIdle(mGraphicQueue); submitInfo Semaphore로 인하여 필요 없음.

// vkImage 화면에 출력
// 세마포어 신호 해제 기다림
VkPresentInfoKHR presentInfo{
        .sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
        .waitSemaphoreCount = 1,
        .pWaitSemaphores = &mSemaphore,
        .swapchainCount = 1,
        .pSwapchains = &mSwapchain,
        .pImageIndices = &swapchainImageIndex
};

VK_CHECK_ERROR(vkQueuePresentKHR(mGraphicQueue, &presentInfo));
VK_CHECK_ERROR(vkQueueWaitIdle(mGraphicQueue));

 

Reference.

https://docs.vulkan.org/guide/latest/synchronization.html

 

Synchronization :: Vulkan Documentation Project

The VK_KHR_synchronization2 extension overhauls the original core synchronization APIs to reduce complexity for application developers, as well as adding a few additional features not present in the original APIs. Read the VK_KHR_synchronization2 chapter f

docs.vulkan.org

 

'Vulkan' 카테고리의 다른 글

7. SPIR-V  (0) 2024.07.04
6. RenderPass  (0) 2024.07.01
4. Pipeline Barrier And Image Layout  (0) 2024.06.24
3. Command buffer  (0) 2024.06.21
2. Surface And SwapChain  (1) 2024.06.11