본문 바로가기

Library/Computer Graphics

DXGI(DirectX Graphics Infrastructure) Wrapper Implementation

DXGI(DirectX Graphics Infrastructure)는 D3D의 렌더링 기능에 비해 상대적으로 변화가 느린, 하드웨어를 다루는 모듈을 D3D로부터 떼어낸 것이다. 주로 디스플레이 어댑터, 출력기기(outputs : 모니터), 스왑 체인과 같은 부분을 따로 구현한 것인데, 이로 인해 D3D는 좀 더 자유롭게 기능을 확장할 수 있게 되었다. 과거에는 프로그래머들이 렌더링과 상관없는 하드웨어 인프라에 접근하는 부분도 D3D의 버전업에 따라 다시 구현해야 했는데, DXGI의 등장으로 더 이상 이런 시간 소모적인 일을 할 필요가 없어졌다.

그러나, D3D10이나 D3D11을 다루는 대부분의 예제 코드들은 스왑 체인과 D3D 디바이스를 같이 생성하고 있다. 예제 코드이긴 하지만, 실제 코드를 이런 방식으로 구성한다면 D3D와 DXGI가 분리된 이점을 전혀 활용하지 못하게 된다. 예를 들어, DXGI를 잘 래핑해 둔 클래스가 있다면, 이것은 D3D10, D3D10.1, D3D11에서도 코드를 수정하지 않고 D3D를 사용할 수 있다. 즉, DXGI의 버전에 의존적인 인터페이스와 그렇지 않은 인터페이스를 구별하여, DXGI를 사용하는 상위 계층으로부터 완전히 구현을 감출 수 있다면 D3D 버전에 상관없이 편하게 래퍼 인터페이스만을 사용하여 렌더링 코드를 작성할 수 있게 된다.

만약, D3D에서 DXGI의 구현을 완전히 감추고, DXGI의 인터페이스만 사용하고자 한다면 다음과 같은 과정을 거쳐 DXGI 디바이스를 생성해야 한다.

1. 먼저, DXGI 1.0인지, 1.1인지에 따라 그에 맞는 DXGI 팩토리를 생성한다. DXGI 팩토리는 해당 렌더러에서 사용할 때 반드시 유일한 존재여야 한다. 즉, DXGI의 인프라를 요구하는 상위 계층에서 얻는 인터페이스 개체들은 이 팩토리를 사용하여 생성하게 되는데, 이런 자원은 반드시 동일한 DXGI 팩토리에서 생성한 개체여야 한다. 보통 D3D 디바이스와 스왑 체인을 동시에 생성하는 것은 이런 이유 때문이다. 만약 Visual Studio의 디버깅 출력 윈도우에 서로 다른 DXGI 팩토리를 사용하여 자원이 생성되었다는 경고가 나왔다면, 잠재적인 시한 폭탄을 안고 있는 것과 마찬가지이다.

2. 이 팩토리를 사용하여 버전에 맞는 어댑터를 생성한다.

3. DXGI를 사용하는 렌더러에서 D3D 디바이스를 생성한다. 디바이스를 생성하는데 어댑터 정보를 넘겨주어야 하는데, 이 때 D3D10과 D3D11에서 어댑터에 따른 디바이스 타입 정보가 달라진다. D3D10에서는 어댑터 정보와 디바이스 타입 정보가 일치하지 않아도 D3D 디바이스를 생성할 수 있지만, D3D11 디바이스를 생성하는데는 생성하는 어댑터에 따라 디바이스 타입 정보가 제한된다. D3D11에서 이들 정보가 명시적인 조합을 만족하지 못한다면, 디바이스를 생성할 수 없다. 자세한 것은 http://msdn.microsoft.com/en-us/library/ee416031(VS.85).aspx의 Remark를 참조하라. 예를 들어, D3D11 디바이스를 생성하는 과정에서 어댑터를 널로 설정하지 않고 직접 생성한 어댑터를 인자로 넘겨준다면, 반드시 드라이버 타입 정보를 D3D_DRIVER_TYPE_UNKNOWN으로 설정해야 한다.

