9.1 KiB
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