본문 바로가기

개발/Rust

Rust에서 DirectX12 개발하기(3) - DirectX 초기화하기(CommandQueue, CommandAllocator, SwapChain 초기화)

이전 편에서는 Rust에서 DirectX12를 사용하기 위해서, 디바이스를 초기화하는 방법을 살펴보았습니다. 

 

2021.06.07 - [개발/Rust] - Rust에서 DirectX12 개발하기(2) - DirectX 초기화하기(디바이스 초기화)

 

Rust에서 DirectX12 개발하기(2) - DirectX 초기화하기(디바이스 초기화)

이전 편에서는 Rust에서 윈도우 창을 띄우기 위한 간단한 프로젝트를 살펴보았습니다. 2021.06.07 - [개발/Rust] - Rust에서 DirectX12 개발하기(1) - 윈도우 창 생성하기 Rust에서 DirectX12 개발하기(1) - 윈도..

honey-balm.tistory.com

 

이번 편에서는, 초기화한 디바이스를 통해서 명령을 전달하기 위한 CommandAllocator와 CommandQueue 그리고 티어링을 방지하기 위한 SwapChain을 구성하는 방법에 대해서 알아보겠습니다

 

 


 

 

DirectX12는 이전 버전의 API와 다르게 멀티-스레드 환경을 고려해서 설계된 API입니다. DirectX11의 경우는 Deferred Context를 통해서 멀티-스레드를 지원했지만, 기본적으로 Immediate Context을 통해서 함수를 호출할 때 명령을 바로 명령 큐에 담는 구조였습니다[각주:1]

 

하지만 DirectX12부터는 멀티-스레드를 기본 지원하기 때문에 기존의 Immediate Context 방식의 API는 사라졌습니다. 그 대신, Command List를 통해서 Command(명령)를 호출하면, Command List는 호출한 Command들을 보관하고 있다가 특정 시점에 Command Queue로 보관하고 있던 명령들을 전송합니다. 이렇게 Command Queue에 명령들이 쌓이게 되면, GPU는 자신이 처리할 수 있는 시점에, 명령들을 하나씩 처리하게 됩니다[각주:2]. 위 과정을 그림으로 표현하면 아래와 같습니다.

 

그림 1. Allocator - List - Queue

 

위 그림은 Command Allocator와 Command List, Command Queue의 관계를 그림으로 표현한 것입니다. 물론, 실제로는 이것보다 복잡하고 다른 부분이 많지만 그런 부분은 차치하겠습니다. 

 

우선 Command Queue입니다. Command Queue는 GPU가 실행해야하는 명령을 담고 있는 하나의 큐입니다. Command Queue는 Command List가 실행하고자 한 명령들은 자신의 큐에 담고 있습니다. 이런 큐들은 큐에 들어오는 즉시 실행되는 것이 아니라 먼저 큐에 들어온 순서대로 명령들을 GPU가 처리합니다. 즉, GPU가 처리해야하는 명령들을 보관하는 장소라고 생각하시면 됩니다. 

 

다음은 Command List와 Command Allocator입니다. Command Allocator의 경우는 설명에는 없었지만, 그림에는 있습니다. Command List는 자신이 실행하고 싶은 명령들을 가지고 있어야하는데, 이 명령들을 보관하는 장소가 바로 Command Allocator 입니다. 

 

Command List에서 자신이 실행하고 싶은 명령들을 호출하게 되면, 명령들은 Command Allocator에 저장되게 되고, Command List에서 명령을 실행하게 되면(실제로 명령이 실행되는 것이 아니라, 실행될 수 있도록 Command Queue로 보내는 것을 의미합니다), Command Queue가 Command Allocator에 저장돼있는 명령들을 가리키게 됩니다. 

 