4. 디바이스를 성공적으로 생성했다면, 스왑 체인을 생성한다. 스왑 체인을 생성하기 위해서는 D3D 디바이스 정보가 필요하다. 위에서도 말했지만, 어댑터, 스왑 체인과 같은 자원들은 동일한 DXGI 팩토리를 사용하여 생성해야 한다.

DXGI를 잘 래핑하는 클래스를 만들기 위해서는 최소한 위의 요구 사항을 만족해야 한다. 이를 구현하는데는 타입 특질을 사용하는 템플릿 프로그래밍 기법과 함수 오버라이딩이 매우 유용할 것이다. 다음과 같은 간단한 구현 예를 들어보자.

먼저, DXGI의 인터페이스들을 살펴보면, DXGI 1.0 버전은 IDXGIFactory와 같은 형태이며, 1.1 버전은 IDXGIFactory1, IDXGIAdapter1과 같은 형태이다. 1.0 버전과 1.1 버전은 팩토리, 어댑터처럼 서로 다른 인터페이스를 가지는 부분도 있고, IDXGISwapChain처럼 동일한 인터페이스를 가지고 있는 부분도 있다. 그리고, DXGI를 사용하는 상위 모듈은 이러한 차이를 신경 쓰지 않고 코드를 작성할 수 있어야 한다. 그렇다면, 가장 먼저 해야 할 일은 DXGI의 인터페이스 중에서 버전에 따라 변하는 인터페이스와 버전과 무관하게 공통적으로 쓰이는 인터페이스를 구별하는 것이다. 편의를 위해서, 팩토리, 어댑터, 스왑 체인만으로 DXGI 래퍼 클래스를 구현한다고 하자. 여기서 팩토리와 어댑터의 인터페이스는 DXGI의 버전에 따라 달라지며, 스왑 체인의 인터페이스는 1.0과 1.1 버전에 상관없이 동일하다.


struct DXGICommon
{
    typedef IDXGISwapChain SwapChain;
    typedef DXGI_SWAP_CHAIN_DESC SwapChainDesc;
};

struct DXGIv1_0 : public DXGICommon
{
    typedef IDXGIFactory Factory;
    typedef IDXGIAdapter Adapter;
};

struct DXGIv1_1 : public DXGICommon
{
    typedef IDXGIFactory1 Factory;
    typedef IDXGIAdapter1 Adapter;
};


이렇게 코드를 구성하면, 새롭게 typedef 된 자료형을 참조하는 다른 개체들은, 실제 인터페이스가 무엇이냐에 상관없이 동일한 코드를 사용할 수 있게 된다. 그러나, 자료형을 새로 정의했다고 해서 끝나는 것이 아니라, 이를 감싼 래퍼는 이 자료형에 맞는 인스턴스를 실제로 생성해서 돌려주어야 한다. 즉, 이 래퍼를 사용하는 상위 모듈을 생각해보자. 상위 모듈은 위의 DXGI 특질(traits)을 필요에 따라 선택할 수 있어야 하고, 선택한 DXGI 자료형에 대해 적절한 인스턴스를 획득할 수 있어야 한다. 무엇이 생각나는가? 템플릿은 바로 여기에 꼭 맞는 해결책이다. 래퍼 클래스를 DXGIWrapper라고 하면,


// DXGI_traits는 DXGIv1_0, 또는 DXGIv1_1이 될 수 있다
template< typename DXGI_traits >
class DXGIWrapper : public DXGI_traits
{
private:
    typedef typename DXGI_traits::Factory Factory;
    typedef typename DXGI_traits::Adapter Adapter;
    typedef typename DXGI_traits::SwapChain SwapChain;
....
};


