Взаимодействие Unreal Insights c Unreal Engine 5 с точки зрения исходного кода

Читая исходный код Unreal Engine 5 я частенько стал натыкаться на загадочный макрос UE_TRACE_LOG (например, использование этого макроса можно заметить в коде UE_LOG). В этой статье я хотел бы рассказать, зачем нужен макрос UE_TRACE_LOG и как он связан с Unreal Insights.

Unreal Insights

Начнем пожалуй с того, что такое Unreal Insights.

Unreal Insights — это отдельная программа, которая по мере хода игры собирает различную полезную информацию и структурирует её. Например в Unreal Insights можно посмотреть количество кадров в разные моменты времени, потребление памяти, сетевую производительность и т.п.

Unreal Insights Interface

Unreal Insights Interface

UE_TRACE_EVENT

Trace ивент — это некоторая структура, которая содержит поля типа TField (1*). Они используются для того, чтобы понять, сколько памяти требуется для хранения тех типов данных, под которые создается TField, в некотором буфере, а также для реализации метода FieldName, в котором вызывается функция Impl класса FFieldSet (2*).

В общем, Trace ивент задает общий размер указанных типов данных, для хранения их в некотором буфере, а также контролирует, в каком именно порядке эти данные должны помещаться в буфер.

Объявление Trace ивента:

Чтобы объявить Trace ивент, нужно вызвать два макроса: UE_TRACE_EVENT_BEGIN и UE_TRACE_EVENT_END;, а между ними вставить один или несколько макросов UE_TRACE_EVENT_FIELD.

  • Макрос UE_TRACE_EVENT_BEGIN (LoggerName, EventName, …) объявляет структуру типа F##LoggerName##EventName##Fields (3*), которая как раз таки и является самим Trace ивентом.

  • Макрос UE_TRACE_EVENT_FIELD (FieldType, FieldName) добавляет новое поле типа TField в Trace ивент.

  • Макрос UE_TRACE_EVENT_END () служит завершением объявления структуры F##LoggerName##EventName##Fields.

Пример объявления Trace ивента:

UE_TRACE_EVENT_BEGIN(Logging, LogMessageSpec, NoSync|Important)
	UE_TRACE_EVENT_FIELD(const void*, LogPoint)
	UE_TRACE_EVENT_FIELD(const void*, CategoryPointer)
	UE_TRACE_EVENT_FIELD(int32, Line)
	UE_TRACE_EVENT_FIELD(uint8, Verbosity)
	UE_TRACE_EVENT_FIELD(UE::Trace::AnsiString, FileName)
	UE_TRACE_EVENT_FIELD(UE::Trace::WideString, FormatString)
UE_TRACE_EVENT_END() 

Итого, смысл структуры F##LoggerName##EventName##Fields заключается в хранении полей типа TField, которые в свою очередь хранят информацию о некотором типе (выбранном нами макросом UE_TRACE_EVENT_FIELD).

Чтобы записать значения некоторого типа в буфер, нужно вызвать функции с именем FieldName (параметр FieldName мы передаем в макрос UE_TRACE_EVENT_FIELD при объявлении Trace ивента), и передать в них некоторые значения. Важно понимать, что требуется вызывать эти функции по тому же порядку, в котором располагаются поля типа TField в Trace ивенте (сверху вниз).

Trace ивент не используется непосредственно для хранения данных.

(1*) TField — это структура, у которой определены поля Index, Offset и Size (которые задаются в зависимости от переданного в эту структуру типа). Эти поля так или иначе используются при записи данных в буфер через функцию Impl.

Например, в одной из специализаций структуры FFieldSet, используется поле Offset для смещения указателя буфера при записи:

template 
struct FLogScope::FFieldSet
{
	static void Impl(FLogScope* Scope, const Type& Value)
	{
		uint8* Dest = (uint8*)(Scope->Ptr) + FieldMeta::Offset;
		::memcpy(Dest, &Value, sizeof(Type));
	}
};

Здесь TField передается в качестве шаблонного типа FieldMeta.

(2*) FFieldSet — структура, которая содержит метод Impl, использующийся для того, чтобы сказать каким именно образом нужно записывать тот или иной тип данных в буфер. То есть в зависимости от определенного типа данных, у FFieldSet может существовать определенная специализация. Соответственно метод Impl будет отличаться.

