14 KiB
MediaCodec + ImageReader + Vulkan 동기화 리팩토링 설계서
작성일: 2025-10-14
목적: 튜토리얼 패턴을 따라 데드락 및 GPU race condition 해결
참고: Vulkan+Image+Tutorial.md
1. 현재 구현의 문제점
🔴 Critical Issues
1.1 Image.close() 타이밍 오류 (GPU 사용 중 메모리 반환)
// 현재: ProcessAsyncOutputFrame 시작 시 즉시 close()
surface_manager->ReleaseImage(); // GPU가 아직 사용 중일 수 있음!
// 문제:
// - GPU가 VkImage 렌더링 중
// - Image.close() → AHardwareBuffer 반환
// - MediaCodec이 같은 버퍼에 새 프레임 쓰기 시작
// - 결과: 메모리 오염, 프레임 깨짐
심각도: High 발현 확률: Medium (GPU 속도 의존적) 영향: 화면 깜박임, 프레임 오염, 간헐적 크래시
1.2 VkFence 없음 (GPU 완료 보장 불가)
// 현재: vkQueueSubmit만 호출
vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE); // 펜스 없음
// 문제: GPU 작업 완료 시점을 알 수 없음
심각도: High 발현 확률: 100% 영향: 동기화 보장 불가, 메모리 안전성 결여
1.3 releaseOutputBuffer 블로킹
// 현재: MediaCodec 콜백 내에서 동기적으로
AMediaCodec_releaseOutputBuffer(m_codec, output_index, true); // 블로킹!
// 바로 다음
AcquireLatestImage(); // 프레임 준비 안 됨 → null
원인:
releaseOutputBuffer(render=true)→ 비동기 렌더링 시작- 즉시
AcquireLatestImage()→ 프레임 아직 준비 안 됨 - ImageReader 버퍼 3개 모두
Image.close()안 되어 사용 중 - 다음 프레임:
releaseOutputBuffer()→ 버퍼 공간 없음 → 블로킹!
심각도: High 발현 확률: High 영향: 초당 1-2 프레임만 디코딩 가능, 재생 불가
2. 목표 아키텍처 (튜토리얼 패턴)
2.1 핵심 원칙
-
역할 분리:
- 디코더 스레드 = 생산자 (Image 획득 후 큐에 넣고 즉시 리턴)
- 렌더링 스레드 = 소비자 (큐에서 꺼내 렌더링, GPU 완료 후 Image.close())
-
비동기 통신:
- 스레드 간 통신은 오직 Thread-Safe Queue로만
- 콜백 함수 내에서 절대 wait/sleep/lock 금지
-
버퍼 반납 시점:
Image.close()는 GPU 렌더링 완료 후에만 호출vkWaitForFences()로 GPU 완료 확인
-
동기화 객체:
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
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
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의 렌더링 로직)
// 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
bool MediaCodecAsyncHandler::ProcessAsyncOutputFrame(...) {
LogInfo("ProcessAsyncOutputFrame: ENTRY");
// GPU 완료 대기 후 이전 프레임 릴리즈
MediaCodecSurfaceManager* surface_manager = m_decoder->GetSurfaceManager();
if (surface_manager) {
VkDevice device = static_cast<VkDevice>(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
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<DecodedFrameData> 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
// 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
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
// VavCore C API 추가
VAVCORE_API void vavcore_set_current_frame_fence(
VavCorePlayer* player,
uint64_t vk_fence // VkFence를 uint64_t로 전달
);
5.2 내부 API 변경
// 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 동기화 작동 확인
-
로그 확인:
✅ 기대: ReleaseImageAfterGPU: Waiting for GPU fence... ✅ 기대: ReleaseImageAfterGPU: GPU complete, releasing Image ✅ 기대: ProcessAsyncOutputFrame: releaseOutputBuffer returned status=0 ❌ 없어야 함: WaitForAsyncFrame timed out -
프레임레이트 측정:
- 목표: 30 FPS 이상
- 현재: 1-2 FPS
-
메모리 오염 확인:
- 프레임 깜박임 없음
- 화면 찢어짐 없음
6.2 Phase 2 검증
목표: Push 모델로 완전한 비동기 파이프라인 확인
- CPU 사용률: 감소 (블로킹 제거)
- 지연 시간: 감소 (버퍼 순환 개선)
- 안정성: 장시간 재생 테스트 (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
- Vulkan 문서: VK_ANDROID_external_memory_android_hardware_buffer
10. 다음 단계
- ✅ 설계서 작성 완료
- ⏭️ Phase 1 구현 시작
MediaCodecSurfaceManagerVkFence 추가ProcessAsyncOutputFrameGPU 동기화- 빌드 및 테스트
- ⏭️ Phase 2 검토 (Phase 1 검증 후)