# 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 단계를 조율합니다. ```cpp 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 m_playerOrder; // Registered player IDs std::set m_bufferedPlayers; // Players that completed INITIAL_BUFFERING }; ``` ### FrameProcessor 수정 INITIAL_BUFFERING 단계에서만 Round-Robin 적용: ```cpp bool FrameProcessor::ProcessFrame(VavCorePlayer* player, std::function 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 통합 플레이어 등록/해제: ```cpp 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*