본문 바로가기

Library/Computer Graphics

CS_OWNDC with WGL (OpenGL Extentions for Windows)

OpenGL은 대단히 잘 구성되어 있는 크로스 플랫폼 그래픽 라이브러리이지만, 확장 기능(OpenGL Extentions)과 연관된 플랫폼 이식 작업은 그렇게 쉬운 편은 아니다. OpenGL은 플랫폼 의존적인 부분을 표준으로 포함하고 있지 않으며, 윈도우를 생성하는 작업과 같은 하드웨어 의존적인 구현을 여기에 의존하고 있기 때문이다. OpenGL 코드를 다른 플랫폼으로 이식할 때, 렌더링 코드는 수정할 필요가 거의 없지만, 플랫폼 의존적인 벤더 확장 부분을 수정하는 것은 생각보다 잔손이 많이 가는 작업이다. 그 중에서도, 특히 WGL(OpenGL Extentions for Windows, 위글)은 GDI DC(DeviceContext) 관리에 주의해야 할 부분이 있다.

보통, Win32 윈도우를 생성할 때 DC와 관련된 클래스 스타일은 생략하기 마련이다. 하지만, 이것은 WGL을 사용할 때 중요한 이슈가 된다. 윈도우에서 DC를 얻고 해제하기 위해서는 GetDC, ReleaseDC와 같은 함수를 사용하는데, ReleaseDC는 GetDC로 DC를 얻은, 같은 스레드 컨텍스트 내에서 호출되어야 정확하게 DC를 해제하기 때문이다. 다음을 살펴보자.

....

class DCManager
{
private:
    HWND m_hWnd;
    HDC m_hDC;

public:
    DCManager(HWND hWnd) : m_hWnd(hWnd)
    {
        m_hDC = GetDC(m_hWnd);
    }

    ~DCManager()
    {
        ReleaseDC(m_hWnd, m_hDC);
    }
};

....

void Foo()
{
    ....
    static DCManager static_dc_mgr(hwnd);
    DCManager dc_mgr(hwnd);
    ....
}

....


Foo는 DC를 사용하는 어떤 윈도우 핸들러라고 하자. DCManager는 블럭을 벗어나면서 자동으로 획득한 DC를 자동으로 해제하는 간단한 클래스이다. 위 코드의 ReleaseDC는 원하는대로 정확하게 동작할까?

Foo()의 지역 변수로 선언된 DCManager는 경우에 따라서 정확하게 동작할 수도, 그렇지 않을 수도 있다. 즉, 컴파일러가 DCManager의 생성자, 소멸자 코드를 Foo() 안에 바로 전개한다면 ReleaseDC는 획득한 DC를 제대로 해제해줄 것이다. 그러나, static으로 선언된 DCManager는 그렇지 못하다. GetDC를 사용하여 DC를 획득한 시점과 ReleaseDC를 호출하는 시점이 같은 스레드에서 이루어진다는 보장이 없기 때문에, 원하는대로 동작하지 않는다. 이것은 DC라는 자원이 누수된다는 것을 뜻한다. 왜 이렇게 될까? 이것은, 메모리가 귀하고 상대적으로 윈도우에서 DC가 비싼 자원이었던 시절, GetDC와 ReleaseDC가 서로 다른 스레드에서 실행될 때 제한된 메모리 때문에 문제를 일으킬 수 있었기 때문이다.

이것은, GetDC를 호출하는 부분을 CreateGLWindow()와 같은 부분에 배치하고, ReleaseDC를 호출하는 부분을 DestoryGLWindow()와 같은 부분으로 나누어 배치했을 때, 이들이 제대로 동작하지 않는다는 것을 뜻한다. ReleaseDC가 실패해도 시스템이 죽어버리는 것은 아니고, ReleaseDC는 대부분의 프로그래머가 리턴값을 받아보지 않기 때문에 지나치는 부분이다. MSDN의 OpenGL 포팅 코드를 보면 전통적인 하나의 윈도우 프로시저에서 DC를 획득하고, HGLRC 렌더링 컨텍스트까지 획득 / 삭제하는 모습을 볼 수 있는데, 이것은 이 문제를 피하기 위한 것이다.

