# Present Sequential Execution Design **Date**: 2025-10-11 **Author**: Claude Code **Status**: Implementation in Progress ## Problem Statement ### Current Issue: UI Thread Overload 4개 플레이어가 동시에 UI 스레드의 `DispatcherQueue`에 `Present()` 요청을 보내면서 발생하는 문제: ``` Frame 169 타임라인 (4-player simultaneous): 11:00:59.325 - Player#2 TryEnqueue() → DispatcherQueue 11:00:59.325 - Player#3 TryEnqueue() → DispatcherQueue (대기 시작) 11:00:59.326 - Player#5 TryEnqueue() → DispatcherQueue (대기 시작) 11:00:59.326 - Player#4 TryEnqueue() → DispatcherQueue (대기 시작) Result: - Player#2: QUEUE_DELAY: 12.0 ms ✅ - Player#3: QUEUE_DELAY: 31.4 ms ❌ - Player#5: QUEUE_DELAY: 43.5 ms ❌ ``` ### Root Cause Analysis **WinUI3 SwapChainPanel 제약:** - `SwapChainPanel`은 XAML UI 요소 (COM STA 모델) - `Present()`는 **반드시 UI 스레드**에서만 호출 가능 - 모든 플레이어가 **동일한 DispatcherQueue** 사용 - UI 스레드는 **순차 처리**만 가능 (병렬 불가) **문제 시나리오:** ``` UI Thread (Single-threaded): [Player#2 Present 0.5ms] → [Player#3 Present 0.5ms] → [Player#5 Present 0.5ms] → [Player#4 Present 0.5ms] ↑ 대기 시작 ↑ 대기 누적 1ms ↑ 대기 누적 1.5ms ``` **QUEUE_DELAY 증가 메커니즘:** 1. 모든 플레이어가 거의 동시에 `TryEnqueue()` 호출 2. UI 스레드는 하나씩만 처리 가능 3. 뒤쪽 플레이어는 앞쪽 플레이어의 Present 완료까지 대기 4. QUEUE_DELAY = (앞선 플레이어 수) × (Present 시간) + (UI 스레드 스케줄링 지연) ## Design Solution: Present Sequential Execution ### Core Concept **GlobalFrameScheduler에 Present 순서 제어 추가:** - NVDEC 디코딩: 병렬 처리 (Hybrid Round-Robin 이미 구현됨) - **Present 호출: 순차 처리** (새로 추가) ### Architecture Design ``` Timing Thread (30fps tick) ↓ [Player#2] → Decode (parallel) → Render (parallel) → TryEnqueue() → [UI Thread] [Player#3] → Decode (parallel) → Render (parallel) → TryEnqueue() → [UI Thread] [Player#5] → Decode (parallel) → Render (parallel) → TryEnqueue() → [UI Thread] [Player#4] → Decode (parallel) → Render (parallel) → TryEnqueue() → [UI Thread] ↓ [Present Sequential Execution on UI Thread] WaitForPresentTurn(playerId) → Present() → SignalNextPresent(playerId) ↓ ↓ ↓ Player#2 (0.5ms) Player#3 (0.5ms) Player#5 (0.5ms) ... ``` ### Implementation Plan #### 1. GlobalFrameScheduler Extension **New Members:** ```cpp class GlobalFrameScheduler { private: // Existing NVDEC buffering control std::mutex m_mutex; std::condition_variable m_cv; int m_currentPlayer; // ... // NEW: Present sequential control std::mutex m_presentMutex; std::condition_variable m_presentCV; int m_currentPresentPlayer; // Start from 2 (first player ID) public: // NEW: Present sequential methods void WaitForPresentTurn(int playerId); void SignalNextPresent(int playerId); void ResetPresentScheduler(); // Called when players change }; ``` **WaitForPresentTurn() Logic:** ```cpp void GlobalFrameScheduler::WaitForPresentTurn(int playerId) { std::unique_lock lock(m_presentMutex); // Wait until it's my turn m_presentCV.wait(lock, [this, playerId]() { return m_currentPresentPlayer == playerId; }); LOGF_DEBUG("[GlobalFrameScheduler] Player#%d acquired PRESENT turn", playerId); } ``` **SignalNextPresent() Logic:** ```cpp void GlobalFrameScheduler::SignalNextPresent(int playerId) { std::unique_lock lock(m_presentMutex); // Advance to next player (circular) std::lock_guard playerLock(m_mutex); auto it = std::find(m_registeredPlayers.begin(), m_registeredPlayers.end(), playerId); if (it != m_registeredPlayers.end()) { ++it; if (it == m_registeredPlayers.end()) { it = m_registeredPlayers.begin(); // Wrap around } m_currentPresentPlayer = *it; } m_presentCV.notify_all(); LOGF_DEBUG("[GlobalFrameScheduler] Player#%d signaled next PRESENT player: #%d", playerId, m_currentPresentPlayer); } ``` #### 2. FrameProcessor Integration **Modify Present Callback:** ```cpp // FrameProcessor.cpp line 277-304 bool enqueued = m_dispatcherQueue.TryEnqueue( winrt::Microsoft::UI::Dispatching::DispatcherQueuePriority::High, [this, onComplete, processStart]() { auto uiCallbackStart = std::chrono::high_resolution_clock::now(); double queueDelay = std::chrono::duration(uiCallbackStart - processStart).count(); // NEW: Wait for Present turn (sequential execution) auto presentWaitStart = std::chrono::high_resolution_clock::now(); GlobalFrameScheduler::GetInstance().WaitForPresentTurn(m_playerInstanceId); auto presentWaitEnd = std::chrono::high_resolution_clock::now(); double presentWaitTime = std::chrono::duration(presentWaitEnd - presentWaitStart).count(); // Execute Present auto presentStart = std::chrono::high_resolution_clock::now(); HRESULT hr = m_renderer->Present(); auto presentEnd = std::chrono::high_resolution_clock::now(); double presentTime = std::chrono::duration(presentEnd - presentStart).count(); // NEW: Signal next player GlobalFrameScheduler::GetInstance().SignalNextPresent(m_playerInstanceId); bool presentSuccess = SUCCEEDED(hr); if (!presentSuccess) { LOGF_ERROR("[Player#%d] [FrameProcessor] Present error: HRESULT = 0x%08X", m_playerInstanceId, hr); } else { auto totalEnd = std::chrono::high_resolution_clock::now(); double totalTime = std::chrono::duration(totalEnd - processStart).count(); LOGF_INFO("[Player#%d] [FrameProcessor] PRESENT: %.1f ms | WAIT: %.1f ms | QUEUE_DELAY: %.1f ms | TOTAL: %.1f ms", m_playerInstanceId, presentTime, presentWaitTime, queueDelay, totalTime); } m_frameProcessing.store(false); if (onComplete) { onComplete(presentSuccess); } }); ``` #### 3. PlaybackController Integration **Initialize Present Scheduler:** ```cpp // PlaybackController.cpp - OnPlayClicked() void PlaybackController::OnPlayClicked() { // ... existing code ... // NEW: Reset Present scheduler when starting playback GlobalFrameScheduler::GetInstance().ResetPresentScheduler(); // ... rest of playback initialization ... } ``` ## Expected Performance Improvement ### Before (Current State) **Frame 169 QUEUE_DELAY:** ``` Player#2: QUEUE_DELAY: 12.0 ms (First in queue) Player#3: QUEUE_DELAY: 31.4 ms (Second, waits ~19ms) Player#5: QUEUE_DELAY: 43.5 ms (Third, waits ~31ms) Player#4: QUEUE_DELAY: 38.0 ms (Fourth, waits ~26ms) ``` **Problem:** Uncontrolled DispatcherQueue causes unpredictable delays ### After (Expected State) **Frame 169 QUEUE_DELAY (with Sequential Present):** ``` Player#2: QUEUE_DELAY: 2.0 ms (Immediate execution + 0.5ms Present) Player#3: QUEUE_DELAY: 2.5 ms (Wait 0.5ms for #2 + 0.5ms Present) Player#5: QUEUE_DELAY: 3.0 ms (Wait 1.0ms for #2,#3 + 0.5ms Present) Player#4: QUEUE_DELAY: 3.5 ms (Wait 1.5ms for #2,#3,#5 + 0.5ms Present) ``` **Improvement:** - Player#2: 12.0ms → 2.0ms (83% reduction) - Player#3: 31.4ms → 2.5ms (92% reduction) - Player#5: 43.5ms → 3.0ms (93% reduction) - Player#4: 38.0ms → 3.5ms (91% reduction) ### Total Frame Time Impact **Before:** ``` PlaybackController Frame 169: - callback=19.76ms (includes 43.5ms worst QUEUE_DELAY) - sleep=12.46ms - total=32.42ms ❌ Exceeds 33.33ms target (frame skip!) ``` **After:** ``` PlaybackController Frame 169: - callback=3.5ms (max QUEUE_DELAY reduced to 3.5ms) - sleep=28.0ms - total=31.5ms ✅ Under 33.33ms target (smooth playback) ``` ## Implementation Steps ### Phase 1: GlobalFrameScheduler Extension 1. Add Present sequential control members 2. Implement `WaitForPresentTurn()` 3. Implement `SignalNextPresent()` 4. Implement `ResetPresentScheduler()` ### Phase 2: FrameProcessor Integration 1. Modify Present callback with Wait/Signal 2. Add Present wait time logging 3. Update LOGF_INFO format ### Phase 3: PlaybackController Integration 1. Add `ResetPresentScheduler()` call on playback start 2. Ensure proper cleanup on stop ### Phase 4: Testing 1. Build and run 4-player simultaneous playback 2. Verify QUEUE_DELAY reduction in time.log 3. Check for smooth playback (no stuttering) 4. Monitor total frame time (should be <33.33ms) ## Alternatives Considered ### ❌ Option 2: SwapChain별 Present 스레드 분리 **Rejected Reason:** WinUI3 COM STA 제약으로 인해 기술적으로 불가능 - `SwapChainPanel`은 UI 요소 - `Present()`는 반드시 UI 스레드에서만 호출 가능 - 별도 스레드에서 호출 시 `RPC_E_WRONG_THREAD` 에러 발생 ### ⚠️ Option 3: VSync 제거 + 비동기 Present **Rejected Reason:** 화면 찢어짐 위험, 프레임 스킵 발생 가능 - `Present(0, DXGI_PRESENT_DO_NOT_WAIT)` 사용 - GPU 부하 시 `DXGI_ERROR_WAS_STILL_DRAWING` 에러 → 프레임 스킵 - VSync 없이는 30fps 페이싱 제어 어려움 ## Risk Assessment ### Low Risk - **구현 복잡도**: 낮음 (GlobalFrameScheduler 기존 패턴 재사용) - **버그 가능성**: 낮음 (단순한 순차 제어 로직) - **성능 영향**: 긍정적 (QUEUE_DELAY 대폭 감소) ### Testing Requirements - 4-player simultaneous playback - 다양한 비디오 파일 (720p, 1080p, 4K) - 장시간 재생 안정성 테스트 ## Success Criteria 1. **QUEUE_DELAY**: 모든 플레이어 <5ms 2. **Total Frame Time**: 모든 플레이어 <33.33ms 3. **Visual Smoothness**: 화면 떨림 없이 부드러운 재생 4. **No Frame Drops**: 프레임 드롭 0개 ## Future Enhancements ### Option: Adaptive Present Scheduling 프레임 시간에 여유가 있을 때 일부 플레이어 병렬 Present 허용 - Total frame time < 25ms: 2개 플레이어 동시 Present 허용 - Total frame time > 30ms: 완전 순차 처리 ### Option: Dynamic Player Priority QUEUE_DELAY가 높은 플레이어에게 더 높은 Present 우선순위 부여 --- ## ❌ **Implementation Failed - UI Thread Deadlock** **날짜**: 2025-10-11 **상태**: 구현 실패, 롤백 완료 ### 실패 원인 Present Sequential Execution을 구현했지만 **UI 스레드 데드락**으로 인해 실패했습니다. ### 데드락 시나리오 ``` UI Thread (Single-threaded DispatcherQueue): ┌─────────────────────────────────────────────────┐ │ Player#2 TryEnqueue() → WaitForPresentTurn() │ ← UI 스레드에서 대기 │ Player#3 TryEnqueue() → WaitForPresentTurn() │ ← UI 스레드에서 대기 │ Player#4 TryEnqueue() → WaitForPresentTurn() │ ← UI 스레드에서 대기 │ Player#5 TryEnqueue() → WaitForPresentTurn() │ ← UI 스레드에서 대기 └─────────────────────────────────────────────────┘ ↓ 모든 콜백이 UI 스레드 내에서 동시에 대기 ↓ UI 스레드가 블록되어 아무도 진행 못함 ↓ **DEADLOCK** ``` ### 근본 문제 **WinUI3 DispatcherQueue의 한계:** 1. **단일 UI 스레드**: DispatcherQueue는 단일 스레드에서 순차 실행 2. **콜백 내 대기 불가**: UI 스레드 콜백 내에서 다른 콜백을 기다리면 데드락 3. **Present()는 UI 스레드 전용**: SwapChainPanel.Present()는 UI 스레드에서만 호출 가능 ### 실패한 구현 ```cpp // ❌ DEADLOCK: UI 스레드 콜백 내에서 순차 대기 m_dispatcherQueue.TryEnqueue([this]() { // UI 스레드에서 실행됨 GlobalFrameScheduler::GetInstance().WaitForPresentTurn(playerId); // 대기 시작 m_renderer->Present(); // 여기까지 도달 못함 GlobalFrameScheduler::GetInstance().SignalNextPresent(playerId); }); ``` ### 테스트 결과 - Frame 16-19: 정상 디코딩 및 Present 수행 - Frame 20 이후: **모든 플레이어가 WaitForPresentTurn()에서 블록** - 결과: 영상 재생 중단, 앱은 응답하지만 프레임 처리 안됨 ### 왜 처음 몇 프레임은 작동했나? 초기 프레임들은 **플레이어들이 순차적으로 DispatcherQueue에 진입**했기 때문에 일시적으로 작동했습니다. 하지만 TRIPLE_FILLING 단계 이후 모든 플레이어가 동시에 Present를 시도하면서 데드락이 발생했습니다. ## Conclusion **Present Sequential Execution은 WinUI3에서 구현 불가능**합니다. **실패 이유:** - ❌ UI 스레드 데드락: 단일 스레드에서 순차 대기 불가능 - ❌ 아키텍처 제약: SwapChainPanel이 UI 스레드 전용 - ❌ 대안 없음: 백그라운드 스레드에서 Present() 호출 불가 **대안 탐색 필요:** - Option 1: DispatcherQueue 외부에서 Present 순서 제어 (불가능) - Option 2: VSync 타이밍 조정으로 QUEUE_DELAY 최소화 - Option 3: 플레이어 수 제한 또는 프레임 레이트 조정 - Option 4: QUEUE_DELAY를 허용하고 다른 병목 해결에 집중 **Next Action**: QUEUE_DELAY 문제는 근본적으로 해결 불가능. Hybrid Round-Robin으로 DECODE 병목만 해결하고, QUEUE_DELAY는 WinUI3 제약으로 받아들임.