← SEARCHABLE ARCHIVE
DirectX 12 · 2026-07-22

DX12 스왑 체인과 Resource Barrier

Back Buffer 수명과 PRESENT·RENDER_TARGET 상태 전환 추적하기

INTERACTIVE READING MODE →

본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.

Swap Chain은 화면을 바꾸는 함수 하나가 아니라 몇 개의 Back Buffer를 표시 시스템과 교대로 사용하는 규칙이다. DirectX 12에서는 현재 버퍼가 렌더 대상인지, 표시 대기 중인지까지 앱이 명시해야 한다.

Flip Model Swap Chain을 만든다

DXGI_SWAP_CHAIN_DESC1 desc{};
desc.Width = width;
desc.Height = height;
desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.SampleDesc = { 1, 0 };
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.BufferCount = 3;
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
desc.Scaling = DXGI_SCALING_STRETCH;

ComPtr<IDXGISwapChain1> swapChain1;
ThrowIfFailed(factory->CreateSwapChainForHwnd(
    commandQueue.Get(),
    hwnd,
    &desc,
    nullptr,
    nullptr,
    &swapChain1));

ThrowIfFailed(swapChain1.As(&swapChain));
frameIndex = swapChain->GetCurrentBackBufferIndex();

D3D12 Swap Chain은 Direct Command Queue와 연결한다. BufferCount = 3이면 Back Buffer가 세 개일 뿐 GPU가 세 프레임을 무조건 동시에 처리한다는 뜻은 아니다. CPU가 재사용하려는 프레임 자원의 Fence를 따로 확인해야 한다.

Back Buffer마다 RTV가 하나씩 필요하다

D3D12_DESCRIPTOR_HEAP_DESC heapDesc{};
heapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
heapDesc.NumDescriptors = kFrameCount;
ThrowIfFailed(device->CreateDescriptorHeap(
    &heapDesc, IID_PPV_ARGS(&rtvHeap)));