Реализация стандартной структуры FFieldSet, где в методе Impl осуществляется обычное копирование данных в буфер через функцию memcpy:

template 
struct FLogScope::FFieldSet
{
	static void Impl(FLogScope* Scope, const Type& Value)
	{
		uint8* Dest = (uint8*)(Scope->Ptr) + FieldMeta::Offset;
		::memcpy(Dest, &Value, sizeof(Type));
	}
};

(3*) Полный код структуры F##LoggerName##EventName##Fields:

struct F##LoggerName##EventName##Fields \
	{ \
		enum \
		{ \
			Important			= UE::Trace::Private::FEventInfo::Flag_Important, \
			NoSync				= UE::Trace::Private::FEventInfo::Flag_NoSync, \
			Definition8bit		= UE::Trace::Private::FEventInfo::Flag_Definition8, \
			Definition16bit		= UE::Trace::Private::FEventInfo::Flag_Definition16, \
			Definition32bit		= UE::Trace::Private::FEventInfo::Flag_Definition32, \
			Definition64bit		= UE::Trace::Private::FEventInfo::Flag_Definition64, \
			DefinitionBits		= UE::Trace::Private::FEventInfo::DefinitionBits, \
			PartialEventFlags	= (0, ##__VA_ARGS__), \
		}; \
		enum : bool { bIsImportant = ((0, ##__VA_ARGS__) & Important) != 0, bIsDefinition = ((0, ##__VA_ARGS__) & DefinitionBits) != 0,\
		bIsDefinition8 = ((0, ##__VA_ARGS__) & Definition8bit) != 0, \
		bIsDefinition16 = ((0, ##__VA_ARGS__) & Definition16bit) != 0,\
		bIsDefinition32 = ((0, ##__VA_ARGS__) & Definition32bit) != 0, \
		bIsDefinition64 = ((0, ##__VA_ARGS__) & Definition64bit) != 0,}; \
		typedef std::conditional_t>> DefinitionType;\
		static constexpr uint32 GetSize() { return EventProps_Meta::Size; } \ - сумма размеров всех типов полей TField (то есть сумма типов, которые хранит TField).
		static uint32 TSAN_SAFE GetUid() { static uint32 Uid = 0; return (Uid = Uid ? Uid : Initialize()); } \ - ID данного инвента
		static uint32 FORCENOINLINE Initialize() \ генерирует ID для нашего ивента и определяет информацию о ивенте (UE::Trace::Private::FEventInfo Info), а так же вписывает инфу и ID в экземпляр FEventNode.
		{ \
			static const uint32 Uid_ThreadSafeInit = [] () \
			{ \
				using namespace UE::Trace; \
				static F##LoggerName##EventName##Fields Fields; \
				static UE::Trace::Private::FEventInfo Info = \
				{ \
					FLiteralName(#LoggerName), \
					FLiteralName(#EventName), \
					(FFieldDesc*)(&Fields), \
					EventProps_Meta::NumFields, \
					uint16(EventFlags), \
				}; \
				return LoggerName##EventName##Event.Initialize(&Info); \
			}(); \
			return Uid_ThreadSafeInit; \
		} \
		typedef UE::Trace::TField<0 /*Index*/, 0 /*Offset*/,

Код макроса UE_TRACE_EVENT_FIELD:

#define TRACE_PRIVATE_EVENT_FIELD(FieldType, FieldName) \
		FieldType> FieldName##_Meta; \
		FieldName##_Meta const FieldName##_Field = UE::Trace::FLiteralName(#FieldName); \
		template  auto FieldName(Ts... ts) const { \
			LogScopeType::FFieldSet::Impl((LogScopeType*)this, Forward(ts)...); \ - информация записывается поверх полей TField.
			return true; \
		} \
		typedef UE::Trace::TField< \
			FieldName##_Meta::Index + 1, \
			FieldName##_Meta::Offset + FieldName##_Meta::Size,

Как можно видеть, макрос UE_TRACE_EVENT_FIELD дополняет структуру F##LoggerName##EventName##Fields, тем самым объявляя новый TField.

UE_TRACE_LOG

Макрос, который регистрирует наш ивент (записывает информацию, которую мы вносим через оператор<<, в некоторый буфер. В конечном итоге этот буфер передается в Unreal Insights).

Пример использования UE_TRACE_LOG:

UE_TRACE_LOG(Logging, LogCategory, LogChannel, NameLen * sizeof(ANSICHAR))
		<< LogCategory.CategoryPointer(Category)
		<< LogCategory.DefaultVerbosity(DefaultVerbosity)
		<< LogCategory.Name(Name, NameLen);

Параметры Logging и LogCategory являются атрибутами структуры F##LoggerName##EventName##Fields. При этом параметр LogChannel также является «экземпляром» структуры F##LoggerName##EventName##Fields. Последний параметр NameLen * sizeof (ANSICHAR) представляет собой размер, который необходимо выделить в буфере для занесения в него переданных данных.

Скрытый текст

Полный код UE_TRACE_LOG:

#define UE_TRACE_LOG(LoggerName, EventName, ChannelsExpr, ...) \
	TRACE_PRIVATE_LOG_PRELUDE(Enter, LoggerName, EventName, ChannelsExpr, ##__VA_ARGS__) \
		TRACE_PRIVATE_LOG_EPILOG()

Полный код TRACE_PRIVATE_LOG_PRELUDE:

#define TRACE_PRIVATE_LOG_PRELUDE(EnterFunc, LoggerName, EventName, ChannelsExpr, ...) \
	if (TRACE_PRIVATE_CHANNELEXPR_IS_ENABLED(ChannelsExpr)) \
		if (auto LogScope = F##LoggerName##EventName##Fields::LogScopeType::EnterFunc(__VA_ARGS__)) \ - создание экземпляра класса FLogScope, который будет хранить всю вносимую информацию нашего ивента.
			if (const auto& __restrict EventName = *UE_LAUNDER((F##LoggerName##EventName##Fields*)(&LogScope))) \ - получаем указатель на начало буффера и читаем его как структуру F##LoggerName##EventName##Fields(ивент), для того чтобы инициализировать память буффера нашими значениями (тут важно сказать, что мы не инициализируем объект F##LoggerName##EventName##Fields, так как он тут и не нужен. Мы просто пользуемся его функционалом для выделения памяти)
				((void)EventName), - возможно некоторые компиляторы выдают сообщение о неиспользованной переменной => кастим EventName к void, чтобы предупреждения не выдавало

Полный код TRACE_PRIVATE_LOG_EPILOG:

#define TRACE_PRIVATE_LOG_EPILOG() \
	LogScope += LogScope - оператор+=: сохранение указателя на буффер в некоторое хранилище для того чтобы в дальнейшем иметь в нему доступ.

FLogScope

FLogScope — класс, который хранит некоторый буфер. FLogScope также имеет инструменты для записи различных данных в этот буфер (через перегрузки операторов += и <<).

Оператор <<:

const FLogScope&	operator << (bool) const	{ return *this; }

Оператор +=:

template 
inline void TLogScope::operator += (const FLogScope&) const
{
	if constexpr (bMaybeHasAux)
	{
		FWriteBuffer* LatestBuffer = Writer_GetBuffer();
		LatestBuffer->Cursor[0] = uint8(EKnownEventUids::AuxDataTerminal << EKnownEventUids::_UidShift);
		LatestBuffer->Cursor++;

		Commit(LatestBuffer);
	}
	else
	{
		Commit();
	}
}

Метод Commit в свою очередь просто назначает полю Commited буфера нужный адрес в памяти (куда были выгружены данные:, а именно поле Cursor буфера).

В дальнейшем Unreal Insights будет обращаться именно к полю Commited (также поле Commited помечено ключевым словом volatile).

Итоги

То есть сначала мы в буфер правого LogScope записываем информацию (оператор<<) (она отобразится и для левого LogScope, так как при передаче в оператор+= LogScope не копируется), а затем эта информация сохраняется в буфере этого LogScope.

Тут также стоит сказать, что перед записью основной информации в FLogScope, сначала записывается информация о ID текущего Trace ивента и размере наполняемого пакета (текущего буфера). То есть именно по переданному ID Trace ивента Unreal Insights понимает, в какую вкладку (Log, Frame, …) включать поступающую из памяти информацию.

© Habrahabr.ru