201 lines
12 KiB
Markdown
201 lines
12 KiB
Markdown
> Android MediaCodec AV1 (Async Decoding) + ImageReader + Vulkan 의 조합에서 Image 접근에서 발생하는 데드락을 해소하려면 API 를 어떻게 정교하게
|
|
호출해야 하는지 예제코드를 중심으로 설명해줘.
|
|
|
|
✦ 안녕하세요! Android에서 MediaCodec (비동기), ImageReader, Vulkan을 연동할 때 발생하는 데드락 문제는 매우 흔하면서도 까다로운 주제입니다.
|
|
질문하신 내용의 핵심은 각기 다른 스레드에서 동작하는 하드웨어 컴포넌트(디코더, GPU) 간의 버퍼 소유권과 상태를 어떻게 정확하게 동기화하는가에
|
|
있습니다.
|
|
|
|
데드락이 발생하는 주된 원인과 이를 해결하기 위한 정교한 API 호출 흐름을 예제 코드 중심으로 설명해 드리겠습니다.
|
|
|
|
데드락이 발생하는 시나리오 (잘못된 접근)
|
|
|
|
1. [MediaCodec 스레드] onOutputBufferAvailable 콜백이 호출됩니다.
|
|
2. [MediaCodec 스레드] 디코딩된 데이터를 ImageReader의 Surface로 렌더링합니다 (mediaCodec.releaseOutputBuffer(index, true)).
|
|
3. [ImageReader 스레드] onImageAvailable 콜백이 호출됩니다.
|
|
4. [ImageReader 스레드] imageReader.acquireNextImage()로 Image 객체를 획득합니다.
|
|
5. [ImageReader 스레드] 이 Image를 즉시 Vulkan 렌더링 스레드로 전달하고, 렌더링이 끝날 때까지 기다립니다 (Block).
|
|
6. [Vulkan 렌더링 스레드] Image를 사용하여 렌더링을 수행합니다.
|
|
7. [MediaCodec 스레드] 한편, MediaCodec은 다음 프레임을 계속 디코딩하여 ImageReader로 보내려고 합니다. 하지만 ImageReader의 모든 버퍼(Image)가
|
|
아직 close()되지 않았고, 5번 단계에서 스레드가 대기 중이므로 새 버퍼를 제공할 수 없습니다. MediaCodec은 ImageReader가 버퍼를 반납하기를
|
|
기다리며 멈춥니다 (Block).
|
|
8. 결과: 데드락!
|
|
* ImageReader 스레드는 렌더링이 끝나길 기다리고,
|
|
* MediaCodec은 ImageReader가 버퍼를 비워주길 기다리는 순환 대기(Circular Wait) 상태에 빠집니다.
|
|
|
|
---
|
|
|
|
해결 전략: 디커플링(Decoupling)과 명시적 동기화(Explicit Synchronization)
|
|
|
|
핵심은 디코더 스레드와 렌더링 스레드를 완전히 분리하고, 두 스레드 간의 통신은 스레드 안전 큐(Thread-Safe Queue)를 통해서만 이루어지게 하는
|
|
것입니다. 절대로 한쪽 스레드가 다른 쪽의 작업 완료를 직접 기다려서는 안 됩니다.
|
|
|
|
아키텍처:
|
|
|
|
1. MediaCodec 스레드: ImageReader로부터 Image를 받으면, 필요한 정보(AHardwareBuffer, SyncFence)를 추출하여 '작업 큐'에 넣고 즉시 리턴합니다.
|
|
절대 대기하지 않습니다.
|
|
2. 스레드 안전 큐 (Thread-Safe Queue): 디코더 스레드가 생산한 '디코딩된 프레임 정보'를 저장합니다. 렌더링 스레드는 이 큐에서 정보를
|
|
소비합니다.
|
|
3. Vulkan 렌더링 스레드: 메인 렌더링 루프에서 '작업 큐'를 확인합니다.
|
|
* 새 프레임이 있으면 큐에서 꺼냅니다.
|
|
* (가장 중요) 이전에 사용했던 프레임의 렌더링이 GPU에서 완료되었는지 Vulkan 펜스(VkFence)로 확인합니다.
|
|
* GPU 작업이 완료되었다면, 이전 `Image` 객체를 `close()`하여 `ImageReader`에 버퍼를 반납합니다.
|
|
* 이제 새로 받은 프레임 정보를 사용하여 Vulkan 렌더링 명령을 제출합니다.
|
|
|
|
---
|
|
|
|
예제 코드로 보는 정교한 API 호출법
|
|
|
|
아래는 Kotlin (Android App)과 C++ (Vulkan NDK)의 개념을 혼합한 의사 코드 스타일의 예제입니다. API 호출의 흐름에 집중해 주세요.
|
|
|
|
1. 공유 데이터 구조 및 큐 정의
|
|
|
|
디코더와 렌더러 간에 주고받을 데이터 구조와 이들을 담을 큐를 정의합니다.
|
|
|
|
1 // 디코딩된 프레임 정보를 담을 데이터 클래스
|
|
2 data class DecodedFrame(
|
|
3 val image: Image, // ImageReader로부터 받은 Image 객체
|
|
4 val hardwareBuffer: HardwareBuffer, // Vulkan에서 사용할 버퍼
|
|
5 val presentationTimeUs: Long, // 프레임 PTS
|
|
6 var gpuCompleteFence: Long = 0L // (C++ 네이티브 포인터) 이 프레임의 렌더링 완료를 알릴 VkFence
|
|
7 )
|
|
8
|
|
9 // 스레드 안전 큐 (생산자-소비자 패턴)
|
|
10 val frameQueue: BlockingQueue<DecodedFrame> = LinkedBlockingQueue(2) // 큐 크기는 ImageReader 버퍼 수와 비슷하게 설정
|
|
|
|
2. MediaCodec 및 ImageReader 설정 (디코더 스레드)
|
|
|
|
ImageReader.OnImageAvailableListener가 핵심입니다. 이 콜백은 절대 블로킹되면 안 됩니다.
|
|
|
|
1 // ImageReader 리스너 설정
|
|
2 val imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 3) // 버퍼 3개
|
|
3 imageReader.setOnImageAvailableListener({ reader ->
|
|
4 val image: Image? = try {
|
|
5 reader.acquireNextImage()
|
|
6 } catch (e: IllegalStateException) {
|
|
7 // 리더가 닫혔을 때 발생 가능
|
|
8 null
|
|
9 }
|
|
10
|
|
11 if (image != null) {
|
|
12 // 1. Image에서 HardwareBuffer 추출
|
|
13 val hardwareBuffer = image.hardwareBuffer
|
|
14 ?: run {
|
|
15 // HardwareBuffer를 지원하지 않는 경우, 여기서 처리
|
|
16 image.close()
|
|
17 return@setOnImageAvailableListener
|
|
18 }
|
|
19
|
|
20 // 2. DecodedFrame 객체 생성
|
|
21 val frame = DecodedFrame(
|
|
22 image = image,
|
|
23 hardwareBuffer = hardwareBuffer,
|
|
24 presentationTimeUs = image.timestamp
|
|
25 )
|
|
26
|
|
27 // 3. 큐에 프레임을 넣는다. 큐가 꽉 찼으면 잠시 기다리지만(offer의 타임아웃),
|
|
28 // 렌더러가 제 역할을 한다면 이 경우는 거의 발생하지 않아야 함.
|
|
29 // offer는 즉시 리턴하므로 콜백을 블록하지 않음.
|
|
30 if (!frameQueue.offer(frame, 200, TimeUnit.MILLISECONDS)) {
|
|
31 // 큐에 넣기 실패 (렌더러가 심하게 느림). 프레임을 드롭하고 버퍼를 즉시 반환.
|
|
32 Log.w("Decoder", "Dropping frame, queue is full.")
|
|
33 image.close()
|
|
34 hardwareBuffer.close()
|
|
35 }
|
|
36 }
|
|
37 }, mediaCodecCallbackHandler) // 별도 핸들러 스레드에서 실행
|
|
38
|
|
39 // MediaCodec 콜백 설정
|
|
40 val codecCallback = object : MediaCodec.Callback() {
|
|
41 override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
|
|
42 // 디코딩된 출력을 ImageReader의 Surface로 보낸다.
|
|
43 // 이 작업은 블로킹되지 않는다.
|
|
44 codec.releaseOutputBuffer(index, true)
|
|
45 }
|
|
46 // onInputBufferAvailable, onError, onOutputFormatChanged 등 구현...
|
|
47 }
|
|
48
|
|
49 mediaCodec.setCallback(codecCallback, mediaCodecCallbackHandler)
|
|
50 // ...
|
|
51 mediaCodec.configure(format, imageReader.surface, null, 0)
|
|
52 mediaCodec.start()
|
|
|
|
3. Vulkan 렌더링 루프 (렌더링 스레드)
|
|
|
|
렌더링 스레드는 큐에서 프레임을 가져와 처리하고, 다 쓴 이전 프레임의 버퍼를 반납하는 역할을 담당합니다.
|
|
|
|
1 // 네이티브 C++ Vulkan 렌더러 의사 코드
|
|
2
|
|
3 // 현재 화면에 그리고 있거나, 그리기가 끝난 프레임 정보
|
|
4 DecodedFrame* pCurrentFrame = nullptr;
|
|
5
|
|
6 void renderLoop() {
|
|
7 while (true) {
|
|
8 // 1. 큐에서 새로운 프레임을 가져온다 (블로킹 대기)
|
|
9 DecodedFrame* pNewFrame = frameQueue.take(); // Java의 큐에서 JNI를 통해 가져옴
|
|
10
|
|
11 // 2. (가장 중요) 이전에 렌더링했던 프레임이 있다면, GPU 작업이 끝났는지 확인하고 리소스를 해제한다.
|
|
12 if (pCurrentFrame != nullptr) {
|
|
13 // 2a. 이전 프레임 렌더링에 사용된 VkFence가 signaled 상태가 될 때까지 기다린다.
|
|
14 // 타임아웃을 길게 설정하여 GPU가 완료할 시간을 충분히 준다.
|
|
15 vkWaitForFences(vkDevice, 1, &pCurrentFrame->gpuCompleteFence, VK_TRUE, UINT64_MAX);
|
|
16
|
|
17 // 2b. GPU가 이전 프레임의 hardwareBuffer 사용을 마쳤으므로,
|
|
18 // VkFence와 관련 Vulkan 리소스를 파괴한다.
|
|
19 vkDestroyFence(vkDevice, pCurrentFrame->gpuCompleteFence, nullptr);
|
|
20 destroyVulkanImageAndMemoryForBuffer(pCurrentFrame->hardwareBuffer); // 이전에 생성한 VkImage/VkDeviceMemory 해제
|
|
21
|
|
22 // 2c. (데드락 해소의 핵심) Java의 Image 객체를 close()하여 ImageReader에 버퍼를 반납한다.
|
|
23 // JNI를 통해 Java의 image.close()와 hardwareBuffer.close() 호출
|
|
24 releaseJavaFrameObjects(pCurrentFrame);
|
|
25
|
|
26 delete pCurrentFrame;
|
|
27 }
|
|
28
|
|
29 // 3. 새로운 프레임을 현재 프레임으로 설정
|
|
30 pCurrentFrame = pNewFrame;
|
|
31
|
|
32 // 4. 새로운 프레임의 AHardwareBuffer를 Vulkan 리소스로 변환(Import)한다.
|
|
33 // - VK_ANDROID_external_memory_android_hardware_buffer 확장이 필요.
|
|
34 VkImage vkImage = importHardwareBufferToVkImage(pCurrentFrame->hardwareBuffer);
|
|
35 VkDeviceMemory vkMemory = importHardwareBufferToVkDeviceMemory(pCurrentFrame->hardwareBuffer);
|
|
36 vkBindImageMemory(vkDevice, vkImage, vkMemory, 0);
|
|
37 // ... VkImageView, VkSampler 생성 ...
|
|
38
|
|
39 // 5. 렌더링 완료를 추적할 새로운 VkFence를 생성한다.
|
|
40 VkFenceCreateInfo fenceInfo = { VK_STRUCTURE_TYPE_FENCE_CREATE_INFO };
|
|
41 vkCreateFence(vkDevice, &fenceInfo, nullptr, &pCurrentFrame->gpuCompleteFence);
|
|
42
|
|
43 // 6. Vulkan 커맨드 버퍼를 기록하고 제출(submit)한다.
|
|
44 // - YUV to RGB 변환 셰이더 등을 사용
|
|
45 VkCommandBuffer cmd = recordCommandBuffer(vkImage);
|
|
46
|
|
47 VkSubmitInfo submitInfo = { ... };
|
|
48 submitInfo.pCommandBuffers = &cmd;
|
|
49 // 디코더가 버퍼 쓰기를 완료했음을 GPU에 알리는 동기화는 VkExternalMemoryAcquireUnmodifiedEXT 등을 사용하거나,
|
|
50 // Android 13 (API 33) 이상에서는 MediaCodec/Image에서 받은 SyncFence를 Vulkan Semaphore로 변환하여
|
|
51 // pWaitSemaphores에 추가하는 것이 더 정교한 방법입니다.
|
|
52
|
|
53 // 커맨드 제출 시, 작업이 끝나면 pCurrentFrame->gpuCompleteFence를 signal하도록 설정
|
|
54 vkQueueSubmit(vkGraphicsQueue, 1, &submitInfo, pCurrentFrame->gpuCompleteFence);
|
|
55
|
|
56 // ... 스왑체인에 Present ...
|
|
57 }
|
|
58 }
|
|
|
|
요약 및 핵심 포인트
|
|
|
|
1. 역할 분리: 디코더 스레드는 생산자 역할만, 렌더링 스레드는 소비자 역할만 수행합니다.
|
|
2. 비동기 통신: 두 스레드 간의 통신은 오직 스레드 안전 큐를 통해서만 이루어집니다. 콜백 함수 안에서 절대 wait, sleep, lock 등 대기 작업을
|
|
수행하지 마세요.
|
|
3. 버퍼 반납 시점: Image.close()는 GPU가 해당 버퍼(Image)를 사용한 렌더링을 완전히 마친 후에 호출해야 합니다. 이 시점을 아는 유일한 방법은
|
|
vkQueueSubmit에 전달한 VkFence를 기다리는 것입니다.
|
|
4. 자원 관리: pCurrentFrame 변수처럼, 현재 렌더링 중인 프레임과 다음 루프에서 해제해야 할 프레임을 명확히 구분하여 관리해야 합니다.
|
|
5. 동기화 객체:
|
|
* `VkFence`: CPU(렌더링 스레드)가 GPU 작업 완료를 기다릴 때 사용합니다.
|
|
* `SyncFence` (API 33+): Image.getHardwareBuffer().getSyncFence()로 얻을 수 있으며, 디코더가 버퍼에 쓰기를 완료했음을 보장합니다. 이를
|
|
Vulkan VkSemaphore로 변환하여 vkQueueSubmit의 pWaitSemaphores에 전달하면, GPU가 디코더 작업을 기다리게 할 수 있어 가장 이상적인
|
|
동기화가 가능합니다.
|
|
|
|
이 아키텍처를 따르면 MediaCodec, ImageReader, Vulkan이 각자의 파이프라인을 막힘없이 효율적으로 실행할 수 있어 데드락을 원천적으로 방지하고
|
|
최적의 성능을 얻을 수 있습니다.
|