471 lines
14 KiB
Markdown
471 lines
14 KiB
Markdown
# 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 검증 후)
|