매크로

UE3 의 native 에는 유용한 매크로들이 존재한다. 이것들에 대해 살펴본다.

랜더 스레드에 커맨드 추가

Game Thread → Render Thread 방향으로 커맨드를 전달하기 위한 매크로.

// RenderingThread.h
#define ENQUEUE_RENDER_COMMAND( TypeName, Params ) \
    { \
        check( IsInGameThread() ); \                     // 이 매크로를 사용하는 측은 당연히 Game Thread 여야 함
        if ( GIsThreadedRendering ) \                    // 랜더링 스레드가 수행중이라면
        { \
            ... [생략] \                                 // 랜더링 스레드에 의해 사용되는 링버퍼에 커맨드를 추가하는 로직
        } \
        else \
        { \
            TypeName TypeName##Command Params; \         // 랜더링 스레드가 수행중이지 않다면 바로 실행
            TypeName##Command.Execute(); \
        } \
    }
  • 요약 : 랜더링 스레드가 있으면 링버퍼에 추가, 없다면 바로 실행
  • 부연 : 매크로 첫번째 파라메터는 커맨드 수행 내용이 정의된 class명 이어야 하며, 두번째 파라메터는 생성자로 넘길 파라메터들의 묶음이다. 이 묶음들은 괄호 () 로 묶여 있어야 한다.

위 매크로의 첫번째 파라메터로 들어가는 class 는 멤버함수로 Execute 가 있어야 한다는 전제가 깔려있는데, 이것은 사용자가 잊어버릴 수도 있는 매우 불친절한 전제이다. 그러므로 Execute 멤버함수를 생성해주고 이놈이 무엇을 수행할 지를 정의해주는 아래 매크로가 또 존재한다.

// RenderingThread.h
#define ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER( TypeName, ParamType1, ParamName1, ParamValue1, Code ) \
    class TypeName : public FRenderCommand \
    { \
    public: \
        typename ParamType1 _ParamType1; \
        TypeName( const _ParamType1& In##ParamName1 ) : \
            ParamName1( In##ParamName1 ) \
        {} \
        virtual UINT Execute() \
        { \
            Code; \
            return sizeof( *this ); \
        } \
        virtual const TCHAR* DescribeCommand() \
        { \
            return TEXT( #TypeName ); \
        } \
    private: \
        ParamType1 ParamName1; \
    }; \
    ENQUEUE_RENDER_COMMAND( TypeName, (ParamValue1) );
  • 요약 : Code 를 수행하는 랜더커맨드를 생성하여 큐에 추가한다. 이 수행에는 1개의 변수가 필요하다. 그 변수의 타입은 ParamType1 이고, Code 상의 인스턴스이름은 ParamName1 이며, 실제 인스턴스는 ParamValue1 이다.
  • 부연 : 당연히 Code 내용 어디엔가 ParamName1 을 사용하는 부분이 있어야 한다.

그럼 이에 대한 예를 살펴본다.

// LaunchEngineLoop.cpp
class FFrameEndSync
{
    ...
    void Sync( UBOOL bAllowOneFrameThreadLag )
    {
        ...
        ENQUEUE_UNIQUE_RENDER_COMMAND_ONEPARAMETER(
            FenceCommand,                       // class 이름
            FEvent*,                            // Code 수행에 필요한 변수의 타입
            EventToTrigger,                     // Code 수행에 필요한 변수 인스턴스 이름, 이것은 Code 어디엔가 사용되어져야 한다.
            Event[EventIndex],                  // 사용되어지는 실제 변수
            {                                   // Code. 여기서 위에 지정한 인스턴스 이름이 사용되는 것을 볼 수 있다.
                EventToTrigger->Trigger();
            }
        );
        ...
    }
    ...
};

위 FFrameEndSync 는 Game Thread 와 Render Thread 간의 동기화를 맞추기 위해 사용되는 class 이다.

CPU Cycle 측정

CPU 의 Cycle 을 측정하는 매크로.

// UnFile.h
#define CLOCK_CYCLES( Timer )    { Timer -= appCycles(); }
#define UNCLOCK_CYCLES( Timer )  { Timer += appCycles(); }

측정 시작 전에 CLOCK_CYCLES 를 호출하고 측정 완료 시점에 UNCLOCK_CYCLES 를 호출하는데 특이한 것은 시작할 때 현재 CPU Cycle 을 뺀다는 것이고 종료시점에 더한다는 것이다.

이것은 Timer 변수가 어떤 타입이 되었건 값의 범위를 넘어서서 생기는 winding 에는 상관하지 않고 최종적으로 변경된 CPU Cycle 차이만을 얻기위한 것이다.

구간 CPU Cycle 측정

특정 구간에서의 CPU 의 소모 Cycle 을 측정하는 매크로. 위 매크로의 작동방식과는 약간의 차이가 있다. 위 CLOCK_CYCLES 방식은 특정 변수에 기록하는 반면 이것은 Stats 에 기록한다.

// UnStats.h
#define SCOPE_CYCLE_COUNTER( Stats ) \
    FScopeCycleCounter CycleCount_##Stat( Stat );

앞에 FScope… 이 붙은 것은 특정 Scope1) 에서 class 인스턴스의 생성자와 소멸자가 자동으로 호출되는 특징을 이용하여 무엇을 측정하기 위한 class 의 prefix 라고 보면 된다.

FScope… 뒤에 CycleCounter 가 붙었으니 대충 특정 구간에서 CPU 의 Cycle 을 측정하는 class 정도로 생각하면 되겠다.

FScopeCycleCounter 는 내부적으로 FStatManager 를 이용하여 측정한다.

1) 보통 brace 한쌍 {} 으로 구분되어 지는 지역을 일컫는다.