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

14 KiB
Raw Permalink Blame History

Present Sequential Execution Design

Date: 2025-10-11 Author: Claude Code Status: Implementation in Progress

Problem Statement

Current Issue: UI Thread Overload

4개 플레이어가 동시에 UI 스레드의 DispatcherQueuePresent() 요청을 보내면서 발생하는 문제:

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:

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:

void GlobalFrameScheduler::WaitForPresentTurn(int playerId) {
    std::unique_lock<std::mutex> 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:

void GlobalFrameScheduler::SignalNextPresent(int playerId) {
    std::unique_lock<std::mutex> lock(m_presentMutex);

    // Advance to next player (circular)
    std::lock_guard<std::mutex> 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:

// 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<double, std::milli>(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<double, std::milli>(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<double, std::milli>(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<double, std::milli>(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:

// 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 스레드에서만 호출 가능

실패한 구현

// ❌ 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 제약으로 받아들임.