# MediaCodec + ImageReader + Vulkan 동기화 리팩토링 설계서 **작성일:** 2025-10-14 **목적:** 튜토리얼 패턴을 따라 데드락 및 GPU race condition 해결 **참고:** `Vulkan+Image+Tutorial.md` --- ## 1. 현재 구현의 문제점 ### 🔴 Critical Issues #### 1.1 Image.close() 타이밍 오류 (GPU 사용 중 메모리 반환) ```cpp // 현재: ProcessAsyncOutputFrame 시작 시 즉시 close() surface_manager->ReleaseImage(); // GPU가 아직 사용 중일 수 있음! // 문제: // - GPU가 VkImage 렌더링 중 // - Image.close() → AHardwareBuffer 반환 // - MediaCodec이 같은 버퍼에 새 프레임 쓰기 시작 // - 결과: 메모리 오염, 프레임 깨짐 ``` **심각도:** High **발현 확률:** Medium (GPU 속도 의존적) **영향:** 화면 깜박임, 프레임 오염, 간헐적 크래시 #### 1.2 VkFence 없음 (GPU 완료 보장 불가) ```cpp // 현재: vkQueueSubmit만 호출 vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE); // 펜스 없음 // 문제: GPU 작업 완료 시점을 알 수 없음 ``` **심각도:** High **발현 확률:** 100% **영향:** 동기화 보장 불가, 메모리 안전성 결여 #### 1.3 releaseOutputBuffer 블로킹 ```cpp // 현재: MediaCodec 콜백 내에서 동기적으로 AMediaCodec_releaseOutputBuffer(m_codec, output_index, true); // 블로킹! // 바로 다음 AcquireLatestImage(); // 프레임 준비 안 됨 → null ``` **원인:** 1. `releaseOutputBuffer(render=true)` → 비동기 렌더링 시작 2. 즉시 `AcquireLatestImage()` → 프레임 아직 준비 안 됨 3. ImageReader 버퍼 3개 모두 `Image.close()` 안 되어 사용 중 4. 다음 프레임: `releaseOutputBuffer()` → 버퍼 공간 없음 → **블로킹!** **심각도:** High **발현 확률:** High **영향:** 초당 1-2 프레임만 디코딩 가능, 재생 불가 --- ## 2. 목표 아키텍처 (튜토리얼 패턴) ### 2.1 핵심 원칙 1. **역할 분리:** - 디코더 스레드 = 생산자 (Image 획득 후 큐에 넣고 즉시 리턴) - 렌더링 스레드 = 소비자 (큐에서 꺼내 렌더링, GPU 완료 후 Image.close()) 2. **비동기 통신:** - 스레드 간 통신은 **오직** Thread-Safe Queue로만 - 콜백 함수 내에서 절대 wait/sleep/lock 금지 3. **버퍼 반납 시점:** - `Image.close()`는 **GPU 렌더링 완료 후에만** 호출 - `vkWaitForFences()`로 GPU 완료 확인 4. **동기화 객체:** - `VkFence`: CPU가 GPU 작업 완료를 기다림 - `SyncFence` (API 33+): 디코더 쓰기 완료 보장 ### 2.2 데이터 플로우 ``` ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ MediaCodec │────▶│ ImageReader │────▶│ Frame Queue │ │ (Decoder) │ │ (OnImageAvail│ │ (BlockingQ) │ └─────────────┘ └──────────────┘ └──────┬──────┘ │ ┌─────────────────────────────┘ ▼ ┌───────────────────────┐ │ Vulkan Render Loop │ ├───────────────────────┤ │ 1. frameQueue.take() │ │ 2. vkWaitForFences() │ ← 이전 프레임 GPU 완료 대기 │ 3. image.close() │ ← 안전하게 버퍼 반환 │ 4. VkImage 생성 │ │ 5. vkQueueSubmit() │ └───────────────────────┘ ``` --- ## 3. 단계별 구현 계획 ### Phase 1: 즉시 조치 (단기 - GPU 동기화) **목표:** VkFence 추가하여 GPU 완료 대기 후 Image.close() **예상 시간:** 2-3시간 **우선순위:** 🔴 Critical #### 3.1.1 MediaCodecSurfaceManager에 VkFence 추가 **파일:** `MediaCodecSurfaceManager.h` ```cpp class MediaCodecSurfaceManager { private: // 현재 프레임 추적 jobject m_current_image; AHardwareBuffer* m_current_ahardware_buffer; VkFence m_current_frame_fence; // ← 추가 public: // GPU 렌더링 완료 펜스 설정 (렌더러에서 호출) void SetCurrentFrameFence(VkFence fence); // 이전 프레임 GPU 완료 대기 후 Image 릴리즈 void ReleaseImageAfterGPU(VkDevice device); }; ``` **파일:** `MediaCodecSurfaceManager.cpp` ```cpp void MediaCodecSurfaceManager::SetCurrentFrameFence(VkFence fence) { m_current_frame_fence = fence; } void MediaCodecSurfaceManager::ReleaseImageAfterGPU(VkDevice device) { if (m_current_frame_fence != VK_NULL_HANDLE) { // GPU 완료 대기 (최대 1초) VkResult result = vkWaitForFences(device, 1, &m_current_frame_fence, VK_TRUE, 1000000000); if (result == VK_SUCCESS) { vkDestroyFence(device, m_current_frame_fence, nullptr); m_current_frame_fence = VK_NULL_HANDLE; // 이제 안전하게 Image 릴리즈 ReleaseImage(); // AHardwareBuffer도 릴리즈 if (m_current_ahardware_buffer) { AHardwareBuffer_release(m_current_ahardware_buffer); m_current_ahardware_buffer = nullptr; } } else { LogError("vkWaitForFences failed or timed out: " + std::to_string(result)); } } } ``` #### 3.1.2 VulkanRenderer에서 vkQueueSubmit 시 Fence 전달 **파일:** `vulkan_renderer.cpp` (또는 VulkanVideoView의 렌더링 로직) ```cpp // vkQueueSubmit 호출 전 VkFenceCreateInfo fenceInfo = {}; fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; VkFence renderCompleteFence; vkCreateFence(m_vkDevice, &fenceInfo, nullptr, &renderCompleteFence); // 커맨드 제출 VkSubmitInfo submitInfo = { ... }; vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, renderCompleteFence); // VavCore에 펜스 전달 (JNI 호출) vavcore_set_current_frame_fence(m_player, renderCompleteFence); ``` #### 3.1.3 ProcessAsyncOutputFrame 수정 **파일:** `MediaCodecAsyncHandler.cpp` ```cpp bool MediaCodecAsyncHandler::ProcessAsyncOutputFrame(...) { LogInfo("ProcessAsyncOutputFrame: ENTRY"); // GPU 완료 대기 후 이전 프레임 릴리즈 MediaCodecSurfaceManager* surface_manager = m_decoder->GetSurfaceManager(); if (surface_manager) { VkDevice device = static_cast(surface_manager->GetVulkanDevice()); surface_manager->ReleaseImageAfterGPU(device); // ← GPU 완료 대기 } // 이제 새 프레임 처리 AMediaCodec_releaseOutputBuffer(m_codec, output_index, true); // ... (나머지 로직) } ``` **변경 요약:** - ❌ 삭제: 즉시 `ReleaseImage()` 호출 - ✅ 추가: `ReleaseImageAfterGPU()` - GPU 완료 대기 후 릴리즈 --- ### Phase 2: 장기 조치 (완전한 패턴) **목표:** OnImageAvailableListener + 프레임 큐 + Push 모델 **예상 시간:** 1-2일 **우선순위:** 🟡 Medium (Phase 1 완료 후) #### 3.2.1 프레임 큐 추가 **파일:** `MediaCodecAV1Decoder.h` ```cpp struct DecodedFrameData { jobject java_image; // Java Image 객체 (Global Ref) AHardwareBuffer* hardware_buffer; int64_t presentation_time_us; VkFence gpu_complete_fence; }; class MediaCodecAV1Decoder { private: std::queue m_frame_queue; std::mutex m_queue_mutex; std::condition_variable m_queue_cv; const size_t MAX_QUEUE_SIZE = 3; // ImageReader 버퍼 수와 동일 }; ``` #### 3.2.2 OnImageAvailableListener 구현 **파일:** `MediaCodecSurfaceManager.cpp` ```cpp // JNI 콜백 등록 void MediaCodecSurfaceManager::SetupImageReader(uint32_t width, uint32_t height) { // ... 기존 ImageReader 생성 ... // OnImageAvailableListener 설정 jclass listenerClass = env->FindClass("android/media/ImageReader$OnImageAvailableListener"); // Java 리스너 구현 생성 (네이티브 콜백 호출) // ... jmethodID setListenerMethod = env->GetMethodID( imageReaderClass, "setOnImageAvailableListener", "(Landroid/media/ImageReader$OnImageAvailableListener;Landroid/os/Handler;)V" ); env->CallVoidMethod(m_image_reader, setListenerMethod, listener, handler); } // 네이티브 콜백 (JNI에서 호출됨) void MediaCodecSurfaceManager::OnImageAvailable(jobject image_reader) { JNIEnv* env = GetJNIEnv(); // acquireNextImage() 호출 jobject image = AcquireNextImage(image_reader); if (!image) return; // HardwareBuffer 추출 AHardwareBuffer* ahb = GetHardwareBufferFromImage(image); // DecodedFrameData 생성 DecodedFrameData frame_data; frame_data.java_image = env->NewGlobalRef(image); frame_data.hardware_buffer = ahb; frame_data.presentation_time_us = GetImageTimestamp(image); // 큐에 넣기 (블로킹하지 않음 - offer 사용) if (!m_decoder->EnqueueFrame(frame_data, 200)) { // 큐 가득참 - 프레임 드롭 LogWarning("Frame dropped - queue full"); env->DeleteGlobalRef(frame_data.java_image); AHardwareBuffer_release(ahb); } env->DeleteLocalRef(image); } ``` #### 3.2.3 렌더링 루프에서 프레임 소비 **파일:** `vulkan_renderer.cpp` ```cpp void VulkanVideoRenderer::RenderFrame() { // 1. 큐에서 새 프레임 가져오기 (논블로킹) DecodedFrameData* new_frame = nullptr; if (m_player->TryDequeueFrame(&new_frame, 0)) { // 타임아웃 0 = 즉시 리턴 // 2. 이전 프레임 GPU 완료 대기 및 릴리즈 if (m_current_frame) { vkWaitForFences(m_vkDevice, 1, &m_current_frame->gpu_complete_fence, VK_TRUE, UINT64_MAX); vkDestroyFence(m_vkDevice, m_current_frame->gpu_complete_fence, nullptr); // Image.close() 호출 (JNI) ReleaseJavaImage(m_current_frame->java_image); AHardwareBuffer_release(m_current_frame->hardware_buffer); delete m_current_frame; } // 3. 새 프레임을 현재 프레임으로 설정 m_current_frame = new_frame; // 4. VkImage 생성 VkImage vk_image = ImportHardwareBufferToVkImage(m_current_frame->hardware_buffer); // 5. VkFence 생성 VkFenceCreateInfo fenceInfo = { VK_STRUCTURE_TYPE_FENCE_CREATE_INFO }; vkCreateFence(m_vkDevice, &fenceInfo, nullptr, &m_current_frame->gpu_complete_fence); // 6. 렌더링 커맨드 제출 VkSubmitInfo submitInfo = { ... }; vkQueueSubmit(m_graphicsQueue, 1, &submitInfo, m_current_frame->gpu_complete_fence); } // 스왑체인 Present // ... } ``` --- ## 4. 수정할 파일 목록 ### Phase 1 (즉시 조치) | 파일 | 작업 | 우선순위 | |------|------|---------| | `MediaCodecSurfaceManager.h` | VkFence 멤버 추가, 메서드 선언 | 🔴 High | | `MediaCodecSurfaceManager.cpp` | `ReleaseImageAfterGPU()` 구현 | 🔴 High | | `MediaCodecAsyncHandler.cpp` | GPU 동기화 후 릴리즈로 변경 | 🔴 High | | `vulkan_renderer.cpp` | vkCreateFence, JNI 호출 추가 | 🔴 High | | `vavcore_jni.cpp` | `vavcore_set_current_frame_fence()` 추가 | 🔴 High | ### Phase 2 (장기) | 파일 | 작업 | 우선순위 | |------|------|---------| | `MediaCodecAV1Decoder.h` | 프레임 큐 추가 | 🟡 Medium | | `MediaCodecSurfaceManager.cpp` | OnImageAvailableListener 구현 | 🟡 Medium | | `vulkan_renderer.cpp` | 렌더링 루프 리팩토링 | 🟡 Medium | --- ## 5. API 변경사항 ### 5.1 새로운 Public API ```cpp // VavCore C API 추가 VAVCORE_API void vavcore_set_current_frame_fence( VavCorePlayer* player, uint64_t vk_fence // VkFence를 uint64_t로 전달 ); ``` ### 5.2 내부 API 변경 ```cpp // MediaCodecSurfaceManager + void SetCurrentFrameFence(VkFence fence); + void ReleaseImageAfterGPU(VkDevice device); // MediaCodecAV1Decoder (Phase 2) + bool EnqueueFrame(const DecodedFrameData& frame, int timeout_ms); + bool TryDequeueFrame(DecodedFrameData** out_frame, int timeout_ms); ``` --- ## 6. 테스트 계획 ### 6.1 Phase 1 검증 **목표:** VkFence 동기화 작동 확인 1. **로그 확인:** ``` ✅ 기대: ReleaseImageAfterGPU: Waiting for GPU fence... ✅ 기대: ReleaseImageAfterGPU: GPU complete, releasing Image ✅ 기대: ProcessAsyncOutputFrame: releaseOutputBuffer returned status=0 ❌ 없어야 함: WaitForAsyncFrame timed out ``` 2. **프레임레이트 측정:** - 목표: 30 FPS 이상 - 현재: 1-2 FPS 3. **메모리 오염 확인:** - 프레임 깜박임 없음 - 화면 찢어짐 없음 ### 6.2 Phase 2 검증 **목표:** Push 모델로 완전한 비동기 파이프라인 확인 1. **CPU 사용률:** 감소 (블로킹 제거) 2. **지연 시간:** 감소 (버퍼 순환 개선) 3. **안정성:** 장시간 재생 테스트 (1시간+) --- ## 7. 위험 요소 및 대응 ### 7.1 VkDevice 접근 문제 **위험:** `MediaCodecAsyncHandler`에서 VkDevice 접근 불가 **대응:** `MediaCodecSurfaceManager`에 VkDevice 저장됨 - 사용 가능 ### 7.2 JNI 스레드 안전성 **위험:** 여러 스레드에서 JNI 호출 시 크래시 **대응:** `GetJNIEnv()`가 자동으로 스레드 attach 처리 ### 7.3 VkFence 누수 **위험:** 펜스 생성 후 파괴 누락 시 메모리 누수 **대응:** RAII 패턴 또는 명시적 파괴 추가 --- ## 8. 성능 예상 ### 현재 (Phase 0) - FPS: 1-2 (블로킹으로 인한 극심한 저하) - 지연: 500ms+ (타임아웃 대기) - CPU: 높음 (스핀락 대기) ### Phase 1 완료 후 - FPS: 30-60 (정상) - 지연: 16-33ms (정상) - CPU: 중간 (동기 대기) ### Phase 2 완료 후 - FPS: 60+ (최적) - 지연: <16ms (최적) - CPU: 낮음 (완전 비동기) --- ## 9. 레퍼런스 - **튜토리얼:** `Vulkan+Image+Tutorial.md` - **Android 문서:** [MediaCodec + Surface](https://developer.android.com/reference/android/media/MediaCodec#using-an-output-surface) - **Vulkan 문서:** [VK_ANDROID_external_memory_android_hardware_buffer](https://registry.khronos.org/vulkan/specs/1.3-extensions/man/html/VK_ANDROID_external_memory_android_hardware_buffer.html) --- ## 10. 다음 단계 1. ✅ 설계서 작성 완료 2. ⏭️ Phase 1 구현 시작 - `MediaCodecSurfaceManager` VkFence 추가 - `ProcessAsyncOutputFrame` GPU 동기화 - 빌드 및 테스트 3. ⏭️ Phase 2 검토 (Phase 1 검증 후)