본문 바로가기

개발/Rust

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

이전 편에서는 Rust에서 윈도우 창을 띄우기 위한 간단한 프로젝트를 살펴보았습니다. 

 

2021.06.07 - [개발/Rust] - Rust에서 DirectX12 개발하기(1) - 윈도우 창 생성하기

 

Rust에서 DirectX12 개발하기(1) - 윈도우 창 생성하기

이전 편에서는 Rust에서 DirectX12 개발을 하기 위한 기본적인 프로젝트 설정에 대해서 알아보았습니다. 2021.06.06 - [개발/Rust] - Rust에서 DirectX12 개발하기(0) - 개발환경 준비하기 Rust에서 DirectX12 개..

honey-balm.tistory.com

 

이번 편에서는 DirectX를 이용하기에 앞서서, 여러 리소스와 장치를 초기화하는 과정을 알아보겠습니다. 

 

 


 

DirectX12를 사용하기 앞서서, 해당 컴퓨터에 DirectX12를 제대로 지원하는 하드웨어(GPU)가 있는지 확인해야 합니다. 물론 지원되는 하드웨어가 없는 경우에는 WARP 디바이스[각주:1]를 이용해서 소프트웨어적으로 처리하는 것도 가능합니다. 하지만 이 강의에서는 모든 하드웨어가 DirectX12를 지원한다고 가정을 하고 진행하겠습니다. 

 

기본적으로 DirectX12의 D3D12 는 컴퓨터 화면에 간단한 도형에서부터, 복잡한 게임 화면까지 그릴 수 있는 기능을 제공하는 3D 그래픽스 라이브러리입니다. 그리고 현대의 많은 컴퓨터들은 이런 복잡한 작업을 전용 하드웨어(GPU)의 도움을 받아서 빠르게 처리합니다. 그렇기 때문에 DirectX12를 제대로 사용하기 위해서는 현재 컴퓨터에 연결되어있는 GPU와 같은 전용 하드웨어에 대한 정보를 얻을 수 있어야합니다. 이런 하드웨어에 대한 정보를 쉽게 다룰 수 있는 라이브러리가 바로 DXGI[각주:2]입니다. 물론 DXGI에는 하드웨어의 정보를 얻는 것 외에도 스왑 체인 구성, 전체 화면 전환 관리 등 그래픽스 라이브러리에 독립적으로 사용할 수 있는 API를 제공하고 있습니다. 

 

우선은 D3D12 라이브러리에서 GPU에 명령을 전달하기 위해서는 GPU를 대표하는 디바이스 인터페이스가 있어야합니다. 이것을 담당하는 인터페이스가 ID3D12Device[각주:3] 인터페이스입니다. ID3D12Device는 생성할 때 어떤 GPU를 사용할지에 대한 정보가 있어야하는데, 이것을 담당하는 인터페이스는 IDXGIAdapter[각주:4] 입니다. 위 과정에 대한 간략한 그림은 아래와 같습니다. 

 

그림 1. GPU - Adapter - Device

 

사용할 GPU 중 하나를 택해서 IDXGIAdapter를 생성하고, 생성된 IDXGIAdapter를 매개변수로 ID3D12Device를 생성하면 됩니다. 이렇게 여러 IDXGIAdapter를 나열할 때 사용되는 인터페이스가 IDXGIFactory[각주:5] 인터페이스입니다. IDXGIFactory를 추가한 과정은 아래 그림과 같습니다.

 

그림 2. GPU - Factory - Adapter - Device

 

즉, IDXGIFactory를 통해서 여러 GPU를 Adapter로 변환해서 나열해주고, 그 중 하나를 선택해서 Device를 생성할 때 넘겨주면 해당 GPU와 연결된 Device를 얻을 수 있게 됩니다. 각 과정을 함수로 만들면 아래와 같습니다.

 

// factory를 생성한다
fn create_factory(&self) -> Result<IDXGIFactory> {
    todo!()
}

// DirectX12를 지원하는 처음 adapter를 반환한다.
fn create_adapter(&self, factory: &IDXGIFactory) -> Result<IDXGIAdapter> {
    todo!()
}

// device를 생성한다.
fn create_device(&self, adapter: &IDXGIAdapter) -> Result<ID3D12Device> {
    todo!()
}

 

 


 

 

IDXGIFactory를 생성하는 create_factory 함수를 먼저 살펴보겠습니다. IDXGIFactory 인터페이스는 CreateDXGIFactoty[각주:6] 함수를 통해서 생성할 수 있습니다. DXGI 라이브러리에 정의된 구조체들은 IDXGIFactory, IDXGIFactory1, IDXGIAdapter2 등과 같이 여러 버전이 있습니다. IDXGIFactory1은 IDXGIFactory를 상속하고, 새로운 기능이 추가된 인터페이스입니다. 자신의 용도에 맞게 생성하면 됩니다.

 

create_factory 함수의 전체 코드는 아래와 같습니다.

 

fn create_factory(&self) -> Result<IDXGIFactory> {
    let factory = unsafe { CreateDXGIFactory() }?;

    Ok(factory)
}

 

