본문으로 건너뛰기
뒤로가기

[Vulkan] renderFinished 세마포어는 왜 프레임이 아니라 이미지에 묶어야 하는가

TL;DRrenderFinished 세마포어를 frame slot으로 인덱싱하면, present가 아직 사용 중인 세마포어를 다시 signal하려는 에러가 발생합니다. fence는 submit 완료만 보장하고 present 완료는 보장하지 않기 때문입니다. renderFinished는 스왑체인 이미지 인덱스로, imageAvailable은 frame slot으로 관리하면 해결됩니다.

Table of contents

Open Table of contents

들어가며

Emberlit 프로젝트에서 frames-in-flight를 스왑체인 이미지 수(3)에서 2로 줄였더니 Validation Layer 에러가 발생했습니다. 이 글에서는 에러의 원인을 추적하면서 fence와 semaphore가 각각 뭘 보장하는지, 그리고 세마포어를 어떤 기준으로 관리해야 하는지를 정리합니다.

이 글에서 다루는 내용:

사전 지식: Vulkan의 스왑체인, 커맨드 버퍼 제출 흐름에 대한 기본 이해를 전제로 합니다.


1. 문제 상황

frames-in-flight를 2로 줄였더니 다음 에러가 발생했습니다.

vkQueueSubmit(): pSubmits[0].pSignalSemaphores[0] is being signaled by VkQueue,
but it may still be in use by VkSwapchainKHR.

“세마포어가 아직 스왑체인에서 사용 중인데 다시 signal하려 한다.” 이 에러를 이해하려면 프레임 렌더링 흐름에서 fence와 semaphore가 각각 뭘 보장하는지 알아야 합니다.


2. 프레임 렌더링 흐름

한 프레임의 흐름은 다음과 같습니다.

1. vkWaitForFences(inFlight[frame])          — 이전 프레임의 GPU 작업 완료 대기
2. vkAcquireNextImageKHR(sem = imageAvailable[frame], &imageIndex) — 이미지 획득
3. vkQueueSubmit(wait = imageAvailable[frame],
                 signal = renderFinished[?],
                 fence = inFlight[frame])    — 렌더링 명령 제출
4. vkQueuePresentKHR(wait = renderFinished[?]) — 화면 표시

이 흐름에서 actor는 3개입니다.


3. Fence — CPU ↔ GPU submit 동기화

vkQueueSubmit에 연결됩니다. graphics queue가 해당 submit batch의 실행을 끝내면 signal됩니다. CPU는 vkWaitForFences로 기다릴 수 있습니다.

vkQueueSubmit(queue, 1, &submitInfo, m_inFlight[m_currentFrame]);
// 이 fence가 signal되는 시점 = 이 submit batch의 GPU 실행 완료

vkQueuePresentKHR(presentQueue, &presentInfo);
// 기본 경로에는 present 완료를 알려주는 fence가 없다

fence가 보장하는 것: “같은 frame slot의 이전 submit batch는 GPU에서 끝났다”

fence가 보장하지 않는 것: “그 submit 결과를 사용한 present까지 끝났다”

즉 fence는 submit의 lifetime을 추적하지, swapchain image의 present lifetime까지 추적하지 않습니다.


4. Semaphore — GPU ↔ GPU 동기화

바이너리 세마포어의 규칙:

4.1 imageAvailable — presentation engine → graphics queue

vkAcquireNextImageKHR(device, swapchain, UINT64_MAX,
    m_imageAvailable[currentFrame],
    VK_NULL_HANDLE,
    &m_imageIndex);

vkAcquireNextImageKHR는 성공 시 이미지 인덱스를 리턴합니다. 하지만 이것만으로는 해당 이미지가 사용 가능하다는 의미가 아닙니다. 실제로 프레젠트 엔진이 해당 이미지 사용을 끝내고 imageAvailable 세마포어 또는 acquire에 넘긴 fence를 signal해야 안전하게 렌더링할 수 있습니다.

vkQueueSubmit에서 이 세마포어를 wait한 후 렌더링을 시작합니다.

