Files
video-v1/vav2/docs/working/Round_Robin_Initial_Buffering_Design.md

9.1 KiB

Round-Robin Initial Buffering Design

목적 (Purpose)

초기 로딩의 race condition으로 인한 재생 프레임 떨림(jitter) 방지

4개의 VideoPlayerControl2가 동시에 재생 시작 시, NVDEC DPB 초기 버퍼링(0-15 프레임)에서 발생하는 큐 포화를 방지하고, 이후 재생 단계에서 안정적인 타이밍을 보장합니다.

핵심 아이디어 (Core Idea)

Phase별 전략

Phase 1: INITIAL_BUFFERING (frames 0-15) - Round-Robin 적용

  • 목적: NVDEC DPB 16프레임 순차 채우기
  • 방식: Player#0 → Player#1 → Player#2 → Player#3 순서로 1프레임씩 순차 제출
  • 이유:
    • NULL surface 제출이므로 화면에 표시 안 됨 → 타이밍 시차 무관
    • NVDEC 큐 포화 방지 → 부하 분산
    • Race condition 제거 → 안정적인 DPB 초기화

Phase 2: TRIPLE_FILLING (frames 16-18) - 독립 타이밍

  • 목적: Triple buffer 채우기 및 첫 화면 표시
  • 방식: 각 플레이어가 33.33ms 간격으로 독립 실행
  • 이유:
    • DPB 이미 채워짐 → 자연스러운 부하 분산
    • 화면 동기화 필수 → 정확한 타이밍 필요

Phase 3: NORMAL_PLAYBACK (frames 19+) - 독립 타이밍

  • 목적: 안정적인 30fps 재생
  • 방식: 각 플레이어가 33.33ms 간격으로 독립 실행

아키텍처 설계 (Architecture)

GlobalFrameScheduler (싱글톤)

전역 프레임 스케줄러로 모든 플레이어의 INITIAL_BUFFERING 단계를 조율합니다.

class GlobalFrameScheduler {
public:
    static GlobalFrameScheduler& GetInstance();

    // Player lifecycle management
    void RegisterPlayer(int playerId);
    void UnregisterPlayer(int playerId);

    // Round-robin coordination (INITIAL_BUFFERING only)
    void WaitForMyTurnInBuffering(int playerId);
    void SignalNextPlayer(int playerId);

    // Synchronization barrier for phase transition
    void WaitAllPlayersBuffered();
    void SignalPlayerBuffered(int playerId);

    // Reset state
    void ResetRoundRobin();

private:
    std::mutex m_mutex;
    std::condition_variable m_cv;
    int m_currentTurn = 0;  // Current player's turn
    std::vector<int> m_playerOrder;  // Registered player IDs
    std::set<int> m_bufferedPlayers;  // Players that completed INITIAL_BUFFERING
};

FrameProcessor 수정

INITIAL_BUFFERING 단계에서만 Round-Robin 적용:

bool FrameProcessor::ProcessFrame(VavCorePlayer* player, std::function<void(bool)> onComplete) {
    // Check if previous frame is still processing
    bool expected = false;
    if (!m_frameProcessing.compare_exchange_strong(expected, true)) {
        m_framesDropped++;
        return false;
    }

    auto decodeStart = std::chrono::high_resolution_clock::now();
    VavCoreVideoFrame vavFrame = {};
    VavCoreResult result;

    if (m_decoderType == VAVCORE_DECODER_DAV1D) {
        // DAV1D: CPU decoding (no round-robin needed)
        result = vavcore_decode_next_frame(player, &vavFrame);
        if (result == VAVCORE_SUCCESS) {
            vavFrame.surface_type = VAVCORE_SURFACE_CPU;
        }
    } else {
        // NVDEC/Hardware: Apply round-robin during INITIAL_BUFFERING
        if (m_framesDecoded < VAVCORE_NVDEC_INITIAL_BUFFERING) {
            // Wait for my turn (blocking)
            GlobalFrameScheduler::GetInstance().WaitForMyTurnInBuffering(m_playerInstanceId);

            // Decode to NULL surface (fill NVDEC DPB)
            result = vavcore_decode_to_surface(
                player,
                VAVCORE_SURFACE_D3D12_RESOURCE,
                nullptr,  // NULL surface during buffering
                &vavFrame
            );

            // Signal next player
            GlobalFrameScheduler::GetInstance().SignalNextPlayer(m_playerInstanceId);

            // Check if this was the last buffering frame
            if (m_framesDecoded == VAVCORE_NVDEC_INITIAL_BUFFERING - 1) {
                GlobalFrameScheduler::GetInstance().SignalPlayerBuffered(m_playerInstanceId);
                // Wait for all players to complete buffering
                GlobalFrameScheduler::GetInstance().WaitAllPlayersBuffered();
            }
        }
        // TRIPLE_FILLING and NORMAL_PLAYBACK: Independent timing (no round-robin)
        else if (m_framesDecoded < VAVCORE_NVDEC_INITIAL_BUFFERING + VAV2PLAYER_TRIPLE_BUFFER_SIZE) {
            // Triple buffer filling
            auto backend = m_renderer->GetRGBASurfaceBackend();
            ID3D12Resource* decodeTexture = backend->GetNextDecodeTexture();
            result = vavcore_decode_to_surface(player, VAVCORE_SURFACE_D3D12_RESOURCE, decodeTexture, &vavFrame);
            if (result == VAVCORE_SUCCESS) {
                backend->AdvanceDecodeOnly();
            }
        } else {
            // Normal playback
            auto backend = m_renderer->GetRGBASurfaceBackend();
            ID3D12Resource* decodeTexture = backend->GetNextDecodeTexture();
            result = vavcore_decode_to_surface(player, VAVCORE_SURFACE_D3D12_RESOURCE, decodeTexture, &vavFrame);
            if (result == VAVCORE_SUCCESS) {
                backend->AdvanceFrame();
            }
        }
    }

    // Handle result and continue with render pipeline...
}

