컨텐츠 검색
[DirectX 11] 1. 렌더링 엔진의 골격: Device, Context 그리고 SwapChain의 이해

2026. 1. 18. 22:44DirectX 스터디

DirectX 11 학습의 첫걸음은 흔히 '좌절'로 시작되곤 합니다. 화면에 삼각형 하나를 띄우기 위해 작성해야 하는 윈도우 생성, 디바이스 초기화, 스왑 체인 설정 등 수백 줄의 초기화 코드(Boilerplate Code) 때문입니다.


이번 포스팅에서는 Microsoft의 DirectXTK(DirectX Tool Kit)를 활용하여 이 지루한 과정을 단축하고, 그 뒤에 숨겨진 렌더링 엔진의 핵심 객체인 Device, Context, SwapChain의 역할과 작동 원리를 분석해보았습니다. 단순히 라이브러리를 가져다 쓰는 것을 넘어, 엔진이 내부적으로 어떻게 하드웨어를 제어하는지 이해하는 것이 목표입니다.


1. DirectXTK와 DeviceResources

DirectX 11의 초기화 코드는 매우 복잡하지만, 결국 목적은 GPU와 통신할 준비를 하는 것입니다.

DirectXTK는 DeviceResources라는 클래스를 통해 이 복잡한 과정을 캡슐화합니다.


우리는 이 클래스 덕분에 RenderUpdate라는 게임 로직의 본질에 집중할 수 있습니다.

하지만 엔진 개발자를 지향한다면 DeviceResources 내부에서 생성되는 3가지 핵심 인터페이스를 반드시 이해해야 합니다.

graph TD
    %% 노드 스타일 정의
    classDef main fill:#333,stroke:#fff,stroke-width:2px,color:#fff;
    classDef abstract fill:#005c99,stroke:#fff,stroke-width:2px,color:#fff;
    classDef dx fill:#fff,stroke:#333,stroke-width:1px,color:#000;

    Game[Game Class<br/>Logic & Loop]:::main -->|Owns & Uses| DR[DeviceResources<br/>DirectXTK Helper]:::abstract

    subgraph DirectX_Internals [DirectX 11 Core Objects]
        direction TB
        style DirectX_Internals fill:#f4f4f4,stroke:#333,stroke-dasharray: 5 5

        DR -- Wraps --> Device[ID3D11Device<br/>The Creator]:::dx
        DR -- Wraps --> Context[ID3D11DeviceContext<br/>The Worker]:::dx
        DR -- Wraps --> SwapChain[IDXGISwapChain<br/>The Presenter]:::dx
    end

    %% 관계 설명
    Device -.->|Creates| Resources(Textures / Buffers)
    Context -.->|Draws| Resources
    Context -.->|Renders to| SwapChain

① The Creator: ID3D11Device (디바이스)

  • 정의: 그래픽 카드(GPU) 하드웨어 그 자체를 추상화한 가상 객체입니다.
  • 역할: 리소스 생성(Creation)과 메모리 할당을 담당합니다.
  • 설명: 텍스처(이미지), 버퍼(정점 정보), 셰이더 등을 만들 때 우리는 항상 이 Device 객체에게 요청해야 합니다. 즉, 렌더링에 필요한 '재료'를 만드는 공장장과 같습니다.

② The Worker: ID3D11DeviceContext (디바이스 컨텍스트)

  • 정의: GPU에게 명령을 내리는 인터페이스입니다.
  • 역할: 렌더링 명령(Command) 수행 및 파이프라인 설정을 담당합니다.
  • 설명: "이 텍스처를 사용해라(IA 단계)", "지금 그려라(Draw)"와 같은 실질적인 작업 지시는 모두 Context가 담당합니다. Device가 재료를 만들면, Context는 그 재료를 가지고 요리를 하는 셰프라고 볼 수 있습니다.

③ The Presenter: IDXGISwapChain (스왑 체인)

  • 정의: 렌더링된 이미지를 모니터에 출력하기 위한 버퍼 관리자입니다.
  • 역할: 더블 버퍼링(Double Buffering)을 통해 부드러운 화면 전환을 관리합니다.

2. Deep Dive: 티어링과 더블 버퍼링

그래픽스 프로그래밍에서 가장 중요한 개념 중 하나인 티어링(Tearing) 현상과 이를 해결하는 더블 버퍼링에 대해 자세히 알아보겠습니다.

티어링(Tearing)이란?

티어링은 화면 가로 방향으로 이미지가 찢어진 것처럼 보이는 현상입니다. 게임을 할 때 화면을 급격히 돌리면 위쪽은 현재 프레임인데, 아래쪽은 이전 프레임이 출력되어 어긋나 보이는 경우가 이에 해당합니다.

발생 원인: 모니터와 GPU의 속도 차이