typename을 사용한 것은, 이것이 타입 의존적인 이름이기 때문이다. 이제, DXGIWrapper에 필요한 것은 이 자료형에 맞는 인스턴스를 생성하는 구현 코드들이다. 어떤 구현 코드를 사용하는 것이 좋을까? 여기서도 템플릿 함수를 사용하면 좋겠지만, 템플릿 멤버 함수는 템플릿 클래스와 달리 복수의 템플릿 인자에 대해 특화할 수 없다는 단점을 갖기 때문에 활용할 여지가 상대적으로 적다. 물론, 여기서는 자료형 하나만 다루기 때문에 템플릿 특화 함수를 사용하더라도 당장 문제는 없지만, 앞으로 새로운 인자가 추가된다면, 이것은 래퍼 클래스 전체를 수술해야 하는 번거로움을 가져오게 된다. 또, 템플릿 멤버 함수를 사용하여 함수를 구성한다고 하더라도, 템플릿 인자에 따라 하나의 함수 내부에서 타입을 확인하고 각각의 구현 코드를 작성해야 하기 때문에, 구현 함수가 컴파일 의존성을 먹어치우는 좋지 않은 코드가 될 확률이 높다.

여기서 올바른 해결책은 바로 함수 오버라이딩이다. 즉, DXGIWrapper는 DXGI의 버전에 따라 적당한 인스턴스를 생성하는 것이 목적이며, typedef 자료형이라고 하더라도 실제로는 DXGI 1.0 인터페이스인지 1.1 인터페이스인지 명확하게 전달해야 한다. 따라서, 메커니즘 상 이런 부분에 약점을 보이는 템플릿 멤버 함수 특화보다, 컴파일러에 의해 완벽하게 매치되는 함수를 찾는 것이 보장되는 함수 오버라이딩이 훨씬 좋은 선택이다. 물론, 약간의 코드 중복이 발생하기는 하지만, 이것은 라이브러리를 작성하는 입장에서, 클라이언트의 수고를 그만큼 덜어준다는 점을 생각하면 충분히 감내할 수 있는 부분이다.


template< typename DXGI_traits >
class DXGIWrapper : public DXGI_traits
{
private:
    typedef typename DXGI_traits::Factory Factory;
    typedef typename DXGI_traits::Adapter Adapter;
    typedef typename DXGI_traits::SwapChain SwapChain;

public:
    BOOL CreateFactory(IDXGIFactory **factory);
    BOOL CreateFactory(IDXGIFactory1 **factory);
    BOOL CreateAdapter(IDXGIAdapter **adapter);
    BOOL CreateAdapter(IDXGIAdapter1 **adapter);
};

template< typename DXGI_traits >
BOOL DXGIWrapper< DXGI_traits >::CreateFactory(IDXGIFactory **factory)
{
    HRESULT hr = CreateDXGIFactory(__uuidof(IDXGIFactory), (void **)factory);
    if(FAILED(hr))
        return FALSE;

....
    return TRUE;
}

template< typename DXGI_traits >
BOOL DXGIWrapper< DXGI_traits >::CreateFactory(IDXGIFactory1 **factory)
{
    HRESULT hr = CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void **)factory);
    if(FAILED(hr))
        return FALSE;

....
    return TRUE;
}

....


어댑터의 경우도 이와 마찬가지로, DXGI 1.0 인터페이스와 1.1 인터페이스에 따라 오버라이딩을 통해 정의하면 된다. 이제 문제가 되는 것은, 생성된 팩토리, 어댑터, 스왑 체인과 같은 인스턴스를 누가 소유하는냐이다. DXGIWrapper가 소유한다면, DXGIWrapper가 이들 인스턴스가 안전하게 소멸되는 것을 책임져야 한다. 그러나, DXGIWrapper 개체를 이용하는 상위 모듈에서 이들 개체를 소유한다면, 이 인스턴스의 관리 책임은 상위 모듈로 넘어가게 된다. 즉, 다음과 같이 구성하는 것이 가능해진다.