PlaybackController 통합

플레이어 등록/해제:

PlaybackController::PlaybackController() {
    LoadDecoderSettings();

    // Register to GlobalFrameScheduler (if using NVDEC)
    if (m_frameProcessor && m_decoderType != VAVCORE_DECODER_DAV1D) {
        int playerId = m_frameProcessor->GetPlayerInstanceId();
        GlobalFrameScheduler::GetInstance().RegisterPlayer(playerId);
    }
}

PlaybackController::~PlaybackController() {
    Stop();
    Unload();

    // Unregister from GlobalFrameScheduler
    if (m_frameProcessor && m_decoderType != VAVCORE_DECODER_DAV1D) {
        int playerId = m_frameProcessor->GetPlayerInstanceId();
        GlobalFrameScheduler::GetInstance().UnregisterPlayer(playerId);
    }
}

동작 시나리오 (4 Players)

INITIAL_BUFFERING (frames 0-15): Round-Robin

Time 0ms:   Player#0 Frame 0 (NULL) → signal Player#1
Time 6ms:   Player#1 Frame 0 (NULL) → signal Player#2
Time 12ms:  Player#2 Frame 0 (NULL) → signal Player#3
Time 18ms:  Player#3 Frame 0 (NULL) → signal Player#0
Time 24ms:  Player#0 Frame 1 (NULL) → signal Player#1
...
Time 354ms: Player#2 Frame 15 (NULL) → signal Player#3
Time 360ms: Player#3 Frame 15 (NULL) → signal buffered
Time 366ms: All players buffered → WaitAllPlayersBuffered() released

→ 모든 플레이어의 NVDEC DPB 안정적으로 채워짐

Synchronization Barrier

Player#0: Frame 15 완료 → SignalPlayerBuffered(0) → WaitAllPlayersBuffered()
Player#1: Frame 15 완료 → SignalPlayerBuffered(1) → WaitAllPlayersBuffered()
Player#2: Frame 15 완료 → SignalPlayerBuffered(2) → WaitAllPlayersBuffered()
Player#3: Frame 15 완료 → SignalPlayerBuffered(3) → WaitAllPlayersBuffered()

→ 4개 모두 도착 → 동시 해제 → TRIPLE_FILLING 동시 시작

TRIPLE_FILLING (frames 16-18): Independent Timing

Time 0ms:   All players Frame 16 (동시 실행, DPB 이미 채워져 부하 분산)
Time 33ms:  All players Frame 17
Time 66ms:  All players Frame 18

NORMAL_PLAYBACK (frames 19+): Independent Timing

Time 99ms:  All players Frame 19
Time 132ms: All players Frame 20
...
→ 안정적인 30fps 재생

기대 효과 (Expected Benefits)

1. Race Condition 제거

  • NVDEC 큐 순차 제출로 초기 로딩 충돌 방지
  • 안정적인 DPB 초기화

2. 프레임 떨림(Jitter) 방지

  • INITIAL_BUFFERING 완료 후 동기화 배리어로 모든 플레이어 동시 시작
  • TRIPLE_FILLING 이후 정확한 33.33ms 타이밍 보장

3. 확장성

  • 플레이어 수 증가 시: INITIAL_BUFFERING만 비례 증가, 이후는 독립
  • 8개 플레이어: 초기 로딩 ~480ms, 이후 동일한 30fps 성능

4. NVDEC 부하 분산

  • INITIAL_BUFFERING: 순차 제출로 큐 포화 방지
  • TRIPLE_FILLING 이후: DPB 덕분에 자연스러운 부하 분산

구현 파일 (Implementation Files)

새로 생성할 파일

  • src/Playback/GlobalFrameScheduler.h - 전역 프레임 스케줄러 헤더
  • src/Playback/GlobalFrameScheduler.cpp - 전역 프레임 스케줄러 구현

수정할 파일

  • src/Playback/FrameProcessor.h - GetPlayerInstanceId() getter 추가
  • src/Playback/FrameProcessor.cpp - Round-robin 로직 통합
  • src/Playback/PlaybackController.cpp - 플레이어 등록/해제 추가
  • Vav2Player.vcxproj - 새 파일 프로젝트에 추가

제한사항 (Limitations)

DAV1D 디코더 제외

  • DAV1D는 CPU 디코더이므로 NVDEC 큐 문제 없음
  • Round-robin 적용하지 않고 독립 실행

플레이어 동적 추가/제거

  • 현재 설계는 재생 시작 전 모든 플레이어 등록 가정
  • 재생 중 플레이어 추가/제거 시 동기화 복잡도 증가 → 추후 확장

INITIAL_BUFFERING 시간 증가

  • 4 플레이어: 단일 대비 ~4배 시간 소요 (0.36초 → 1.44초)
  • 사용자 경험: "로딩 중" UI 필요

Created: 2025-10-11 Purpose: Prevent frame jitter from initial loading race condition