이 구조의 장점은 기존의 방식과 다르게 명령이 순차적으로 호출되는 것이 아니라, 개발자가 여러 Command List를 통해서 명령 호출을 여러 쓰레드에 분배하고, 원하는 시점에 명령을 실행할 수 있다는 점입니다. 하지만 모든 멀티쓰레딩 프로그래밍이 그렇듯 메모리 오염(Corruption)와 널 포인터 참조(Null Dereference)에 주의해야합니다. DirectX12에서는 이런 부분을 Fence라는 것을 통해서 해결합니다. 자세한 설명은 나중에 하겠습니다. 

 

 


 

 

그렇다면 이제 CommandQueue를 생성하겠습니다. CommandQueue는 디바이스의 CreateCommandQueue[각주:3] 함수를 통해서 생성할 수 있습니다. 매개변수로 Command Queue의 설명자(Descriptor)를 전달하는 부분이 있는데, 이러한 설명자는 DirectX에서 다양한 리소스를 초기화할 때 해당 리소스에 정보를 전달해주는 용도로 사용되곤 합니다. 여기서는 D3D12_COMMAND_QUEUE_DESC[각주:4]라는 설명자를 이용합니다. 이 설명자에는 Type[각주:5]이라는 필드가 있는데, 이 필드에 CommandQueue의 타입을 지정해주면 됩니다. 이 샘플에서는 Direct 타입을 사용합니다. 코드는 아래와 같습니다. 

 

fn create_command_queue(&self, device: &ID3D12Device) -> Result<ID3D12CommandQueue> {
    let command_queue = unsafe {
        device.CreateCommandQueue(&D3D12_COMMAND_QUEUE_DESC {
            Type: D3D12_COMMAND_LIST_TYPE_DIRECT,
            ..Default::default()
        })
    }?;

    Ok(command_queue)
}

 

CommandQueue를 생성했으면, CommandAllocator와 CommandList를 생성해서 명령을 처리하는 파이프라인을 구성할 수 있습니다. 

 

그 다음으로는 CommandAllocator 입니다. CommandAllocator는 CommandList의 명령들을 실제로 저장하고 있는 역활을 합니다. CommandAllocator를 생성하는 코드는 아래와 같습니다.

 

fn create_command_allocator(&self, device: &ID3D12Device) -> Result<ID3D12CommandAllocator> {
    let command_allocator =
        unsafe { device.CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT) }?;

    Ok(command_allocator)
}
    

 

CommandAllocator를 생성하기 위해서는 ID3D12Device::CreateCommandAllocator[각주:6] 함수를 호출하면 됩니다. 이 함수에는 파라미터로 어떤 타입의 커맨드 리스트를 사용할지 전달해줘야 합니다. 위에서 CommandQueue를 생성할 때 사용한 타입과 동일한 D3D12_COMMAND_LIST_TYPE_DIRECT을 전달해줍니다. 

 

 


 

 

다음으로는 CommandList의 생성이 아닌 SwapChain의 생성에 대해서 먼저 알아보겠습니다. SwapChain은 화면에 렌더링한 이미지를 표현할 때 발생하는 티어링(Tearing)[각주:7]을 방지하기 위해서 사용됩니다. 티어링이 발생하는 이유는 모니터가 새롭게 화면을 그리는 빈도와 GPU가 이미지를 생성하는 빈도가 일치하지 않을 때 발생됩니다. 

 

이를 방지하기 위해서 GPU가 이미지를 생성하고 있는 중간에 화면에 이미지를 표현하는 것이 아닌, 완성된 이미지만을 화면에 표시하면 됩니다. 이를 위해서 더블 버퍼링이라는 기술이 사용됩니다. 더블 버퍼링이란 두 개의 버퍼를 사용해서 하나의 버퍼는 완성된 이미지를 저장하고, 다른 하나는 렌더링 중인 이미지를 저장합니다. 버퍼는 화면에 그려질 이미지를 담고 있습니다. 아래의 그림은 하나의 버퍼를 사용하는 경우와 두개의 버퍼를 사용하는 경우의 차이를 그림으로 표현한 것입니다.

