37 KiB
Triple Buffering Refactoring Design
문서 작성일: 2025-10-10 작성자: Claude Code 상태: 설계 완료, 구현 대기 중
📋 목차
개요
목적
현재 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. 현재 코드의 동기화 구조
// 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 변경사항
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<ID3D12Resource> m_stagingTexture;
// ❌ ComPtr<ID3D12CommandAllocator> m_copyCommandAllocator;
// ❌ ComPtr<ID3D12GraphicsCommandList> m_copyCommandList;
// ❌ ComPtr<ID3D12Fence> 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<ID3D12Resource> m_rgbaTextures[BUFFER_COUNT]; // 3개 텍스처 유지
};
FrameProcessor.cpp 변경사항
bool FrameProcessor::ProcessFrame(VavCorePlayer* player,
std::function<void(bool success)> 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 변경사항
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() 수정
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):
// ❌ 제거
ComPtr<ID3D12Resource> m_stagingTexture;
ComPtr<ID3D12CommandAllocator> m_copyCommandAllocator;
ComPtr<ID3D12GraphicsCommandList> m_copyCommandList;
ComPtr<ID3D12Fence> m_copyFence;
UINT64 m_copyFenceValue = 0;
HANDLE m_copyFenceEvent = nullptr;
int m_currentTextureIndex = 0;
bool m_firstCopy = true;
추가할 코드:
// ✅ 추가
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):
// ❌ 제거
ID3D12Resource* GetNextVideoTexture();
HRESULT CopyToStagingTexture(ID3D12Resource* sourceTexture);
HRESULT WaitForCopyCompletion();
ID3D12Resource* GetStagingTexture() const;
int GetCurrentTextureIndex() const;
추가할 메서드 선언:
// ✅ 추가
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()- 완전 제거
새로 추가할 구현:
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() 구조:
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
수정 후 코드:
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 함수 전체)
주요 변경사항:
// 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):
// ❌ 제거
// 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() 구조:
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 디코딩 부분)
수정 후 코드:
} 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
수정 후 코드:
auto decodeEnd = std::chrono::high_resolution_clock::now();
double decodeTime = std::chrono::duration<double, std::milli>(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):
// ❌ 완전 제거
// 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() 함수 내부
주요 변경사항:
// ❌ 제거 (기존 코드 없음, RenderToBackBuffer가 내부에서 처리)
// ✅ RenderToBackBuffer에서 자동으로 GetCurrentRenderTexture() 사용
// 추가 수정 불필요 (RGBASurfaceBackend::RenderToBackBuffer가 이미 올바른 텍스처 사용)
Phase 4: 빌드 및 초기 테스트
Step 4.1: 빌드 오류 수정
- Vav2Player.vcxproj 빌드
- 컴파일 오류 수정
- 링크 오류 수정
빌드 명령어:
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 버퍼링이 정상 동작하는지 확인
테스트 절차:
- 비디오 파일 로드
- 재생 시작
- 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] 순환이 정상인지 확인
테스트 절차:
- 30프레임 이상 재생
- 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 렌더링 시나리오에서 안정성 확인
테스트 절차:
- 60fps 모니터에서 비디오 재생 (VSync 활성화)
- 1분 이상 재생
- 프레임 드롭, 스터터링 확인
예상 결과:
✅ 부드러운 60fps 렌더링
✅ 프레임 드롭 0개
✅ 같은 디코딩 프레임이 2번 렌더링됨
✅ 메모리 누수 없음
실패 조건:
- ❌ 스터터링 발생
- ❌ 프레임 드롭 발생
- ❌ 디코딩/렌더링 비동기 깨짐
Test Case 4: Seek 및 Reset 동작
목적: 탐색 및 재시작 시 triple buffering이 올바르게 재초기화되는지 확인
테스트 절차:
- 비디오 중간으로 seek
- 재생 재시작
- 버퍼링 단계가 다시 정상 실행되는지 확인
예상 결과:
✅ 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 업데이트가 충분히 빠르다면 현재 방식 유지 (권장)
검증 방법:
auto srvUpdateStart = std::chrono::high_resolution_clock::now();
UpdateSRVForCurrentRenderTexture();
auto srvUpdateEnd = std::chrono::high_resolution_clock::now();
double srvUpdateTime = std::chrono::duration<double, std::milli>(srvUpdateEnd - srvUpdateStart).count();
LOGF_DEBUG("[RGBASurfaceBackend] SRV update time: %.3f ms", srvUpdateTime);
위험 2: 인덱스 동기화 실패
문제: render_idx와 decode_idx가 올바르게 전환되지 않아 같은 텍스처를 동시에 읽기/쓰기
대응 방안:
- 모든 AdvanceFrame() 호출에 로깅 추가
- 인덱스 충돌 감지 로직 추가
검증 코드:
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 디코딩 중 에러 발생 시 버퍼가 부분적으로만 채워짐
대응 방안:
- 에러 발생 시 재시도 로직
- 최소 버퍼 확보 검증
검증 코드:
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;
}
}
}
성공 기준
필수 달성 목표
- ✅ 빌드 성공: 모든 컴파일/링크 오류 해결
- ✅ NULL 텍스처 제거: Frame 19+ NULL 텍스처 문제 완전 해결
- ✅ 부드러운 재생: 30fps 디코딩 + 60fps 렌더링 안정적 동작
- ✅ 코드 단순화: Staging texture 관련 ~200줄 제거
우수 달성 목표
- ✅ 성능 개선: GPU copy 제거로 2-5ms 절약
- ✅ 메모리 효율: Staging texture 제거로 VRAM 절약
- ✅ 코드 가독성: 명확한 triple buffering 구조
탁월 달성 목표
- ✅ 확장성: 다른 렌더링 백엔드에도 적용 가능한 구조
- ✅ 문서화: 완전한 설계 문서 및 코드 주석
- ✅ 테스트 커버리지: 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.hD:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Rendering\RGBASurfaceBackend.cppD:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Playback\FrameProcessor.hD:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Playback\FrameProcessor.cppD:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Rendering\D3D12VideoRenderer.hD:\Project\video-av1\vav2\platforms\windows\applications\vav2player\Vav2Player\src\Rendering\D3D12VideoRenderer.cpp
기술 문서
- Vav2Player Stutter Fix Design - Staging texture 도입 배경
- VavCore NVDEC DPB Redesign - CUDA DPB 구조
- Vav2Player NVDEC DPB Integration - Late binding 메커니즘
문서 버전: 1.0 최종 수정: 2025-10-10