ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [2] - Message Pump
    Graphics 2021. 7. 3. 00:35

    Event-Driven Programming

    MS-DOS같은 어플리케이션과 달리 Window-based 어플리케이션은 Event-Driven입니다. 가령, Text editor들은 아무런 input이 없으면 가만히 대기하고 입력이 발생하면 그제야 뭔가 행동을 취합니다. 즉, 아무런 행동을 하지 않으면 Sleep상태가 되는데 심지어 cursor가 blink하는 것조차도 Timer Event입니다. 이 Event-Driven Application은 어떤 Input을 위해서 함수를 호출하지 않습니다. 대신에 시스템이 어떤 Input을 넣어줄 때까지 가만히 대기합니다. 그러면 시스템은 어떤 Window에게 입력이 발생했다는 사실을 어떻게 알려주느냐, 바로 각각의 window가 갖고 있는 Window Procedure의 함수들을 호출하여 알려줍니다. 즉, 어떤 Application에 존재하는 Window들은 Window Procedure라는 함수를 갖고 있는데, 시스템은 어떤 Input이 발생하면 알맞은 Window의 Window Procedure을 호출하여 Input이 발생했음을 알립니다.

     종종 응답하지 않는 윈도우[Window, not the windows]를 볼 수 있는데 이는 시스템이 가장 상위에 보이는 윈도우가 몇 초간 보내는 메시지에 반응을 하지 않는 경우에 완전히 같은 위치에 Ghost Window을 띄어주는 경우입니다. 어찌되었는 Window이기 때문에 크기를 늘리거나 줄이는 등 기본적인 행동은 할 수 있지만 다른 입력에 대해서는 반응을 하지 않을 것입니다. Debugging Mode에서는 시스템이 Ghost Window을 만들지 않습니다. 

    게임은 Process Input -> Update game -> Render -> Process Input -> ... ... ... 과정이 무한히 반복합니다. 각각의 요소들이 하나의 이벤트입니다. Event에 대해 간략하게 설명하면 기본적으로 d3d는 Win32와 App side로 나눌 수 있습니다. Win32쪽에서 I/O Device를 비롯한 입력이 발생하면 이것을 처리할 수 있는 미리 준비된 Queue로 들어가고 App은 해당 Queue에 접근하여 명령어를 추출해옵니다.( GetMessage() 등등)

    추출한 Message는 나름대로 정의한 Filter나 Process를 거쳐서 Translate, Dispatch됩니다. 특히, Translate Message는 추출한 Message를 처리하고 추가적인 Message를 Queue에 전달하는데 이는 더 나중에 기회가 되면 자세히 공부합니다. Dispatch 역시 Message를 처리하고 다시 Win32쪽으로 handle을 내보내는데 Translate message와 함께 다시 다룹니다.

    Messages and Message Queues

     MSDN의 문서를 읽어보면 여기서 정의하는 Messages라는 것은 시스템이 Window Procedure을 통해서 Window에 입력들을 보내줄 때 갖추는 일종의 형식이라고 합니다. 즉, 시스템은 각종 입력을 Messages라는 형태로 Window Procedure를 통해서 Window에 보낸다는 것입니다. 이 Messages는 시스템과 Application 양측에서 생성이 가능합니다. 시스템은 사용자가 마우스를 조금 움직이거나 키보드를 입력, 클릭 등등 동작마다 메시지를 생성합니다. 반대로 Application 측에서 Window의 크기가 변하거나 폰트가 바뀌는 등의 이벤트을 발생시키면 시스템은 Messages을 생성합니다. 

     이 Messages는 크게 네 가지 요소로 이루어져 있습니다. Window Handle, Message Identifier, message parameter1, meesage parameter2. Window handle은 메시지가 보내질 window을 식별하는데 시스템은 Window Handle을 통해서 어떤 Window Procedure이 Messages을 받아야 할지 결정합니다.

    Message Identifier는 메시지의 목적이 무엇인지 상수로 정의가 되어 있습니다. 시스템이 어떤 입력을 받아서 Message로 가공을 하여, Window Handle을 통해서 어떤 Window으로 Message을 송신할 지 결정을 해서 Window Procedure에게 성공적으로 보내졌다면 Window Procedure는 이 Message을 Message Identifier을 통해서 어떻게 처리할지 결정합니다. 예를 들어서, Window Message에 "WM_PAINT"가 있었다면 Window Procedure은 소속된 Window을 어떤 식으로든 칠할 것입니다. 

    Message parameter는 여러 의미가 있습니다. 종합해 보면 Window Procedure가 Window Identifier을 통해서 명령을 수행할 때, 필요한 정보나 정보가 담긴 위치를 참조하기 위해 사용됩니다. 아무런 정보가 필요 없다면 일반적으로 이 부분은 NULL로 채워집니다. 아마 후에 WPARAM, LPARAM의 형태로 나타날 것입니다.

    Window Messages는 크게 두 가지 갈래로 나뉘는데 System-defined, Application-defined가 있습니다. system defined messages는 시스템이 어플리케이션과 소통할 때 발송하며 반대로 어플리케이션이 이미 만들어진 Window에 대한 명령을 통제할 때 사용하기도 합니다. Application defined messages는 어플리케이션이 다른 어플리케이션과 소통하거나 할 때 생성합니다. 

    Messages Rounting

    시스템은 Window Procedure에게 Messages을 보내기 위해서 크게 두 가지의 방법을 사용합니다. 하나는 직접 Window Procedure에게 전송하는 방법이며 다른 하나는 Message Queue라고 불리는 일시적으로 메시지를 저장하는 System-Defined Memory 공간인 FIFO Queue에 저장하는 방법입니다. 이렇게 Queue에 보내진 메시지는 이제 Queued Messages라고 부릅니다. 

    이 Queued messages엔 대표적으로 사용자들이 마우스나 키보드 등의 장치를 통해 들어온 입력들입니다. 어두[語頭]에 WM이라는 것이 붙으면 Window Message을 지칭하는데 마우스, 키보드는 각각 WM_MOUSEMOVE나 WM_LBUTTONDOMN 등의 Constant Message 형식을 갖습니다. 그럼 직접 Window Procedure로 보내진 메시지는 자연스럽게 Unqueued messages가 되겠죠?

    Queued Messages

    시스템은 하나의 Message Queue와 각각의 GUI Thread에 대해서 하나의 thread-specific message queue을 갖고 있습니다. GUI thread가 아닌 thread을 위한 message queue을 만드는 Overhead을 방지하기 위해서 모든 Thread는 Message Queue을 갖지 않은 상태에서 초기화[Initialise]됩니다. 그리고 Thread에서 GUI 함수를 호출하면 그제야 Message Queue을 생성한다고 보면 될 것 같습니다. 참고로 System Message Queue와 Thread-Specific Message Queue을 혼동하면 안 됩니다.  

    메시지가 생성되고 전송되어 처리되는 과정을 조금 알아봤습니다. 공개된 MSDN의 문서에는 사용자에 의해 발생한 입력이 있으면 Device Driver가 입력을 메시지로 변환을 하고 이 메시지를 System Message Queue에 저장합니다. 이렇게 저장된 메시지를 한 번에 하나씩 시스템이 제거[Pop]하고 메시지의 목적지를 확인, 만들어져 있는 Window의 Thread의 Message Queue에 전달합니다. ( 원문에는 Post라는 표현을 씁니다. ) 그러면 각 Thread는 Message Queue의 메시지를 하나씩 제거하면서 Message가 Window에 전달되도록 시스템을 유도합니다. 

    Queue이기 때문에 시스템은 당연히 Message Queue의 마지막으로 들어온 메시지를 가장 나중에 처리하지만 WM_PAINT, WM_QUIT, WM_TIMER는 예외라고 합니다.  예외되는 메시지들은 Queue에 어떠한 메시지도 남아있지 않을 때 비로소 Window에 전달된다고 합니다. (About Messages and Message Queues - Win32 apps | Microsoft Docs)

    figure 1a [From MSDN]

    재미있는 건, WM_PAINT는 같은 메시지가 중복되어 Queue에 존재한다면 하나로 합쳐진다는 점입니다. 이를 통해서 같은 영역을 반복해서 같은 색으로 칠하는 비효율을 피할 수 있습니다.

    * 중요 * 시스템은 메시지를 MSG Structure의 형태로 만들어서 각각의 Thread-Specific Message Queue에 보냅니다. 위에서 Message가 Message Handle, Message Identifier 그리고 두 개의 Parameter로 이루어져 있다고 했는데 MSG Structure는 여기에 추가로 메시지가 보내진 시각, 마우스의 위치를 포함합니다. 참고로 각각의 Thread는 Post Message, PostThreadMessage등을 통해서 자신의 message queue뿐만 아니라 다른 Thread의 Message Queue에도 Message을 보낼 수 있습니다. GetMessage라는 함수를 통해서 어플리케이션 측에서는 이런 과정을 거쳐 전달받은 메시지를 Queue에서 빼서 사용할 수 있습니다. 

    Queue에서 제거하지 않으면서 메시지를 검증할 수 있는 방법이 하나 있는데, 바로 PeekMessage함수를 사용하는 것입니다. 이 함수를 호출하면 MSG 구조체 그대로 Queue에서 복사해 올 수 있습니다. 원문에는 this function fills MSG with information about the message.로 되어 있는데 아마, Queue에서 제거하지 않는 방법으로 소개가 되었으니 시스템이 Window에 보내서 Queue에 저장된 MSG 구조체를 고스란히 복사해오는 것으로 이해하는 편이 나을 것 같습니다.

     Message를 Queue에서 제거한 후엔 어플리케이션 측에서 DispatchMessage라는 함수를 통해서 MSG에 담긴 명령어를 처리하기 위해 적절한 Window Procedure로 보낼 수 있게 시스템을 유도합니다. 이 함수는 직전에 GetMessage나 PeekMessage로 만들어진 MSG에 대한 포인터를 받습니다. 이 함수는 Window Handle, Message Identifier, WPARAM, LPARAM을 포함하지만 시간이나 마우스 포인터의 위치 정보는 포함하지 않습니다. 시스템이 4가지 요소에 마우스 포인터의 위치, 메시지 생성 시간까지 포함하여 각 Thread의 Message Queue에 보내는 것과 차이가 있습니다. 그럼에도 불구하고 시간 또는 마우스 포인터의 정보가 필요하다면 GetMessageTimeGetMessagePos 함수를 이용할 수 있습니다.

    Message handling

    어플리케이션은 반드시 자신의 Thread의 Message Queue에 있는 메시지를 지우고 나서 처리해야 합니다. Single-Threaded 어플리케이션은 WinMain에 message loop을 통해서 메시지가 적합한 Window Procedure로 보내질 수 있도록 설계합니다. Mutiple threads는 각 thread마다 message loop을 갖고 있습니다. 이렇게 놓고 보면 마치 Thread 하나 당 하나의 message loop만 허용되는 것 같습니다.

    ** 그런데 Window Procedure가 함수라고 MSDN에서 설명을 했는데 자꾸 Window Procedure로 뭘 보낸다는 표현이 이해가 안 갑니다. ** 이건 Window Procedure에 대해서만 다룬 문서를 참조하고 나서 이해가 될 것입니다. 

    이 Message Loop는 기본적으로 세 가지 함수의 구성으로 이루어져 있습니다. GetMessage, TranslateMessage, DispatchMessage. 그래서 예시를 들면 어떤 어플리케이션 하나가 자신의 Thread을 위한 Message Queue에서 입력이 있는지 없는지 확인하는 작업을 Message Loop로 구현하면 다음과 같습니다. 

    figure 1b (From MSDN)

    GetMessage는 오류가 발생하면 -1을 반환합니다. 그렇지 않으면 성공적으로 메시지를 꺼내온 것이기 때문에 처리를 하면 됩니다. 주의해야 할 것은 WM_QUIT을 만나면 GetMessage는 FALSE을 반환한다는 점입니다. False가 반환이 되면 message loop는 끝이 날 것입니다.

    GetMessage의 argument을 보면 MSG Structure가 있습니다. 이 함수는 Queue에 있는 MSG을 복사해서 갖고 오는 역할을 하기도 합니다. 만약 두 번째 Argument NULL에 특정한 Window Handle을 명시하면 Queue에서 이 Window을 위한 메시지만을 추출해옵니다. 아래 사진을 보면 두 번째 Argument가 HWND임을 확인할 수 있습니다. 또한, 메시지를 필터링[Filtering]을 할 수도 있는데 이것은 논외이기 때문에 문서만 기록해 두겠습니다.

    figure 1c

    figure 1b을 보면 TranslateMessage라는 함수를 볼 수 있는데 만약 어플리케이션이 키보드의 입력으로 Character을 받는다면 반드시 있어야 합니다. 반대로 말하면 Character을 받지 않는 프로그램이라면 없어도 상관이 없다는 뜻입니다. 

    키보드의 입력이 발생하면 시스템은 Virtual Key Message라는 것을 생성하는데 이 메시지는 키보드의 어떤 키가 눌렸는지 알 수 있는 Virtual Keycode의 정보를 갖고 있습니다. 하지만 키보드의 Character value에 대한 정보를 갖고 있지는 않습니다. (Keyboard Input (Keyboard and Mouse Input) - Win32 apps | Microsoft Docs) 이 문서를 자세히 읽어야 합니다.

    figure 1a

    전에는 WndProc이 아니고 DefWindowProc이라는 기본 Procedure이었습니다. 다만 새로 정의해주었을 뿐 큰 차이는 없습니다. 일반적으로 이 Window Procedure은 Win32 Application의 외형을 정의하는데 물론 예외도 있으며 게임의 경우가 그에 해당합니다. 

    Dispatch Message가 호출되면 Win32 API가 Window를 확인해서 어떤 Pointer가 있는지 보고 그 Message를 Window Procedure에게 전달합니다. 그렇게 되면 Window Procedure은 나름의 처리를 하고 Default Window Procedure을 또 호출합니다. 이 Default Window Procedure이 대단히 중요한 이유는 우리가 쓰지 않을 또는 도저히 모두 커버할 수 없는 것들을 기본으로 제공해주기 때문입니다. 

    GetMessage function (winuser.h) - Win32 apps | Microsoft Doc

    MSG는 msg인스턴스입니다. 이를 통해서 모든 message(현재 활성화된 window)를 다루도록 합시다. BOOL은 D3D에 정의된 boolean인데 MSDN Data Type에 가면 자세히 설명되어 있습니다. (Windows Data Types (BaseTsd.h) - Win32 apps | Microsoft Docs)

     

    Windows Data Types (BaseTsd.h) - Win32 apps

    The data types supported by Windows are used to define function return values, function and message parameters, and structure members.

    docs.microsoft.com

    GetMessage는 오류가 나면 -1을, 정상적으로 Message를 받으면 0이 아닌 양수를 반환하기 때문에 해당 IF statement는 적절해보입니다.

    WndProc은 위에서 말했든 Default Window Procedure을 사용하되, 필요한 기능을 사용하기 위해 만들었습니다. 별 것 아니지만 Window를 닫을 때, 해당 값을 반환할 것입니다. 

    'Graphics' 카테고리의 다른 글

    [5] - Window Framework  (0) 2021.07.10
    [4] - Mouse and Char  (0) 2021.07.03
    [3] - Window Messages  (0) 2021.07.03
    [1] - Creating a window  (0) 2021.07.02
    [ Anything ] C++ - 1  (0) 2021.06.21
Designed by Tistory.