이 현상은 모니터의 주사율(Refresh Rate, Hz)GPU의 렌더링 속도(FPS)가 불일치할 때 발생합니다.

  1. 모니터의 작동 방식 (Scanline): 모니터는 화면을 한 번에 '짠' 하고 보여주는 것이 아닙니다. 아주 빠른 속도로 왼쪽 위에서부터 오른쪽 아래까지 한 줄씩 픽셀을 갱신합니다(Raster Scan).
    • Raster Scan: CRT 모니터나 TV 등에서 화면을 그릴 때, 전자 빔을 이용해 화면의 왼쪽 상단에서 우측 하단까지 픽셀 단위로 가로 줄(Row)을 따라 한 줄씩 순차적으로 스캔하여 이미지를 표시하는 방식
  2. GPU의 간섭: 모니터가 화면의 절반쯤 그렸을 때, GPU가 새로운 프레임 렌더링을 끝내고 프레임 버퍼를 덮어써 버린다면 어떻게 될까요?
    • 결과: 모니터는 윗부분은 '이전 프레임'을 그리고, 아랫부분은 방금 갱신된 '새 프레임'을 그리게 됩니다. 이 두 프레임의 경계선이 우리 눈에는 '찢어짐'으로 보입니다.

해결책: 더블 버퍼링과 VSync

이 문제를 해결하기 위해 DirectX는 스왑 체인(Swap Chain)을 통한 더블 버퍼링 구조를 사용합니다.

sequenceDiagram
    participant GPU
    participant BackBuffer as Back Buffer
    participant FrontBuffer as Front Buffer
    participant Monitor

    Note over GPU, BackBuffer: Frame N 그리기 시작
    GPU->>BackBuffer: Draw(Triangle)
    GPU->>BackBuffer: Draw(Character)

    Note over Monitor, FrontBuffer: Frame N-1 출력 중
    FrontBuffer->>Monitor: Scanline Output

    Note over GPU, BackBuffer: Frame N 완성!

    rect rgb(255, 240, 200)
    Note over BackBuffer, FrontBuffer: Present(Switch)
    BackBuffer->>FrontBuffer: Swap Roles (Pointer Flip)
    end

    Note over Monitor, FrontBuffer: 이제 Frame N 출력 시작
    FrontBuffer->>Monitor: Scanline Output

    Note over GPU, BackBuffer: Frame N+1 그리기 시작
    GPU->>BackBuffer: Clear & Draw...
  • Front Buffer (전면 버퍼): 현재 모니터가 읽어서 화면에 보여주고 있는 버퍼입니다.
  • Back Buffer (후면 버퍼): GPU가 다음에 보여줄 그림을 열심히 그리고 있는 버퍼입니다.
  • Present (제출): Back Buffer에 그림이 완성되면, Front Buffer와 역할을 서로 맞바꿉니다(Swap).

이때 중요한 것이 수직 동기화(VSync)입니다. VSync를 켜면 모니터가 화면을 다 그리고 다시 처음으로 돌아가는 짧은 휴식 시간(Vertical Blanking Interval)이 올 때까지 GPU는 버퍼를 교체하지 않고 대기합니다. 덕분에 티어링 없는 완벽한 화면을 얻을 수 있습니다.


3. 코드 흐름 분석

DirectXTK 템플릿의 코드 흐름을 보며 위 개념들이 실제로 어떻게 구현되어 있는지 확인해보았습니다.

(1) 초기화 (Initialization)

Game::Initialize 함수에서는 DeviceResources를 통해 하드웨어와 연결을 시도합니다.

// 초기화: 실행에 필요한 Direct3D 리소스를 초기화합니다.
void Game::Initialize(HWND window, int width, int height)
{
    // 1. 디바이스 리소스에 윈도우 핸들(HWND)과 크기 정보를 전달합니다.
    m_deviceResources->SetWindow(window, width, height);

    // 2. 이 함수 내부에서 D3D11CreateDevice(), CreateSwapChain() 등이 호출됩니다.
    //    즉, GPU와의 실질적인 연결이 이 한 줄로 처리됩니다.
    m_deviceResources->CreateDeviceResources();

    // 3. 디바이스 생성 후 필요한 리소스(텍스처 등)를 로드하는 함수를 호출합니다.
    CreateDeviceDependentResources();

    // 4. 창 크기에 의존적인 리소스(후면 버퍼 크기 등)를 설정합니다.
    m_deviceResources->CreateWindowSizeDependentResources();

    // 5. 윈도우 크기에 맞춰 우리 게임의 설정(화면 중앙 좌표 등)도 업데이트합니다.
    CreateWindowSizeDependentResources();
}

(2) 렌더링 파이프라인의 시작 (Render Flow)

게임 루프의 핵심인 Render 함수는 Clear -> Draw -> Present의 3단계 흐름을 따릅니다.

