Files
video-v1/MediaCodec_ImageReader_Vulkan_Refactoring_Design.md

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

원인:

  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

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 동기화 작동 확인

  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. 레퍼런스


10. 다음 단계

  1. 설계서 작성 완료
  2. ⏭️ Phase 1 구현 시작
    • MediaCodecSurfaceManager VkFence 추가
    • ProcessAsyncOutputFrame GPU 동기화
    • 빌드 및 테스트
  3. ⏭️ Phase 2 검토 (Phase 1 검증 후)