이 문제는 Ogre3D의 OpenGL 지원 코드에도 발견된다. 이것은 꼭 GetDC / ReleaseDC 짝에만 적용되는 문제가 아니라 CreateDC, DeleteDC에도 마찬가지로 적용된다. 즉, 이 문제 때문에 DC를 생성하고 삭제하는 작업을 별도의 함수로 분리하는 것은 대단히 힘들다는 결론을 얻게 된다. 같은 스레드 컨텍스트 안에서 이 짝이 맞추어져 호출되어야 한다는 것은, Win32 환경에서 OpenGL을 사용하는 프로그램의 설계에 커다란 제한 사항이다. DC를 할당 받는 부분과 해제하는 부분이 같은 스레드 안에서 동작하는 것을 보장하기 위해서는 어쩔 수 없이 하나의, 커다란 메세지 핸들러를 작성할 수 밖에 없기 때문이다. 그러나, 방법이 없는 것은 아니다.

대부분의 Win32 OpenGL 구현은 윈도우 스타일에 CS_OWNDC를 요구하는데, 이것을 이야기하기 전에, GetDC와 ReleaseDC가 하는 일이 정확하게 무엇인지 알아보자. CS_OWNDC와 같은 DC와 관련된 특별한 클래스 스타일이 주어지지 않는다면, GetDC는 호출한 시점에서 시스템이 유효한 DC를 생성한 뒤 이것을 DC 캐시에 넣어둔다. 그리고, 클라이언트는 이 DC를 참조하게 된다. ReleaseDC가 하는 정확한 행동은, 이름처럼 DC를 삭제하는 것이 아니라, DC 캐시에서 제거하는 것이다. 즉, 클라이언트가 다음과 같은 코드를 실행할 때,


HDC hDC_1 = GetDC(hWnd);
HDC hDC_2 = GetDC(hWnd);


hDC_1과 hDC_2는 서로 다른 DC를 얻게 된다. 그리고, 당연한 말이지만 이렇게 얻어진 DC는 같은 스레드 컨텍스트 안에서 ReleaseDC를 통해 해제되어야 DC를 정확하게 관리할 수 있다. 그러나, CS_OWNDC로 윈도우가 생성되었다면, 놀랍게도 hDC_1과 hDC_2는 서로 같은 DC이다. hDC_1과 hDC_2가 서로 다른 DC라고 생각하고 여기에 GDI 함수를 적용했다면, 그 결과가 하나의 DC에 모두 적용되어 나타날 것이다. CS_OWNDC는 윈도우가 생성되는 시점에 DC를 '이 윈도우를 위한' DC를 생성하며, DC 캐시에서 이 DC가 제거되지 않도록 특별한 표시를 해둔다. 즉, ReleaseDC를 호출한다고 하더라도 이 DC가 캐시에서 제거되는 것이 아니라, 윈도우가 파괴되는 시점에서야 DC가 DC 캐시에서 제거된다. 이로 인한 장점은, 당연히 ReleaseDC와 같은 명시적인 정리 작업이 필요하지 않다는 것이다. 이것은 GetDC / ReleaseDC의 호출 시점에 대한 제약을 벗어나게 해 줄 수 있는 좋은 특징이다. 그러나, 위에서 말했던 '언제나 같은 DC만 얻을 수 있다'는 것은 장점이 될 수도, 단점이 될 수도 있다. 많은 Win32 OpenGL 구현들이 CS_OWNDC 스타일을 요구하는 이유 중 하나는 정확한 DC 관리 때문이다.

물론, CS_OWNDC을 사용하지 않고, ReleaseDC가 제대로 DC를 해제하지 않았다고 하더라도 이런 정리 작업은 보통 프로그램이 종료될 때 일어난다. 윈도우즈는 프로그램이 종료될 때 누수된 자원도 최대한 회수하기 때문에 그렇게 큰 문제라고 볼 수는 없다. 프로그램의 실행 시점에서는 문제가 없기 때문에 대부분의 프로그래머들에게 CS_OWNDC이 요구되는 것은 버그로 간주되고 있지만, CS_OWNDC는 아무런 이유도 없이 요구되는 것이 아니다.

즉, WGL을 사용하여 OpenGL 프로그래밍을 하고자 한다면, CS_OWNDC 스타일을 적용한 윈도우를 생성하는 것이 좋다. MS는 OpenGL에 대해 큰 관심을 기울이지 않고 있기 때문에, DC 관리를 위한 다른 좋은 방법이 없는 이상, 이것이 DC 누수를 피할 수 있는 가장 현실적인 해결책이다.