CreateDXGIFactory() 함수를 호출하는 것만으로 간단하게, IDXGIFactory 인터페이스를 생성할 수 있습니다.

 

 


 

 

다음으로는 사용할 어댑터를 반환하는 create_adapter() 함수를 알아보겠습니다. 실제로는 adapter를 새로 생성하는 것이 아닌 열거된 어댑터 중 하나를 선택해서 반환하는 것이지만, 함수 이름의 일관성을 위해서 create를 붙였습니다. 

 

기본적인 로직은 루프를 돌면서 현재 컴퓨터에 연결된 어댑터를 하나씩 확인합니다. 만약 검사하는 어댑터가 기능 수준[각주:7]D3D_FEATURE_LEVEL_12_1을 지원하지 않으면, 다음 어댑터를 검사합니다. 기능 수준을 지원하는 어댑터를 발견하면 바로 반환합니다. 기능 수준은 해당 어댑터가 어떤 버전의 DirectX까지 지원할 수 있는지 알려줍니다.  해당 코드는 아래와 같습니다.

 

// DirectX12를 지원하는 처음 adapter를 반환한다.
fn create_adapter(&self, factory: &IDXGIFactory) -> Result<IDXGIAdapter> {
    // 루프를 돌면서 D3D_FEATURE_LEVEL_12_1을 지원하는
    // 어댑터가 있는지 확인한다.
    for i in 0.. {
        let mut adapter = None;
        let adapter = unsafe { factory.EnumAdapters(i, &mut adapter) }
            .and_some(adapter)
            .unwrap();

        // windows 크레이트에서 제공하는 D3D12CreateDevice 함수는
        // adapter의 지원여부만을 확인하는 방법을 제공하지 않기 때문에
        // d3d12.lib에서 직접 D3D12CreateDevice 함수를 가져온다.
        #[link(name = "d3d12")]
        #[allow(dead_code)]
        extern "system" {
            pub fn D3D12CreateDevice(
                pdapter: windows::RawPtr,
                minimutfeaturelevel: D3D_FEATURE_LEVEL,
                riid: *const windows::Guid,
                ppdevice: *mut *mut std::ffi::c_void,
            ) -> windows::HRESULT;
        }

        if unsafe {
            D3D12CreateDevice(
                adapter.abi(),
                D3D_FEATURE_LEVEL_12_1,
                &ID3D12Device::IID,
                std::ptr::null_mut(),
            )
        }
        .is_ok()
        {
            return Ok(adapter);
        }
    }

    unreachable!()
}

 

 

create_adapter 함수 내부에 D3D12CreateDevice 함수를 선언하는 부분이 있습니다. DirectX12의 D3D12CreateDevice[각주:8]는 ppdevice 매개변수를 null로 설정해서, 디바이스를 생성하지 않고 해당 어댑터가 기능 수준을 지원하는지만 확인할 수 있습니다. 하지만 windows 크레이트에 있는 D3D12CreateDevice[각주:9] 함수는 해당 기능을 지원하지 않고, 디바이스를 생성합니다.

 

모든 어댑터마다 디바이스를 생성하는 과정은 불필요하기 때문에, #[link(name = "d3d12)] 매크로를 통해 d3d12.lib에 정의된 D3D12CreateDevice 함수를 가져옵니다. 이 함수는 stdcall 함수 호출 규칙을 따라야하기 때문에 함수의 앞에 extern "system"을 붙여줍니다. 

 

함수를 선언한 후에는 D3D12CreateDevice 함수를 호출해서 해당 어댑터가 기능 수준을 만족하는지 확인하고 만족을 한다면, 어댑터를 반환합니다. 함수의 첫번째 매개변수로 adapter.abi()가 사용되는데, 이는 adapter 인터페이스의 포인터 주소라고 생각하시면 됩니다. 

 

 


 

 

마지막으로 디바이스를 생성하는 함수를 알아보겠습니다. create_adapter 함수를 통해서 사용할 어댑터를 얻은 다음에, 해당 어댑터를 D3D12CreateDevice 함수의 매개변수로 전달하면, 해당 어댑터를 사용하는 디바이스를 생성할 수 있습니다. 코드는 아래와 같습니다.

 

fn create_device(&self, adapter: &IDXGIAdapter) -> Result<ID3D12Device> {
    let device: ID3D12Device = unsafe { D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_12_1) }?;

    Ok(device)
}

 

 


 

 

해당 create 함수를 호출하는 코드는 아래와 같이 변경되었습니다.

 

// ....
let hwnd = ret.create_window()?;
let factory = ret.create_factory()?;
let adapter = ret.create_adapter(&factory)?;
let device = ret.create_device(&adapter)?;
// ....

 

그리고 생성한 BareBoneGame 구조체를 반환하기 전에 Resources를 초기화해주면 됩니다. 코드는 아래와 같습니다.

 

    // ....
    ret.resources = Some(Resources {
            hwnd,
            factory,
            adapter,
            device,
     });

    Ok(ret)
}

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

 

seonhwi07jp/rust-d3d12 (github.com)

 

seonhwi07jp/rust-d3d12

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

github.com

 

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