259 lines
9.1 KiB
Markdown
259 lines
9.1 KiB
Markdown
|
|
# 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<int> m_playerOrder; // Registered player IDs
|
||
|
|
std::set<int> m_bufferedPlayers; // Players that completed INITIAL_BUFFERING
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### FrameProcessor 수정
|
||
|
|
|
||
|
|
INITIAL_BUFFERING 단계에서만 Round-Robin 적용:
|
||
|
|
|
||
|
|
```cpp
|
||
|
|
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 통합
|
||
|
|
|
||
|
|
플레이어 등록/해제:
|
||
|
|
|
||
|
|
```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*
|