void Game::Render()
{
    // 1. Clear (도화지 지우기)
    // 현재 프레임을 그리기 전, Back Buffer를 깨끗하게 지웁니다.
    // 매 프레임 잔상을 없애기 위한 필수 단계입니다.
    m_deviceResources->Clear(); 

    // 2. Draw (그리기)
    // 추후 이곳에 SpriteBatch나 PrimitiveBatch를 사용하여 
    // 실제 게임 오브젝트를 그리는 명령(Draw Call)이 들어갑니다.
    // context->Draw...(); 

    // 3. Present (제출하기)
    // Back Buffer와 Front Buffer를 교체(Swap)하여 화면에 송출합니다.
    // 이때 VSync 설정에 따라 티어링 방지 여부가 결정됩니다.
    m_deviceResources->Present();
}
// 백버퍼를 지우는 헬퍼 메서드입니다.
void Game::Clear()
{
    // 뷰(렌더 타겟, 깊이 스텐실)를 가져옵니다.
    auto context = m_deviceResources->GetD3DDeviceContext();
    auto renderTarget = m_deviceResources->GetRenderTargetView();
    auto depthStencil = m_deviceResources->GetDepthStencilView();

    // 렌더 타겟(화면)을 'CornflowerBlue' 색상으로 채웁니다.
    context->ClearRenderTargetView(renderTarget, Colors::CornflowerBlue);

    // 깊이/스텐실 버퍼를 초기화합니다.
    context->ClearDepthStencilView(depthStencil, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

    // 그릴 대상을 백버퍼로 설정합니다.
    context->OMSetRenderTargets(1, &renderTarget, depthStencil);

    // 뷰포트(그릴 영역)를 설정합니다.
    const auto viewport = m_deviceResources->GetScreenViewport();
    context->RSSetViewports(1, &viewport);
}
  • 깊이 버퍼 (Depth Buffer / Z-Buffer): 각 픽셀이 카메라로부터 얼마나 떨어져 있는지(깊이)를 기록하여, 더 멀리 있는 물체가 앞에 있는 물체에 가려져 보이지 않게(은면 제거) 하는 메모리입니다.
  • 스텐실 버퍼 (Stencil Buffer): 픽셀마다 특정 값을 매겨 마치 '마스킹 테이프'나 '판화의 구멍'처럼 원하는 특정 영역에만 그림이 그려지거나 그려지지 않도록 제한하는 메모리입니다.

4. 핵심 분석: 왜 Back Buffer에 직접 그리지 않는가? (RTV)

코드를 분석하며 한 가지 의문이 들었습니다.
"왜 Back Buffer 텍스처에 직접 그리지 않고, Render Target View (RTV)라는 것을 거쳐서 그릴까?"


이것은 DirectX 11의 핵심 철학인 리소스(Resource)와 뷰(View)의 분리 때문입니다.

graph BT
    %% 스타일 정의
    classDef resource fill:#333,stroke:#fff,stroke-width:4px,color:#fff;
    classDef rtv fill:#9e2a2b,stroke:#e09f3e,stroke-width:2px,color:#fff,stroke-dasharray: 5 5;
    classDef srv fill:#335c67,stroke:#e09f3e,stroke-width:2px,color:#fff,stroke-dasharray: 5 5;

    %% 메인 노드 (리소스)
    Texture(("Texture2D<br/>Raw Memory Object")):::resource

    %% 뷰 노드
    subgraph Views ["View Layer (Lens)"]
        direction LR
        RTV["RenderTarget View<br/>(RTV)"]:::rtv
        SRV["Shader Resource View<br/>(SRV)"]:::srv
    end

    %% 연결선 (해석 방향)
    RTV -->|"1. Interpret as Canvas<br/>(Write Output)"|Texture
    SRV -->|"2. Interpret as Image<br/>(Read Input)"|Texture

    %% 설명 주석 (그래프 밖)
    %% Note: 실제 Mermaid 렌더링 시에는 내부에 표시됩니다.
  • 리소스(Back Buffer): 텍스처는 단순히 메모리에 있는 데이터 덩어리일 뿐입니다. 이것이 그림용인지, 데이터 저장용인지 GPU는 알 수 없습니다.
  • 뷰(RTV): 우리는 RTV를 생성함으로써 GPU에게 "이 메모리 덩어리를 렌더링 파이프라인의 최종 목적지(도화지)로 사용할 것임"이라고 명확한 용도를 알려줍니다.

따라서 ClearDraw 같은 명령은 항상 데이터 자체가 아닌, 뷰(RTV)를 대상으로 수행됩니다. 이는 동일한 데이터라도 상황에 따라 다른 용도(예: 텍스처로 다시 읽기 등)로 사용할 수 있게 하는 유연성을 제공합니다.


5. 마무리

이번 스터디를 통해 DirectX 11 렌더링의 뼈대가 되는 Device, Context, SwapChain의 역할을 명확히 이해했습니다.

  • DirectXTK는 복잡한 초기화를 도와주지만, 내부적으로는 3대장 객체가 하드웨어를 제어하고 있습니다.
  • 단순해 보이는 Present() 함수 내부에는 티어링을 방지하기 위한 더블 버퍼링과 VSync의 원리가 숨어 있었습니다.
  • 화면을 파란색(CornflowerBlue)으로 지우는 것조차 RTV라는 '뷰'를 통해 이루어짐을 확인했습니다.

현재는 파란색 빈 화면뿐이지만, 이 과정은 화가가 그림을 그리기 위해 캔버스를 짜고 이젤을 세우는 가장 중요한 준비 단계였습니다. 다음 포스팅에서는 이 캔버스 위에 SpriteBatch를 사용하여 2D 이미지를 띄우는 과정을 다루겠습니다.