그림 2. 버퍼링 방식의 차이

 

 

위의 방식은 화면에 출력할 이미지를 담는 버퍼를 하나만 사용한 경우입니다. 만약 버퍼에 이미지를 전부 다 그리는 시점과 디스플레이에서 버퍼에 담긴 이미지를 화면에 출력하는 시점이 일치하지 않으면, 완성되지 않은 이미지가 화면에 출력되게 되는 것입니다. 

 

아래의 방식은 화면에 출력할 이미지를 담는 버퍼를 두개를 사용한 경우입니다. 디스플레이는 매 출력 시점마다 서로 다른 버퍼에 접근해서 이미지를 출력합니다. 이렇게 되면, 아직 완성되지 않은 이미지를 화면에 출력하는 것이 아니라 완성된 이미지만을 화면에 출력하는 것이 가능해집니다. 이때 사용되는 버퍼가 2개이면 더블 버퍼링, 3개이면 트리플 버퍼링이라고 부릅니다. 

 

이와 같은 여러 버퍼들을 사용해서 렌더링을 구성할 때 사용하는 것이 바로 SwapChain입니다. SwapChain은 D3D12가 아닌 DXGI에 포함된 기능입니다. SwapChain을 생성할 때 CreateSwapChainForHwnd[각주:8] 함수를 이용합니다. 이 함수는 DXGIFactory2에 추가된 함수이기 때문에, resources 구조체에 있는 factory 변수로는 함수를 호출할 수 없습니다. C++에서는 이럴 때 QueryInterface 메소드를 이용해서 관련 인터페이스를 얻을 수 있지만, windows 크레이트에는 QueryInterface 대신 각 인터페이스에 적절한 타입으로의 변환을 해주는 cast 함수가 구현되어 있습니다. 이를 사용해서 factory 변수를 IDXGIFactory2로 변환하는 코드는 아래와 같습니다.

 

let factory: IDXGIFactory2 = factory.cast()?;

 

 

그 다음 SwapChain을 생성하기 위해서는 DXGI_SWAP_CHAIN_DESC1[각주:9]를 채워야합니다. 코드는 아래와 같습니다.

 

let swap_chain_desc = DXGI_SWAP_CHAIN_DESC1 {
    BufferCount: buffer_count,
    Width: self.width,
    Height: self.height,
    Format: DXGI_FORMAT_R8G8B8A8_UNORM,
    BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
    SwapEffect: DXGI_SWAP_EFFECT_FLIP_DISCARD,
    SampleDesc: DXGI_SAMPLE_DESC {
        Count: 1,
        ..Default::default()
    },
    ..Default::default()
};

 

 

BufferCount와 Width, Height 필드는 각각 생성할 버퍼의 수와 버퍼의 폭과 높이를 지정하는 필드입니다. Format의 경우 해당 버퍼갚 어떤 형식으로 데이터를 저장할지 지정하는 필드입니다. 여기서는 RGBA에 각각 8비트로 저장하고 UNORM(값의 범위가 0..1) 형식의 DXGI_FORMAT_R8G8B8A8_UNORM[각주:10]으로 지정해줍니다. BufferUsage에는 해당 버퍼가 어떠한 용도로 사용될지 지정해주는데 이 버퍼는 SwapChain안에 구성되는 버퍼이기 때문에 RenderTargetBuffer로 사용됩니다. 그렇기 때문에 DXGI_USAGE_RENDER_TARGET_OUTPUT으로 설정해주면 됩니다. SwapEffect 필드는 각 버퍼가 전환될 때 어떠한 방식으로 전환될지 정하는 필드입니다. 만약에 더블 버퍼링을 이용한다면 백버퍼와 프론트버퍼가 전환될 때를 의미합니다.  DXGI_SWAP_EFFECT는 아래와 같은 종류가 있습니다.

 