const UINT stride = device->GetDescriptorHandleIncrementSize(
    D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

D3D12_CPU_DESCRIPTOR_HANDLE handle =
    rtvHeap->GetCPUDescriptorHandleForHeapStart();

for (UINT i = 0; i < kFrameCount; ++i)
{
    ThrowIfFailed(swapChain->GetBuffer(i, IID_PPV_ARGS(&backBuffers[i])));
    device->CreateRenderTargetView(backBuffers[i].Get(), nullptr, handle);
    rtvHandles[i] = handle;
    handle.ptr += stride;
}

Resource는 실제 이미지 메모리고 RTV는 그 메모리를 Render Target으로 보는 방법을 기록한 Descriptor다. D3D12에서는 모든 Back Buffer에 각각 RTV를 만든다. GetCurrentBackBufferIndex()로 이번 프레임의 Resource와 RTV를 같은 인덱스로 고른다.

Descriptor Handle은 일반 포인터처럼 역참조하지 않는다. Heap 시작 Handle에 Device가 반환한 증분 크기를 더해 인덱스만 이동한다.

Resource State는 메모리의 사용 계약이다

PRESENT
  ↓ transition barrier
RENDER_TARGET
  ↓ clear / draw
PRESENT

GPU는 같은 메모리를 Render Target, Copy 대상, Shader Resource 등 다른 용도로 사용한다. 용도가 바뀌면 Cache 내용을 정리하거나 내부 Layout을 전환해야 할 수 있다. Transition Barrier는 이전 사용과 다음 사용을 Driver에 알리는 계약이다.

D3D12_RESOURCE_BARRIER toRender{};
toRender.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
toRender.Transition.pResource = backBuffers[frameIndex].Get();
toRender.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
toRender.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
toRender.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;

commandList->ResourceBarrier(1, &toRender);

StateBefore는 희망 사항이 아니라 앱이 추적한 현재 상태다. 실제 상태와 다르면 Debug Layer가 일부 오류를 찾지만 모든 Queue와 모든 실행 경로를 완전히 증명해 주는 것은 아니다. 렌더러에는 Resource 또는 Subresource별 상태 추적기가 필요하다.

한 프레임의 명령 순서

auto* backBuffer = backBuffers[frameIndex].Get();
const auto rtv = rtvHandles[frameIndex];

Transition(backBuffer,
           D3D12_RESOURCE_STATE_PRESENT,
           D3D12_RESOURCE_STATE_RENDER_TARGET);

commandList->OMSetRenderTargets(1, &rtv, FALSE, nullptr);

const float clearColor[4] = { 0.03f, 0.04f, 0.07f, 1.0f };
commandList->ClearRenderTargetView(rtv, clearColor, 0, nullptr);

// draw calls

Transition(backBuffer,
           D3D12_RESOURCE_STATE_RENDER_TARGET,
           D3D12_RESOURCE_STATE_PRESENT);

ThrowIfFailed(commandList->Close());
ID3D12CommandList* lists[] = { commandList.Get() };
commandQueue->ExecuteCommandLists(1, lists);
ThrowIfFailed(swapChain->Present(1, 0));

Barrier는 CPU가 즉시 실행하는 동기화 함수가 아니다. Command List에 GPU 작업으로 기록된다. Present도 GPU 전체 완료 표식이 아니다. CPU가 Allocator나 Buffer를 재사용해도 되는 시점은 Fence로 따로 확인한다.

Transition, UAV, Aliasing Barrier를 구분한다

  • Transition Barrier는 Subresource의 사용 상태를 바꾼다.
  • UAV Barrier는 앞선 UAV 접근과 다음 UAV 접근의 순서가 필요함을 알린다.
  • Aliasing Barrier는 같은 Heap 메모리를 다른 Placed Resource로 재사용할 때 소유 전환을 알린다.

Barrier를 많이 넣으면 안전할 것 같지만 Cache Flush와 병렬화를 만들 수 있다. 필요한 의존성만 표현하고, 여러 Transition이 같은 경계에 있다면 한 번의 ResourceBarrier 호출로 배치한다.

Resize는 Back Buffer 수명의 종료다

ResizeBuffers를 호출하기 전에는 기존 Back Buffer를 참조하는 GPU 작업이 끝났고, 모든 ComPtr<ID3D12Resource>가 해제되었으며, 관련 RTV를 더 이상 사용하지 않는지 확인한다.

GPU 완료 대기
→ Back Buffer 참조 해제
→ ResizeBuffers
→ GetCurrentBackBufferIndex
→ Back Buffer와 RTV 재생성
→ Viewport / Scissor / Depth Buffer 갱신

창이 최소화되어 width 또는 height가 0이면 재생성을 미루고 프레임 제출을 건너뛴다. 마우스로 크기를 조절하는 모든 중간 메시지에서 GPU를 전체 대기하면 화면이 심하게 끄덕일 수 있다. 정확성을 먼저 확보한 뒤 Resize 이벤트를 모아 처리하는 정책을 검토한다.

실패를 읽는 순서

  1. GetCurrentBackBufferIndex()와 선택한 RTV 인덱스가 같은지 본다.
  2. 현재 상태가 PRESENT인지 상태 로그를 본다.
  3. RENDER_TARGET으로 전환한 뒤 Clear와 Draw를 기록했는지 본다.
  4. Present전에 다시 PRESENT로 바꾸었는지 본다.
  5. Resize 뒤에 오래된 Resource와 Handle을 재사용하지 않았는지 본다.

Back Buffer를 단순한 이미지 배열로만 보면 Resize, Fence, Barrier 버그가 서로 엉킨다. 현재 소유자, 사용 상태, 재사용 가능 Fence를 한 표에 두면 화면 출력의 수명이 명확해진다.

공식 자료

확인일: 2026-08-15