// DXGIWrapper가 팩토리, 어댑터, 스왑 체인의 소유권을 가지는 경우
// 이들 자원을 직접 소유한다면, DXGIWrapper가 어떤 자료형을 가지는지 명확하기 때문에
// 오버라이딩이 필요하지는 않다. 이런 경우, 템플릿 특화를 통해 각각을 정의해야 한다
template< typename DXGI_traits >
class DXGIWrapper : public DXGI_traits
{
private:
    typedef typename DXGI_traits::Factory Facroy;
    typedef typename DXGI_traits::Adapter Adapter;
    typedef typename DXGI_traits::SwapChain SwapChain;

    Factory *m_pFactory;
    Adapter *m_pAdapter;
    SwapChain *m_pSwapChain;
....

public:
    DXGIWrapper() : m_pFactory(0), m_pAdapter(0), m_pSwapChain(0) {}
    virtual ~DXGIWrapper()
    {
        if(m_pFactory)
        {
            m_pFactory->Release();
            m_pFactory = 0;
        }

        ....
    }

    BOOL CreateFactory()
    {
        HRESULT hr = CreateFactory(__uuidof(ID3DXFactory), ....);
        if(FAILED(hr))
            return FALSE;

....
        return TRUE;
    }

    BOOL CreateAdapter() { .... }
};

template<>
class DXGIWrapper< DXGIv1_1 >
{
....

public:
    BOOL CreateFactory()
    {
        HRESULT hr = CreateFactory(__uuidof(ID3DXFactory1), ....);
        if(FAILED(hr))
            return FALSE;

....
        return TRUE;
    }

    BOOL CreateAdapter() { .... }
};


여기서, DXGIWrapper의 소멸자는 가상으로 정의되어야 한다. DXGIWrapper를 사용하고자 하는 상위 D3D 클래스는 DXGIWrapper를 상속해야 하기 때문에, DXGIWrapper의 내부 정리 작업이 완벽하게 이루어지는 것을 보장하려면 DXGIWrapper의 소멸자는 가상으로 정의되어야 한다.

DXGIWrapper가 팩토리나 어댑터와 같은 인스턴스를 소유한다면, 이 인스턴스를 요청하는 인터페이스는 다음과 같은 형식이 되며, 이 인터페이스를 통해 DXGIWrapper가 관리하는 인스턴스에 접근할 수 있게 된다.


Factory GetFactory() { return m_pFactory; }
Adapter GetAdapter() { return m_pAdapter; }


위 함수들의 리턴값이 DXGIWrapper에게 넘겨주는 템플릿 인자에 의존적인 타입이라는 것을 주의하라. 팩토리나 어댑터가 존재하지 않을 때, 널을 리턴하는 경우를 대비해야 할 것이다.


이 방법 외에, DXGIWrapper를 사용하는 상위 모듈에서 팩토리와 어댑터, 스왑 체인을 소유하는 방법도 있다. 이 경우에는, DXGIWrapper의 Create... 종류의 함수가 위에서처럼 외부 포인터의 주소 자체를 인자로 얻어서 인스턴스를 생성하며, DXGIWrapper는 DXGI의 인스턴스를 생성하는 도우미 함수들만 제공하는 것으로 역할이 줄어들게 된다.

이제 실제로 어떻게 이들이 사용되는지 살펴보자. 만약, D3D10 디바이스를 생성하고 싶을 때, 먼저 DXGI의 팩토리와 어댑터를 생성하고, D3D 디바이스를 생성한 뒤, 최종적으로 스왑 체인을 생성하게 된다. 이 렌더러를 이용하는 클라이언트가 필요한 것은 대부분의 경우 D3D 디바이스에 대한 포인터이다. 따라서, DXGIWrapper를 이용하는 렌더러는 자신의 디바이스 포인터를 외부로 노출하는 인터페이스를 가져야 한다. Device()라는 함수가 그 역할을 수행한다고 하면, 클라이언트는 Device()->DrawPrimitive(...)와 같은 형식으로 이 렌더러를 사용할 수 있게 된다.


