Files
video-v1/MediaCodec_ImageReader_Vulkan_Refactoring_Design.md

471 lines
14 KiB
Markdown
Raw Normal View History

# 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<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`
```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<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`
```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 검증 후)