# Triple Buffering Refactoring Design **문서 작성일**: 2025-10-10 **작성자**: Claude Code **상태**: 설계 완료, 구현 대기 중 ## 📋 목차 1. [개요](#개요) 2. [현재 문제점](#현재-문제점) 3. [새로운 설계](#새로운-설계) 4. [구현 계획](#구현-계획) 5. [테스트 계획](#테스트-계획) --- ## 개요 ### 목적 현재 staging texture 기반 복사 구조를 제거하고, 진정한 triple buffering 구조로 리팩토링하여 30fps 디코딩 + 60fps 렌더링을 안정적으로 지원 ### 핵심 아이디어 - **Staging texture 제거**: 불필요한 복사 및 동기화 오버헤드 제거 - **명확한 버퍼 역할 분리**: Render, Decode, Idle 3가지 상태로 텍스처 관리 - **33ms 간격 프레임 전환**: 디코딩 완료 시 렌더링/디코딩 인덱스 동시 전환 ### 기대 효과 - ✅ 코드 복잡도 감소 (staging texture 관련 ~200줄 제거) - ✅ 명확한 소유권 (렌더링 중인 텍스처는 디코더가 건드리지 않음) - ✅ 자연스러운 동기화 (33ms 간격으로 자동 동기화) - ✅ NULL 텍스처 문제 근본 해결 --- ## 현재 문제점 ### 1. Staging Texture 기반 복사 구조의 문제 ``` DecodeToSurface → texture[0/1/2] → CopyToStagingTexture → staging → Render (33ms) (GPU copy) (읽기 전용) ``` **문제점**: - **불필요한 복사**: GPU 메모리 간 복사 오버헤드 - **복잡한 동기화**: CopyToStagingTexture + WaitForCopyCompletion - **애매한 소유권**: 원본 텍스처가 언제 재사용 가능한지 불명확 - **NULL 텍스처 버그**: Frame 19에서 texture[0]이 NULL이 되는 문제 ### 2. 현재 코드의 동기화 구조 ```cpp // FrameProcessor.cpp (lines 107-139) if (m_framesDecoded >= 16) { ID3D12Resource* rgbaTexture = m_renderer->GetNextRGBATextureForCUDAInterop(); result = vavcore_decode_to_surface(player, VAVCORE_SURFACE_D3D12_RESOURCE, rgbaTexture, &vavFrame); if (result == VAVCORE_SUCCESS) { auto backend = m_renderer->GetRGBASurfaceBackend(); if (backend) { // 문제: 여기서 staging으로 복사하는데 원본 텍스처 재사용 타이밍이 애매함 HRESULT hr = backend->CopyToStagingTexture(rgbaTexture); hr = backend->WaitForCopyCompletion(); } } } ``` ### 3. Triple Buffering 순환 문제 ``` Frame 16: texture[0] → staging → render Frame 17: texture[1] → staging → render (33ms) Frame 18: texture[2] → staging → render (33ms) Frame 19: texture[0] 재사용 시도 → NULL ❌ ``` --- ## 새로운 설계 ### 1. Triple Buffering 구조 #### 버퍼 상태 정의 ``` texture[0]: RENDERING (현재 화면에 출력 중) texture[1]: IDLE (대기 중, 이미 디코딩 완료) texture[2]: DECODING (현재 디코딩 작업 중) ``` #### 프레임 전환 시퀀스 ``` 초기화 단계 (Frames 0-15): DecodeToSurface(NULL) × 16번 → VavCore 내부 CUDA DPB 채우기 Triple Buffer 채우기 (Frames 16-18): Frame 16: DecodeToSurface → texture[0] Frame 17: DecodeToSurface → texture[1] Frame 18: DecodeToSurface → texture[2] 정상 디코딩/렌더링 (Frame 19+): [State: R=0, D=0] Render: texture[0] 화면 출력 (60fps로 여러 번) 33ms 후 디코딩 완료... [State: R=1, D=1] AdvanceFrame() 호출: - m_renderTextureIndex = 1 (texture[1]로 렌더링 전환) - m_decodeTextureIndex = 1 (texture[1]을 다음 디코딩 타겟으로) DecodeToSurface → texture[0] 덮어쓰기 (이제 안전) Render: texture[1] 화면 출력 33ms 후 디코딩 완료... [State: R=2, D=2] AdvanceFrame() 호출: - m_renderTextureIndex = 2 - m_decodeTextureIndex = 2 DecodeToSurface → texture[1] 덮어쓰기 Render: texture[2] 화면 출력 ``` ### 2. 클래스 구조 변경 #### RGBASurfaceBackend.h 변경사항 ```cpp class RGBASurfaceBackend : public IVideoBackend { public: // 삭제될 메서드 // ❌ ID3D12Resource* GetNextVideoTexture(); // ❌ ID3D12Resource* GetStagingTexture() const; // ❌ HRESULT CopyToStagingTexture(ID3D12Resource* sourceTexture); // ❌ HRESULT WaitForCopyCompletion(); // 새로운 메서드 // ✅ GetCurrentRenderTexture() - 렌더링에서 사용 ID3D12Resource* GetCurrentRenderTexture() const { return m_rgbaTextures[m_renderTextureIndex].Get(); } // ✅ GetNextDecodeTexture() - 디코딩에서 사용 ID3D12Resource* GetNextDecodeTexture() const { return m_rgbaTextures[m_decodeTextureIndex].Get(); } // ✅ AdvanceFrame() - 33ms마다 호출 (디코딩 완료 시) void AdvanceFrame() { // 렌더링을 다음 텍스처로 전환 m_renderTextureIndex = (m_renderTextureIndex + 1) % BUFFER_COUNT; // 디코딩도 다음 텍스처로 전환 (이전 렌더링 텍스처를 덮어씀) m_decodeTextureIndex = (m_decodeTextureIndex + 1) % BUFFER_COUNT; LOGF_INFO("[RGBASurfaceBackend] AdvanceFrame: render=%d, decode=%d", m_renderTextureIndex, m_decodeTextureIndex); } // ✅ GetRenderTextureIndex() - 디버깅용 int GetRenderTextureIndex() const { return m_renderTextureIndex; } int GetDecodeTextureIndex() const { return m_decodeTextureIndex; } private: // 삭제될 멤버 변수 // ❌ ComPtr m_stagingTexture; // ❌ ComPtr m_copyCommandAllocator; // ❌ ComPtr m_copyCommandList; // ❌ ComPtr m_copyFence; // ❌ UINT64 m_copyFenceValue = 0; // ❌ HANDLE m_copyFenceEvent = nullptr; // ❌ bool m_firstCopy = true; // ❌ int m_currentTextureIndex = 0; // 새로운 멤버 변수 // ✅ 렌더링용 텍스처 인덱스 (현재 화면에 출력 중) int m_renderTextureIndex = 0; // ✅ 디코딩용 텍스처 인덱스 (다음 디코딩 타겟) int m_decodeTextureIndex = 0; // 기존 유지 ComPtr m_rgbaTextures[BUFFER_COUNT]; // 3개 텍스처 유지 }; ``` #### FrameProcessor.cpp 변경사항 ```cpp bool FrameProcessor::ProcessFrame(VavCorePlayer* player, std::function onComplete) { // ... (기존 초기 검증 코드 유지) VavCoreVideoFrame vavFrame = {}; VavCoreResult result; if (m_decoderType == VAVCORE_DECODER_DAV1D) { // DAV1D: CPU 디코딩 (기존 로직 유지) result = vavcore_decode_next_frame(player, &vavFrame); if (result == VAVCORE_SUCCESS) { vavFrame.surface_type = VAVCORE_SURFACE_CPU; } } else { // NVDEC/Hardware: D3D12 surface decoding with triple buffering // Phase 1: Initial 16-frame buffering (NULL surface) if (m_framesDecoded < 16) { LOGF_DEBUG("[FrameProcessor] Initial buffering phase: frame %llu/16", m_framesDecoded.load()); result = vavcore_decode_to_surface(player, VAVCORE_SURFACE_D3D12_RESOURCE, nullptr, &vavFrame); } // Phase 2: Fill triple buffer (frames 16, 17, 18) else if (m_framesDecoded < 19) { LOGF_DEBUG("[FrameProcessor] Filling triple buffer: frame %llu/19", m_framesDecoded.load()); 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) { // Triple buffer 채우기 완료 시 인덱스 전진 backend->AdvanceFrame(); } } // Phase 3: Normal decoding (frame 19+) else { auto backend = m_renderer->GetRGBASurfaceBackend(); // 다음 디코딩용 텍스처 가져오기 (현재 렌더링 중이 아닌 텍스처) ID3D12Resource* decodeTexture = backend->GetNextDecodeTexture(); LOGF_DEBUG("[FrameProcessor] Normal decoding: decode_idx=%d, render_idx=%d", backend->GetDecodeTextureIndex(), backend->GetRenderTextureIndex()); result = vavcore_decode_to_surface(player, VAVCORE_SURFACE_D3D12_RESOURCE, decodeTexture, &vavFrame); if (result == VAVCORE_SUCCESS) { // 디코딩 완료 - 렌더링/디코딩 인덱스 전환 backend->AdvanceFrame(); LOGF_INFO("[FrameProcessor] Frame decoded, advanced to render_idx=%d", backend->GetRenderTextureIndex()); } } } // Phase 1 & 2: Buffering 단계는 렌더링 안 함 if (m_framesDecoded < 19) { if (result == VAVCORE_PACKET_ACCEPTED || result == VAVCORE_SUCCESS) { m_framesDecoded++; m_frameProcessing.store(false); if (onComplete) onComplete(true); return true; } } // Phase 3: 정상 렌더링 (기존 로직 유지) if (result != VAVCORE_SUCCESS) { // ... (기존 에러 처리) } m_framesDecoded++; // 렌더링 큐에 추가 (기존 로직 유지) bool enqueued = m_dispatcherQueue.TryEnqueue([this, vavFrame, onComplete, player, processStart]() { // ... (기존 렌더링 로직) }); return true; } ``` #### D3D12VideoRenderer.cpp 변경사항 ```cpp HRESULT D3D12VideoRenderer::RenderVideoFrame(const VavCoreVideoFrame& frame, VavCorePlayer* player) { // ... (기존 초기 검증 코드) if (frame.surface_type == VAVCORE_SURFACE_D3D12_RESOURCE) { auto backend = m_rgbaSurfaceBackend.get(); if (!backend) { return E_NOT_VALID_STATE; } // 현재 렌더링용 텍스처 사용 (이미 디코딩 완료된 안정적인 텍스처) ID3D12Resource* renderTexture = backend->GetCurrentRenderTexture(); if (!renderTexture) { LOGF_ERROR("[D3D12VideoRenderer] Current render texture is NULL!"); return E_INVALIDARG; } LOGF_DEBUG("[D3D12VideoRenderer] Rendering texture at index %d", backend->GetRenderTextureIndex()); // SRV 업데이트 (렌더링용 텍스처로) UpdateSRVForTexture(renderTexture); // 렌더링 수행 (기존 RenderToBackBuffer 로직) hr = backend->RenderToBackBuffer(frame, backBuffer, commandList.Get(), rtvHandle); } // ... (나머지 기존 로직) } ``` ### 3. CreateSrvHeap() 수정 ```cpp HRESULT RGBASurfaceBackend::CreateSrvHeap() { // SRV를 동적으로 업데이트하거나, 3개 텍스처 모두 SRV 생성 후 인덱스로 선택 // Option A: 단일 SRV, 매 프레임 업데이트 (간단) D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = {}; srvHeapDesc.NumDescriptors = 1; srvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; srvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; HRESULT hr = m_device->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&m_srvHeap)); if (FAILED(hr)) return hr; // 초기에는 texture[0]의 SRV 생성 UpdateSRVForCurrentRenderTexture(); return S_OK; } // 새로운 헬퍼 메서드 HRESULT RGBASurfaceBackend::UpdateSRVForCurrentRenderTexture() { D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; srvDesc.Texture2D.MipLevels = 1; CD3DX12_CPU_DESCRIPTOR_HANDLE srvHandle(m_srvHeap->GetCPUDescriptorHandleForHeapStart()); m_device->CreateShaderResourceView( m_rgbaTextures[m_renderTextureIndex].Get(), &srvDesc, srvHandle ); return S_OK; } ``` --- ## 구현 계획 ### Phase 1: RGBASurfaceBackend 리팩토링 #### Step 1.1: 멤버 변수 정리 - [ ] `RGBASurfaceBackend.h`: Staging texture 관련 멤버 변수 제거 - [ ] `RGBASurfaceBackend.h`: 새로운 인덱스 변수 추가 (`m_renderTextureIndex`, `m_decodeTextureIndex`) - [ ] `RGBASurfaceBackend.h`: 새로운 메서드 선언 추가 **파일**: `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Rendering\RGBASurfaceBackend.h` **제거할 코드** (lines 84-96): ```cpp // ❌ 제거 ComPtr m_stagingTexture; ComPtr m_copyCommandAllocator; ComPtr m_copyCommandList; ComPtr m_copyFence; UINT64 m_copyFenceValue = 0; HANDLE m_copyFenceEvent = nullptr; int m_currentTextureIndex = 0; bool m_firstCopy = true; ``` **추가할 코드**: ```cpp // ✅ 추가 int m_renderTextureIndex = 0; // Current texture being rendered int m_decodeTextureIndex = 0; // Next texture for decoding ``` #### Step 1.2: 메서드 시그니처 변경 - [ ] `RGBASurfaceBackend.h`: 삭제될 메서드 제거 선언 - [ ] `RGBASurfaceBackend.h`: 새로운 메서드 추가 선언 **파일**: `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Rendering\RGBASurfaceBackend.h` **제거할 메서드 선언** (lines 45-56): ```cpp // ❌ 제거 ID3D12Resource* GetNextVideoTexture(); HRESULT CopyToStagingTexture(ID3D12Resource* sourceTexture); HRESULT WaitForCopyCompletion(); ID3D12Resource* GetStagingTexture() const; int GetCurrentTextureIndex() const; ``` **추가할 메서드 선언**: ```cpp // ✅ 추가 ID3D12Resource* GetCurrentRenderTexture() const; ID3D12Resource* GetNextDecodeTexture() const; void AdvanceFrame(); int GetRenderTextureIndex() const; int GetDecodeTextureIndex() const; HRESULT UpdateSRVForCurrentRenderTexture(); ``` #### Step 1.3: 구현 파일 수정 - [ ] `RGBASurfaceBackend.cpp`: `CreateVideoTexture()` 수정 (staging texture 생성 제거) - [ ] `RGBASurfaceBackend.cpp`: `Shutdown()` 수정 (staging texture 정리 제거) - [ ] `RGBASurfaceBackend.cpp`: 삭제될 메서드 구현 제거 - [ ] `RGBASurfaceBackend.cpp`: 새로운 메서드 구현 추가 **파일**: `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Rendering\RGBASurfaceBackend.cpp` **수정 범위**: - Lines 80-223: `CreateVideoTexture()` - staging texture 생성 코드 제거 - Lines 44-78: `Shutdown()` - staging texture 정리 코드 제거 - Lines 608-627: `GetNextVideoTexture()` - 완전 제거 - Lines 628-696: `CopyToStagingTexture()` - 완전 제거 - Lines 698-744: `WaitForCopyCompletion()` - 완전 제거 **새로 추가할 구현**: ```cpp ID3D12Resource* RGBASurfaceBackend::GetCurrentRenderTexture() const { return m_rgbaTextures[m_renderTextureIndex].Get(); } ID3D12Resource* RGBASurfaceBackend::GetNextDecodeTexture() const { return m_rgbaTextures[m_decodeTextureIndex].Get(); } void RGBASurfaceBackend::AdvanceFrame() { int prevRender = m_renderTextureIndex; int prevDecode = m_decodeTextureIndex; m_renderTextureIndex = (m_renderTextureIndex + 1) % BUFFER_COUNT; m_decodeTextureIndex = (m_decodeTextureIndex + 1) % BUFFER_COUNT; LOGF_INFO("[RGBASurfaceBackend] AdvanceFrame: render %d->%d, decode %d->%d", prevRender, m_renderTextureIndex, prevDecode, m_decodeTextureIndex); // Update SRV to point to new render texture UpdateSRVForCurrentRenderTexture(); } int RGBASurfaceBackend::GetRenderTextureIndex() const { return m_renderTextureIndex; } int RGBASurfaceBackend::GetDecodeTextureIndex() const { return m_decodeTextureIndex; } HRESULT RGBASurfaceBackend::UpdateSRVForCurrentRenderTexture() { if (!m_srvHeap || !m_rgbaTextures[m_renderTextureIndex]) { return E_NOT_VALID_STATE; } D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; srvDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; srvDesc.Texture2D.MipLevels = 1; CD3DX12_CPU_DESCRIPTOR_HANDLE srvHandle(m_srvHeap->GetCPUDescriptorHandleForHeapStart()); m_device->CreateShaderResourceView( m_rgbaTextures[m_renderTextureIndex].Get(), &srvDesc, srvHandle ); LOGF_DEBUG("[RGBASurfaceBackend] Updated SRV for render texture[%d]", m_renderTextureIndex); return S_OK; } ``` #### Step 1.4: CreateVideoTexture() 간소화 - [ ] Staging texture 생성 코드 제거 (lines 143-223) - [ ] Copy command allocator/list 생성 제거 - [ ] Copy fence 생성 제거 **수정 후 CreateVideoTexture() 구조**: ```cpp HRESULT RGBASurfaceBackend::CreateVideoTexture(uint32_t width, uint32_t height) { LOGF_INFO("[RGBASurfaceBackend] CreateVideoTexture called: %ux%u", width, height); m_videoWidth = width; m_videoHeight = height; HRESULT hr = S_OK; // Create RGBA texture descriptor for CUDA Surface Object write D3D12_RESOURCE_DESC rgbaTextureDesc = {}; // ... (기존 descriptor 설정) D3D12_HEAP_PROPERTIES defaultHeapProps = {}; defaultHeapProps.Type = D3D12_HEAP_TYPE_DEFAULT; // Create triple-buffered textures for (int i = 0; i < BUFFER_COUNT; i++) { hr = m_device->CreateCommittedResource( &defaultHeapProps, D3D12_HEAP_FLAG_SHARED, &rgbaTextureDesc, D3D12_RESOURCE_STATE_COMMON, nullptr, IID_PPV_ARGS(&m_rgbaTextures[i]) ); if (FAILED(hr)) { LOGF_ERROR("[RGBASurfaceBackend] Failed to create RGBA texture[%d]: 0x%08X", i, hr); for (int j = 0; j < i; j++) { m_rgbaTextures[j].Reset(); } return hr; } LOGF_INFO("[RGBASurfaceBackend] Created RGBA texture[%d]: %p", i, m_rgbaTextures[i].Get()); } m_renderTextureIndex = 0; m_decodeTextureIndex = 0; LOGF_INFO("[RGBASurfaceBackend] All %d RGBA textures created successfully", BUFFER_COUNT); // Create SRV for rendering hr = CreateSrvHeap(); if (FAILED(hr)) { return hr; } // Update constant buffer hr = UpdateConstantBuffer(); if (FAILED(hr)) { return hr; } return S_OK; } ``` #### Step 1.5: CreateSrvHeap() 수정 - [ ] Staging texture 대신 현재 render texture로 SRV 생성 - [ ] 초기화 시 texture[0]의 SRV 생성 **파일**: `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Rendering\RGBASurfaceBackend.cpp` **수정 위치**: Lines 379-409 **수정 후 코드**: ```cpp HRESULT RGBASurfaceBackend::CreateSrvHeap() { // Create descriptor heap with 1 descriptor for current render texture D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = {}; srvHeapDesc.NumDescriptors = 1; srvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; srvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; HRESULT hr = m_device->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&m_srvHeap)); if (FAILED(hr)) { return hr; } // Create initial SRV for texture[0] (m_renderTextureIndex = 0) hr = UpdateSRVForCurrentRenderTexture(); if (FAILED(hr)) { return hr; } LOGF_INFO("[RGBASurfaceBackend] Created SRV heap for render texture"); return S_OK; } ``` #### Step 1.6: RenderToBackBuffer() 수정 - [ ] Staging texture 참조 제거 - [ ] 현재 render texture 사용하도록 변경 - [ ] Staging texture 관련 주석 제거 **파일**: `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Rendering\RGBASurfaceBackend.cpp` **수정 위치**: Lines 225-301 (RenderToBackBuffer 함수 전체) **주요 변경사항**: ```cpp // Line 233-238: 기존 staging texture 사용 제거 // ❌ 제거 ID3D12Resource* renderTexture = m_stagingTexture.Get(); if (!renderTexture) { LOGF_ERROR("[RGBASurfaceBackend] RenderToBackBuffer: staging texture is NULL!"); return E_INVALIDARG; } // ✅ 추가 ID3D12Resource* renderTexture = m_rgbaTextures[m_renderTextureIndex].Get(); if (!renderTexture) { LOGF_ERROR("[RGBASurfaceBackend] RenderToBackBuffer: render texture[%d] is NULL!", m_renderTextureIndex); return E_INVALIDARG; } LOGF_DEBUG("[RGBASurfaceBackend] RenderToBackBuffer: using texture[%d], ptr=%p", m_renderTextureIndex, renderTexture); // Line 241-243: Staging texture 관련 주석 제거 // ❌ 제거 // Staging texture is already in PIXEL_SHADER_RESOURCE state (set by CopyToStagingTexture) // No barrier needed here // ✅ 추가 // Render texture is in COMMON state (CUDA managed) // No barrier needed for reading in pixel shader ``` #### Step 1.7: Shutdown() 간소화 - [ ] Staging texture Release 제거 - [ ] Copy command objects Release 제거 - [ ] Copy fence Release 제거 **파일**: `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Rendering\RGBASurfaceBackend.cpp` **수정 위치**: Lines 44-78 **제거할 코드** (lines 59-72): ```cpp // ❌ 제거 // Release staging texture and copy command objects m_copyCommandList.Reset(); m_copyCommandAllocator.Reset(); m_stagingTexture.Reset(); // Close fence event handle if (m_copyFenceEvent != nullptr) { CloseHandle(m_copyFenceEvent); m_copyFenceEvent = nullptr; } // Release fence m_copyFence.Reset(); ``` **수정 후 Shutdown() 구조**: ```cpp void RGBASurfaceBackend::Shutdown() { // Release resources m_constantBuffer.Reset(); m_pixelShaderBlob.Reset(); m_vertexShaderBlob.Reset(); m_srvHeap.Reset(); m_pipelineState.Reset(); m_rootSignature.Reset(); // Release all texture buffers for (int i = 0; i < BUFFER_COUNT; i++) { m_rgbaTextures[i].Reset(); } m_renderTextureIndex = 0; m_decodeTextureIndex = 0; // Clear references (not owned) m_device = nullptr; m_commandQueue = nullptr; m_initialized = false; } ``` ### Phase 2: FrameProcessor 수정 #### Step 2.1: ProcessFrame() 로직 변경 - [ ] 초기 16-frame buffering (NULL surface) - 기존 유지 - [ ] Triple buffer 채우기 (frames 16-18) - 새로운 로직 - [ ] 정상 디코딩 (frame 19+) - 새로운 로직 **파일**: `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Playback\FrameProcessor.cpp` **수정 위치**: Lines 88-140 (NVDEC/Hardware 디코딩 부분) **수정 후 코드**: ```cpp } else { // NVDEC/Hardware: D3D12 surface decoding with triple buffering // Phase 1: Initial 16-frame buffering (NULL surface) if (m_framesDecoded < 16) { LOGF_DEBUG("[FrameProcessor] Initial buffering phase: frame %llu/16", m_framesDecoded.load()); result = vavcore_decode_to_surface( player, VAVCORE_SURFACE_D3D12_RESOURCE, nullptr, &vavFrame ); } // Phase 2: Fill triple buffer (frames 16, 17, 18) else if (m_framesDecoded < 19) { LOGF_DEBUG("[FrameProcessor] Filling triple buffer: frame %llu/19", m_framesDecoded.load()); auto backend = m_renderer->GetRGBASurfaceBackend(); if (!backend) { LOGF_ERROR("[FrameProcessor] RGBASurfaceBackend is NULL"); m_frameProcessing.store(false); if (onComplete) onComplete(false); return false; } ID3D12Resource* decodeTexture = backend->GetNextDecodeTexture(); if (!decodeTexture) { LOGF_ERROR("[FrameProcessor] Failed to get decode texture"); m_frameProcessing.store(false); if (onComplete) onComplete(false); return false; } result = vavcore_decode_to_surface( player, VAVCORE_SURFACE_D3D12_RESOURCE, decodeTexture, &vavFrame ); if (result == VAVCORE_SUCCESS) { // Triple buffer 채우기 완료 시 인덱스 전진 backend->AdvanceFrame(); LOGF_INFO("[FrameProcessor] Triple buffer[%llu] filled, advanced frame", m_framesDecoded.load()); } } // Phase 3: Normal decoding (frame 19+) else { auto backend = m_renderer->GetRGBASurfaceBackend(); if (!backend) { LOGF_ERROR("[FrameProcessor] RGBASurfaceBackend is NULL"); m_frameProcessing.store(false); if (onComplete) onComplete(false); return false; } // 다음 디코딩용 텍스처 가져오기 (현재 렌더링 중이 아닌 텍스처) ID3D12Resource* decodeTexture = backend->GetNextDecodeTexture(); if (!decodeTexture) { LOGF_ERROR("[FrameProcessor] Failed to get decode texture"); m_frameProcessing.store(false); if (onComplete) onComplete(false); return false; } LOGF_DEBUG("[FrameProcessor] Normal decoding: decode_idx=%d, render_idx=%d", backend->GetDecodeTextureIndex(), backend->GetRenderTextureIndex()); result = vavcore_decode_to_surface( player, VAVCORE_SURFACE_D3D12_RESOURCE, decodeTexture, &vavFrame ); if (result == VAVCORE_SUCCESS) { // 디코딩 완료 - 렌더링/디코딩 인덱스 전환 backend->AdvanceFrame(); LOGF_INFO("[FrameProcessor] Frame decoded, advanced to render_idx=%d", backend->GetRenderTextureIndex()); } } } ``` #### Step 2.2: 렌더링 시작 조건 변경 - [ ] Phase 1 & 2 (frames 0-18): 렌더링 안 함 - [ ] Phase 3 (frame 19+): 정상 렌더링 **파일**: `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Playback\FrameProcessor.cpp` **수정 위치**: Lines 146-179 **수정 후 코드**: ```cpp auto decodeEnd = std::chrono::high_resolution_clock::now(); double decodeTime = std::chrono::duration(decodeEnd - decodeStart).count(); // Phase 1 & 2: Buffering 단계는 렌더링 안 함 if (m_framesDecoded < 19) { if (result == VAVCORE_PACKET_ACCEPTED || result == VAVCORE_SUCCESS) { m_framesDecoded++; LOGF_DEBUG("[FrameProcessor] Buffering frame %llu, no rendering yet", m_framesDecoded.load()); m_frameProcessing.store(false); if (onComplete) onComplete(true); return true; } } // Error handling for all phases if (result != VAVCORE_SUCCESS) { if (result == VAVCORE_END_OF_STREAM) { LOGF_INFO("[FrameProcessor] End of stream"); m_frameProcessing.store(false); if (onComplete) onComplete(true); return false; } if (result == VAVCORE_PACKET_ACCEPTED) { // VavCore CUDA DPB buffering LOGF_DEBUG("[FrameProcessor] PACKET ACCEPTED - Frame buffered"); m_framesDecoded++; m_frameProcessing.store(false); if (onComplete) onComplete(true); return true; } // All other errors m_decodeErrors++; LOGF_ERROR("[FrameProcessor] Decode ERROR: result=%d", result); m_frameProcessing.store(false); if (onComplete) onComplete(false); return false; } m_framesDecoded++; LOGF_INFO("[FrameProcessor] DECODE: %.1f ms", decodeTime); // Phase 3: Enqueue render on UI thread (frame 19+) bool enqueued = m_dispatcherQueue.TryEnqueue([this, vavFrame, onComplete, player, processStart]() { // ... (기존 렌더링 로직 유지) }); ``` #### Step 2.3: CopyToStagingTexture 호출 제거 - [ ] Lines 123-139의 CopyToStagingTexture + WaitForCopyCompletion 제거 **파일**: `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Playback\FrameProcessor.cpp` **제거할 코드** (lines 123-139): ```cpp // ❌ 완전 제거 // After successful decode, copy to staging texture for safe rendering if (result == VAVCORE_SUCCESS) { auto backend = m_renderer->GetRGBASurfaceBackend(); if (backend) { HRESULT hr = backend->CopyToStagingTexture(rgbaTexture); if (FAILED(hr)) { LOGF_ERROR("[FrameProcessor] Failed to copy to staging texture: 0x%08X", hr); } else { // Wait for GPU copy to complete before proceeding hr = backend->WaitForCopyCompletion(); if (FAILED(hr)) { LOGF_ERROR("[FrameProcessor] Failed to wait for copy completion: 0x%08X", hr); } else { LOGF_INFO("[FrameProcessor] GPU copy completed, staging texture ready"); } } } } ``` ### Phase 3: D3D12VideoRenderer 수정 #### Step 3.1: RenderVideoFrame() 수정 - [ ] GetStagingTexture() 제거 - [ ] GetCurrentRenderTexture() 사용 **파일**: `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Rendering\D3D12VideoRenderer.cpp` **수정 위치**: RenderVideoFrame() 함수 내부 **주요 변경사항**: ```cpp // ❌ 제거 (기존 코드 없음, RenderToBackBuffer가 내부에서 처리) // ✅ RenderToBackBuffer에서 자동으로 GetCurrentRenderTexture() 사용 // 추가 수정 불필요 (RGBASurfaceBackend::RenderToBackBuffer가 이미 올바른 텍스처 사용) ``` ### Phase 4: 빌드 및 초기 테스트 #### Step 4.1: 빌드 오류 수정 - [ ] Vav2Player.vcxproj 빌드 - [ ] 컴파일 오류 수정 - [ ] 링크 오류 수정 **빌드 명령어**: ```bash cd "D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player" "/c/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin/MSBuild.exe" Vav2Player.vcxproj //p:Configuration=Debug //p:Platform=x64 //v:minimal ``` #### Step 4.2: 기본 동작 테스트 - [ ] 애플리케이션 실행 - [ ] 비디오 로드 - [ ] 초기 19프레임 버퍼링 확인 - [ ] 정상 재생 시작 확인 **예상 로그 패턴**: ``` [FrameProcessor] Initial buffering phase: frame 0/16 ... [FrameProcessor] Initial buffering phase: frame 15/16 [FrameProcessor] Filling triple buffer: frame 16/19 [RGBASurfaceBackend] AdvanceFrame: render 0->1, decode 0->1 [FrameProcessor] Triple buffer[16] filled, advanced frame [FrameProcessor] Filling triple buffer: frame 17/19 [RGBASurfaceBackend] AdvanceFrame: render 1->2, decode 1->2 [FrameProcessor] Triple buffer[17] filled, advanced frame [FrameProcessor] Filling triple buffer: frame 18/19 [RGBASurfaceBackend] AdvanceFrame: render 2->0, decode 2->0 [FrameProcessor] Triple buffer[18] filled, advanced frame [FrameProcessor] Normal decoding: decode_idx=0, render_idx=0 [FrameProcessor] Frame decoded, advanced to render_idx=1 [FrameProcessor] DECODE: XX.X ms [FrameProcessor] RENDER: XX.X ms | PRESENT: XX.X ms ``` --- ## 테스트 계획 ### Test Case 1: 초기 버퍼링 단계 검증 **목적**: 16-frame + 3-frame 버퍼링이 정상 동작하는지 확인 **테스트 절차**: 1. 비디오 파일 로드 2. 재생 시작 3. time.log 확인 **예상 결과**: ``` ✅ Frame 0-15: "Initial buffering phase" 로그 16개 ✅ Frame 16-18: "Filling triple buffer" 로그 3개 ✅ Frame 19+: "Normal decoding" 로그 ✅ 각 triple buffer 채우기 후 "AdvanceFrame" 로그 ``` **실패 조건**: - ❌ 19프레임 이전에 렌더링 시도 - ❌ Triple buffer 채우기 중 인덱스 전환 실패 - ❌ NULL 텍스처 에러 ### Test Case 2: Triple Buffering 순환 검증 **목적**: texture[0] → texture[1] → texture[2] → texture[0] 순환이 정상인지 확인 **테스트 절차**: 1. 30프레임 이상 재생 2. time.log에서 render_idx, decode_idx 추적 **예상 결과**: ``` ✅ Frame 19: render_idx=0, decode_idx=0 ✅ Frame 20: render_idx=1, decode_idx=1 ✅ Frame 21: render_idx=2, decode_idx=2 ✅ Frame 22: render_idx=0, decode_idx=0 (순환 완료) ✅ 모든 프레임에서 NULL 텍스처 없음 ``` **실패 조건**: - ❌ Frame 22에서 texture[0] NULL 발생 - ❌ render_idx와 decode_idx가 동일하지 않음 - ❌ 순환 패턴이 깨짐 ### Test Case 3: 60fps 렌더링 안정성 **목적**: 30fps 디코딩 + 60fps 렌더링 시나리오에서 안정성 확인 **테스트 절차**: 1. 60fps 모니터에서 비디오 재생 (VSync 활성화) 2. 1분 이상 재생 3. 프레임 드롭, 스터터링 확인 **예상 결과**: ``` ✅ 부드러운 60fps 렌더링 ✅ 프레임 드롭 0개 ✅ 같은 디코딩 프레임이 2번 렌더링됨 ✅ 메모리 누수 없음 ``` **실패 조건**: - ❌ 스터터링 발생 - ❌ 프레임 드롭 발생 - ❌ 디코딩/렌더링 비동기 깨짐 ### Test Case 4: Seek 및 Reset 동작 **목적**: 탐색 및 재시작 시 triple buffering이 올바르게 재초기화되는지 확인 **테스트 절차**: 1. 비디오 중간으로 seek 2. 재생 재시작 3. 버퍼링 단계가 다시 정상 실행되는지 확인 **예상 결과**: ``` ✅ Seek 후 16-frame buffering 재시작 ✅ Triple buffer 재초기화 ✅ render_idx, decode_idx 모두 0으로 리셋 ✅ 정상 재생 재개 ``` **실패 조건**: - ❌ Seek 후 인덱스 리셋 실패 - ❌ 버퍼링 단계 스킵 - ❌ 이전 프레임 잔상 ### Test Case 5: 성능 측정 **목적**: Staging texture 제거 후 성능 개선 확인 **측정 항목**: - 디코딩 시간 (ms) - 렌더링 시간 (ms) - Present 시간 (ms) - 전체 프레임 처리 시간 (ms) **예상 결과**: ``` ✅ 디코딩 시간: 10-15ms (기존과 동일) ✅ 렌더링 시간: 0.4-0.8ms (기존과 동일 또는 개선) ✅ GPU copy 제거로 2-5ms 절약 ✅ 전체 처리 시간: 11-13ms (개선) ``` **비교 기준** (기존 staging texture 방식): ``` [FrameProcessor] DECODE: 34.0 ms (late binding 포함) [FrameProcessor] RENDER: 0.8 ms | PRESENT: 1.9 ms | TOTAL: 37.1 ms ``` --- ## 위험 요소 및 대응 ### 위험 1: SRV 업데이트 오버헤드 **문제**: 매 프레임마다 `UpdateSRVForCurrentRenderTexture()` 호출 시 성능 저하 가능 **대응 방안**: - Option A: SRV를 3개 생성하고 descriptor table로 선택 - Option B: SRV 업데이트가 충분히 빠르다면 현재 방식 유지 (권장) **검증 방법**: ```cpp auto srvUpdateStart = std::chrono::high_resolution_clock::now(); UpdateSRVForCurrentRenderTexture(); auto srvUpdateEnd = std::chrono::high_resolution_clock::now(); double srvUpdateTime = std::chrono::duration(srvUpdateEnd - srvUpdateStart).count(); LOGF_DEBUG("[RGBASurfaceBackend] SRV update time: %.3f ms", srvUpdateTime); ``` ### 위험 2: 인덱스 동기화 실패 **문제**: render_idx와 decode_idx가 올바르게 전환되지 않아 같은 텍스처를 동시에 읽기/쓰기 **대응 방안**: - 모든 AdvanceFrame() 호출에 로깅 추가 - 인덱스 충돌 감지 로직 추가 **검증 코드**: ```cpp void RGBASurfaceBackend::AdvanceFrame() { int nextRender = (m_renderTextureIndex + 1) % BUFFER_COUNT; int nextDecode = (m_decodeTextureIndex + 1) % BUFFER_COUNT; // Safety check: render and decode should move together if (nextRender != nextDecode) { LOGF_ERROR("[RGBASurfaceBackend] Index mismatch! render=%d, decode=%d", nextRender, nextDecode); } m_renderTextureIndex = nextRender; m_decodeTextureIndex = nextDecode; } ``` ### 위험 3: Triple buffer 채우기 실패 **문제**: Frames 16-18 디코딩 중 에러 발생 시 버퍼가 부분적으로만 채워짐 **대응 방안**: - 에러 발생 시 재시도 로직 - 최소 버퍼 확보 검증 **검증 코드**: ```cpp if (m_framesDecoded >= 19) { // Verify all buffers are filled for (int i = 0; i < BUFFER_COUNT; i++) { if (!m_rgbaTextures[i].Get()) { LOGF_ERROR("[RGBASurfaceBackend] Triple buffer[%d] is NULL!", i); return E_FAIL; } } } ``` --- ## 성공 기준 ### 필수 달성 목표 1. ✅ **빌드 성공**: 모든 컴파일/링크 오류 해결 2. ✅ **NULL 텍스처 제거**: Frame 19+ NULL 텍스처 문제 완전 해결 3. ✅ **부드러운 재생**: 30fps 디코딩 + 60fps 렌더링 안정적 동작 4. ✅ **코드 단순화**: Staging texture 관련 ~200줄 제거 ### 우수 달성 목표 1. ✅ **성능 개선**: GPU copy 제거로 2-5ms 절약 2. ✅ **메모리 효율**: Staging texture 제거로 VRAM 절약 3. ✅ **코드 가독성**: 명확한 triple buffering 구조 ### 탁월 달성 목표 1. ✅ **확장성**: 다른 렌더링 백엔드에도 적용 가능한 구조 2. ✅ **문서화**: 완전한 설계 문서 및 코드 주석 3. ✅ **테스트 커버리지**: 5개 테스트 케이스 모두 통과 --- ## 일정 및 마일스톤 ### Milestone 1: 코드 리팩토링 (1-2시간) - [ ] Phase 1: RGBASurfaceBackend 리팩토링 - [ ] Phase 2: FrameProcessor 수정 - [ ] Phase 3: D3D12VideoRenderer 수정 - [ ] Phase 4: 빌드 및 초기 테스트 ### Milestone 2: 기능 검증 (30분) - [ ] Test Case 1: 초기 버퍼링 단계 검증 - [ ] Test Case 2: Triple Buffering 순환 검증 - [ ] Test Case 3: 60fps 렌더링 안정성 ### Milestone 3: 고급 테스트 (30분) - [ ] Test Case 4: Seek 및 Reset 동작 - [ ] Test Case 5: 성능 측정 - [ ] 문서 업데이트 **총 예상 시간**: 2-3시간 --- ## 참고 자료 ### 관련 파일 - `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Rendering\RGBASurfaceBackend.h` - `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Rendering\RGBASurfaceBackend.cpp` - `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Playback\FrameProcessor.h` - `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Playback\FrameProcessor.cpp` - `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Rendering\D3D12VideoRenderer.h` - `D:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Rendering\D3D12VideoRenderer.cpp` ### 기술 문서 - [Vav2Player Stutter Fix Design](Vav2Player_Stutter_Fix_Design.md) - Staging texture 도입 배경 - [VavCore NVDEC DPB Redesign](../../../docs/completed/windows/VavCore_NVDEC_DPB_Redesign.md) - CUDA DPB 구조 - [Vav2Player NVDEC DPB Integration](../../../docs/completed/windows/Vav2Player_NVDEC_DPB_Integration.md) - Late binding 메커니즘 --- *문서 버전: 1.0* *최종 수정: 2025-10-10*