// DXGI 팩토리, 어댑터 따위를 DXGI 상위 모듈(여기서는 D3D10)에서 소유하는 경우
// D3D10을 사용하는 경우
template< typename DXGI_traits >
class D3D10 : public DXGIWrapper< DXGI_traits >
{
private:
    typename DXGI_traits::Factory m_pFactory;
    typename DXGI_traits::Adapter m_pAdapter;
    typename DXGI_traits::SwapChain m_pSwapChain;

    D3D10Device *m_pDev;

public:
    BOOL InitRenderSystem(HWND hwnd)
    {
        // m_pFactory는 IDXGIFactory이라면, 그에 맞는 DXGIWrapper의 오버라이딩 함수가 호출된다
        DXGI_traits::CreateFactory(&m_pFactory);

        // DXGIWrapper 외부에 팩토리가 존재한다면, 어댑터를 생성할 팩토리가 필요하다
        DXGI_traits::CreateAdapter(m_pFactory, m_pAdapter);

        // D3D10CreateDevice를 호출할 때, DXGIWrapper::GetAdapter()를 호출하면
        // D3D10CreateDevice()가 요구하는 적절한 어댑터를 얻을 수 있다
        D3D10CreateDevice(...., &m_pDev, GetAdapter(), ....);
        ....

        CreateSwapChain(&m_pSwapChain);
        ....
        return TRUE;
    }

    ID3D10Device *Device() const { return m_pDev; }
};

// D3D10.1을 사용하는 경우
template< typename DXGI_traits >
class D3Dv10_1 : public DXGIWrapper< DXGI_traits >
{
private:
    // D3D10.1도 DXGI 1.0을 사용하여 초기화할 수 있다
    typename DXGI_traits::Factory m_pFactory;
    typename DXGI_traits::Adapter m_pAdapter;
    typename DXGI_traits::SwapChain m_pSwapChain;

    D3D10Device1 *m_pDev;

public:
    // 클라이언트는 D3D10을 생성할 때와 마찬가지로 InitRenderSystem()만
    // 호출하면 된다. 구현자는 D3D의 버전에 따라 InitRenderSystem() 내부 구현만
    // 다르게 제공하면 되고, 이 과정조차 DXGIWrapper의 도움으로 어렵지 않게
    // 구현할 수 있다
    BOOL InitRenderSystem(HWND hwnd)
    {
        // D3Dv10_1의 m_pFactory는 IDXGIFactory1이 될 수 있으며,
        // 이에 맞는 적절한 DXGIWrapper의 오버라이딩 함수가 호출된다
        DXGI_traits::CreateFactory(&m_pFactory);

        // 어댑터의 경우도 인자에 맞는 DXGIWrapper가 호출된다
        DXGI_traits::CreateAdapter(m_pFactory, m_pAdapter);

        D3D10CreateDevice(...., &m_pFactory, GetAdapter(), ....);
        ....

        CreateSwapChain(&m_pSwapChain);
        ....
        return TRUE;
    }

    ID3D10Device1 *Device() const { return m_pDev; }
};


위의 코드처럼, D3D 디바이스를 설정하는 과정 자체는 동일한 DXGIWrapper를 사용한다. 따라서, InitRenderSystem()이라는 함수만 각 렌더러의 암시적 인터페이스로 요구하고, 디바이스를 생성하고 셋업하는 과정을 각각 따로 구현하지 않고 동일한 인프라를 사용하여 구현할 수 있다. 이런 방식으로 코드를 구성한다면, 클라이언트는 실제 디바이스를 어떻게 셋업하는지 알 필요없이 InitRenderSystem()만 호출하여 디바이스를 생성하고, 렌더링 코드만 작성하는 것을 생각하면 된다.