Files
video-v1/vav2/docs/working/Triple_Buffering_Refactoring_Design.md
2025-10-11 02:08:57 +09:00

37 KiB
Raw Blame History

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. 현재 코드의 동기화 구조

// 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 버퍼링이 정상 동작하는지 확인

테스트 절차:

  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 업데이트가 충분히 빠르다면 현재 방식 유지 (권장)

검증 방법:

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;
        }
    }
}

성공 기준

필수 달성 목표

  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

기술 문서


문서 버전: 1.0 최종 수정: 2025-10-10