Читая исходный код Unreal Engine 5 я частенько стал натыкаться на загадочный макрос UE_TRACE_LOG (например, использование этого макроса можно заметить в коде UE_LOG). В этой статье я хотел бы рассказать, зачем нужен макрос UE_TRACE_LOG и как он связан с Unreal Insights.
Unreal Insights
Начнем пожалуй с того, что такое Unreal Insights.
Unreal Insights - это отдельная программа, которая по мере хода игры собирает различную полезную информацию и структурирует её. Например в Unreal Insights можно посмотреть количество кадров в разные моменты времени, потребление памяти, сетевую производительность и т.п.
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 <typename FieldMeta, typename Type>
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 <typename FieldMeta, typename Type>
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<bIsDefinition8, UE::Trace::FEventRef8, std::conditional_t<bIsDefinition16, UE::Trace::FEventRef16 , std::conditional_t<bIsDefinition64, UE::Trace::FEventRef64, UE::Trace::FEventRef32>>> 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 <typename... Ts> auto FieldName(Ts... ts) const { \
LogScopeType::FFieldSet<FieldName##_Meta, FieldType>::Impl((LogScopeType*)this, Forward<Ts>(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<F##LoggerName##EventName##Fields>(__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 <bool bMaybeHasAux>
inline void TLogScope<bMaybeHasAux>::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, ...) включать поступающую из памяти информацию.
JordanCpp
Нужно было назвать статью, многоэтажные макросы в Unreal Engine 5:)