4.2 renderFinished — graphics queue → presentation engine

중요: present는 비동기입니다. vkQueuePresentKHR를 호출하면 즉시 리턴하고, 실제 세마포어 wait + 화면 표시는 나중에 프레젠트 엔진이 처리합니다. 그래서 present operation이 renderFinished의 semaphore payload를 언제 완전히 소비하고 더 이상 참조하지 않는지 CPU가 직접 알 수 없습니다.


5. 왜 에러가 나는가

renderFinished를 frame slot(frames-in-flight = 2)으로 인덱싱하면:

Frame 0 (frame=0): image 0 → renderFinished[0]으로 present
Frame 1 (frame=1): image 1 → renderFinished[1]으로 present
Frame 2 (frame=0): image 2 → renderFinished[0]을 다시 signal하려 함

Frame 2에서 fence wait은 Frame 0의 커맨드 버퍼 실행 완료를 보장합니다. 하지만 Frame 0의 present operation이 renderFinished[0]을 더 이상 참조하지 않는지는 보장하지 않습니다. present에는 기본 경로에서 fence가 없기 때문입니다.

Frame 2에서 acquire한 이미지는 image 2이지, image 0이 아닙니다. acquire가 성공해서 image 2를 반환했다는 사실만으로는 image 2가 이미 즉시 안전하게 사용 가능하다는 뜻도 아닙니다. 실제 보장은 acquire에 넘긴 semaphore 또는 fence가 signal된 시점에 생깁니다.

여기서 acquire completionvkAcquireNextImageKHR에 넘긴 semaphore 또는 fence가 signal된 시점을 뜻합니다.

acquire completion이 보장하는 것은:

하지만 여전히 보장하지 않는 것은:

좀 더 직관적으로 보면 이렇습니다.

핵심: 세마포어(renderFinished[0])와 이미지(image 0)의 쌍이 엇갈려서, acquire의 보장이 세마포어까지 커버하지 못합니다.


6. 해결: renderFinished를 이미지별로 관리

imageAvailable[frameIndex]     — frames-in-flight 개수 (2) — fence가 보호
renderFinished[imageIndex]     — swapchain image 개수 (3) — 같은 image의 acquire completion이 간접적으로 재사용 시점을 보장

이렇게 하면 세마포어와 이미지가 항상 같은 쌍으로 움직입니다.

Frame 0: image 0 → renderFinished[0]
Frame 1: image 1 → renderFinished[1]
Frame 2: image 2 → renderFinished[2]
Frame 3: image 0 다시 acquire, acquire semaphore/fence signal 이후 → renderFinished[0] 재사용

왜 안전한가: image 0을 다시 acquire했고, 그 acquire에 넘긴 semaphore 또는 fence가 signal되었다는 것은 image 0의 이전 present/use가 끝났다는 뜻입니다. renderFinished[0]은 항상 image 0의 present와만 짝을 이루므로, 이 시점에서 재사용해도 안전합니다.

imageAvailable은 왜 frame slot으로 괜찮은가

imageAvailable[frame]vkQueueSubmit의 wait에서 소비됩니다. submit에는 fence가 연결되어 있으므로, 다음 프레임에서 같은 frame slot의 fence wait이 끝나면 → submit도 끝남 → imageAvailable도 이미 소비됨 → 안전하게 재사용 가능합니다.


정리하며

핵심 요약:

동기화 객체방향인덱스 기준
FenceGPU submit → CPUframe slot
imageAvailablepresentation engine → graphics queueframe slot
renderFinishedgraphics queue → presentation engineswapchain image

참고 자료


이 게시물은 학습한 내용을 바탕으로 초안을 작성한 뒤, LLM의 도움을 받아 내용을 검수하고 다듬어 완성되었습니다.


공유하기:

이전 글
[Emberlit] CPU 레이트레이서 (1): 광선이 구를 만나는 순간
다음 글
[Emberlit] 가우시안 블러 이해하기: 수식에서 GPU Compute까지