typedef enum DXGI_SWAP_EFFECT {
  DXGI_SWAP_EFFECT_DISCARD,
  DXGI_SWAP_EFFECT_SEQUENTIAL,
  DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL,
  DXGI_SWAP_EFFECT_FLIP_DISCARD
} ;

 

이 중 위 두개는 Blt 모델이고 아래 두개는 FLIP 모델입니다. 두 모델의 차이는 아래의 링크를 참조하시면 됩니다. 간단하게 말하면, 최신 모델인 FLIP 모델을 적용하는 것이 성능적으로 효과적이고 Blt 모델은 특정한 상황(GDI와 동시에 사용해야할 때)에 사용됩니다. 

 

For best performance, use DXGI flip model | DirectX Developer Blog (microsoft.com)

 

For best performance, use DXGI flip model | DirectX Developer Blog

This document picks up where the MSDN “DXGI flip model” article and YouTube DirectX 12: Presentation Modes In Windows 10 and Presentation Enhancements in Windows 10: An Early Look videos left off.  It provides developer guidance on how to maximize per

devblogs.microsoft.com

 

위 코드에서는 DXGI_SWAP_EFFECT_FLIP_DISCARD를 지정했습니다. 마지막으로 SampleDesc는 MSAA를 사용할 때 몇배수의 샘플링을 할지 지정하는 필드입니다. MSAA는 하드웨어에서 지원하는 안티에일리어싱을 방지하기 위해서 생성된 이미지에서 샘플링한 픽셀을 다시 재구성하는 기술입니다. 여기서는 MSAA를 사용하지 않기 때문에 샘플의 Count 필드를 1로 지정하고 나머지는 전부 default 값으로 설정해줍니다. 

 

마지막으로 생성한 IDXGIFactory2::CreateSwapChainDescForHwnd 함수를 호출해서 스왑체인을 생성해주면 됩니다. 코드는 아래와 같습니다.

 

fn create_swap_chain(
    &self,
    buffer_count: u32,
    hwnd: &HWND,
    factory: &IDXGIFactory,
    command_queue: &ID3D12CommandQueue,
) -> Result<(IDXGISwapChain, u32)> {
    let factory: IDXGIFactory2 = factory.cast()?;

    let swap_chain_desc = DXGI_SWAP_CHAIN_DESC1 {
        BufferCount: buffer_count,
        Width: self.width,
        Height: self.height,
        Format: DXGI_FORMAT_R8G8B8A8_UNORM,
        BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT,
        SwapEffect: DXGI_SWAP_EFFECT_FLIP_DISCARD,
        SampleDesc: DXGI_SAMPLE_DESC {
            Count: 1,
            ..Default::default()
        },
        ..Default::default()
    };

    let mut swap_chain = None;
    let swap_chain: IDXGISwapChain3 = unsafe {
        factory.CreateSwapChainForHwnd(
            command_queue,
            hwnd,
            &swap_chain_desc,
            std::ptr::null(),
            None,
            &mut swap_chain,
        )
    }
    .and_some(swap_chain)?
    .cast()?;

    let frame_index = unsafe { swap_chain.GetCurrentBackBufferIndex() };

    Ok((swap_chain.cast()?, frame_index))
}

 

각 생성 함수를 호출하는 코드는 아래와 같습니다.

 

let command_queue = ret.create_command_queue(&device)?;
let command_allocator = ret.create_command_allocator(&device);
let (swap_chain, frame_index) =
    ret.create_swap_chain(2, &hwnd, &factory, &command_queue)?;

 

 


 

 

이번 편에서는 CommandQueue, CommandAllocator, SwapChain에 대한 간략한 개념과 생성하는 방법에 대해서 알아봤습니다. 

 

샘플 프로젝트 코드는 github에 올렸습니다.

 

seonhwi07jp/rust-d3d12 (github.com)

 

seonhwi07jp/rust-d3d12

Contribute to seonhwi07jp/rust-d3d12 development by creating an account on GitHub.

github.com

 

해당 코드는 ver4 브랜치로 이동하면 볼 수 있습니다.