Function<void()> callback;
...
callback += []() { SomethingHappened(); };
callback += []() { SomethingHappenedMoore(); };
Видели такое, мм? )
Те кто писал на C# знают, что это офигенная штука, но в плюсах такого не сделать через +=, придется городить кракозяблы.. Или нет?
Сегодня расскажу про замену std::function в моем движке - o2::Function<>. Он повторяет функционал оригинала из std, но добавляет изрядно синтаксического сахара. Основное - это возможность хранить в коллбеке сразу несколько функций.
А так же немного об оптимизации внутри 😉
Начнем с того, что такое вообще std::function, как оно оборачивает лямду и как это работает.
Вспомним (или узнаем) что лямда - это определенный тип объекта (под каждую лямду отдельный тип), с переопределенной функцией operator(). Собственно когда мы создаем лямду, например вот так:
то мы создаем объект типа этой лямды, в тело которого копируются захваченные переменные var1 и var2, с переопределенной функцией operator(). Этот объект разворачивается примерно в такой код:
Но так происходит не всегда, а только если есть захват переменных. Если нет, то лямда - это просто статичная функция
Собственно, std::function<> заворачивает лямду внутри себя. Он хранит в себе экземпляр объекта типа лямды (my_labmda из псевдокода) и так же имеет перегруженную функцию operator(). Вот псевдокод минимальной реализации std::function
Вспомним (или узнаем) что лямда - это определенный тип объекта (под каждую лямду отдельный тип), с переопределенной функцией operator(). Собственно когда мы создаем лямду, например вот так:
auto myFunc = [var1, var2]() { ... do something ... }
то мы создаем объект типа этой лямды, в тело которого копируются захваченные переменные var1 и var2, с переопределенной функцией operator(). Этот объект разворачивается примерно в такой код:
struct __my_labmda__
{
int var1, var2;
__my_labmda__(int var1, int var2): var1(var1), var2(var2) {}
void operator() { ... do something ... }
};
__my_lambda__ myFunc(var1, var2);
Но так происходит не всегда, а только если есть захват переменных. Если нет, то лямда - это просто статичная функция
Собственно, std::function<> заворачивает лямду внутри себя. Он хранит в себе экземпляр объекта типа лямды (my_labmda из псевдокода) и так же имеет перегруженную функцию operator(). Вот псевдокод минимальной реализации std::function
template<typename _lambda_type, typename _res_type, typename ... _args>
struct function
{
_lambda_type lambda; // Храним объект лямды
function(_lambda_type&& lambda): lambda(std::forward<_lambda_type>(lambda)) {} // Конструируемся из лямды
_res_type operator(_args ... args) { return lambda(args ...); } // Вызов лямды
};
Из этого псевдокода уже видно, что function может хранить только одну lambda. Вторую и последюущие добавить так просто не получится, тк function уже специализирован под конкретный тип лямды - _lambda_type
Выход простой - завернуть _lambda_type lambda в промежуточный объект с интерфейсом:
Который уже использовать для различных типов лямд - SharedLamda. Так же можно под этот интерфейс завернуть и другие типы функций:
- статичная функция - FunctionPtr
- функция класса - ObjFunctionPtr
Далее function превращается в контейнер объектов от интерфейса IFunction:
Этот всевдокод уже показывает как нам сохранить несколько функций в одной. В реальном Function<> богатый api для работы с разными фнукциями и лямдами сразу. Они конструируют нужную реализацию IFunction внутри и добавляют в список
Но что насчет перфоманса? Тут и аллокации, и поинтеры? Моя первая реализация и правда была настолько простой, как в псевдокоде. Но это давало ощутимые просадки, ведь даже на передачу одной лямды требуется работа с вектором, а это и лишняя память, и аллокации...
Выход простой - завернуть _lambda_type lambda в промежуточный объект с интерфейсом:
template<typename _res_type, typename ... _args>
class IFunction<_res_type(_args ...)>
{
public:
virtual _res_type Invoke(_args ... args) const = 0;
};
Который уже использовать для различных типов лямд - SharedLamda. Так же можно под этот интерфейс завернуть и другие типы функций:
- статичная функция - FunctionPtr
- функция класса - ObjFunctionPtr
Далее function превращается в контейнер объектов от интерфейса IFunction:
template<typename _lambda_type, typename _res_type, typename ... _args>
struct function
{
std::vector<IFunction<_res_type(_args ...)>*> functions; // Храним объекты функций
// Добавляем функцию
function& operator+(IFunction<_res_type(_args ...)>* func)
{
functions.push_back(func);
}
// Вызываем все функции, возвращаемое значение берем от последней
_res_type operator(_args ... args)
{
if (functions.size() == 0)
return _res_type();
for (int i = 0; i < functions.size() - 1; i++)
functions[i]->Invoke(args ...);
return functions.back()->Invoke(args ...);
}
};
Этот всевдокод уже показывает как нам сохранить несколько функций в одной. В реальном Function<> богатый api для работы с разными фнукциями и лямдами сразу. Они конструируют нужную реализацию IFunction внутри и добавляют в список
Но что насчет перфоманса? Тут и аллокации, и поинтеры? Моя первая реализация и правда была настолько простой, как в псевдокоде. Но это давало ощутимые просадки, ведь даже на передачу одной лямды требуется работа с вектором, а это и лишняя память, и аллокации...
Здесь я применил довольно простую оптимизацию, наподобие small string optimization. Суть ее простая - если один объект по размеру меньше или равен контейнеру, в который он помещается, то он хранится прямо в области памяти контейнера
Чуть подробнее. Для этого используется Union - это такая штука позволяющая в одном участке памяти как бы хранить несколько типов данных сразу. Все сразу использовать нельзя, но эту память можно интерпретировать как один из этих типов. Например:
Прям так как в примере, естественно, делать нельзя - нужно знать что именно хранится в d. Ведь под все типы данных у нас единая память. Компилятор считает наибольший размер, выравние и удобно заворачивает доступ к конкретному типу
Для более простой работы используется современный std::variant, однако union'ы нам нужны для понимания оптимизации
Ведь в этом union хранится сразу два варианта хранения функций внутри: одна функция или множество
Чуть подробнее. Для этого используется Union - это такая штука позволяющая в одном участке памяти как бы хранить несколько типов данных сразу. Все сразу использовать нельзя, но эту память можно интерпретировать как один из этих типов. Например:
union data
{
float number;
std::string string;
bool flag;
};
data d = ...;
d.number = 5; // запись данных из области памяти d в виде float
d.string = "hello"' // запись данных из области памяти d в виде строки
d.flag = true; // запись данных из области памяти d в виде boolean
Прям так как в примере, естественно, делать нельзя - нужно знать что именно хранится в d. Ведь под все типы данных у нас единая память. Компилятор считает наибольший размер, выравние и удобно заворачивает доступ к конкретному типу
Для более простой работы используется современный std::variant, однако union'ы нам нужны для понимания оптимизации
Ведь в этом union хранится сразу два варианта хранения функций внутри: одна функция или множество
Вот вырезка из кода, в каком виде это хранится:
Рассмотрим по порядку:
-
-
- кусок памяти под саму IFunction -
- указатель на деструктор этой функции. Он необходим для корректного освобождения хранимой функции. Ведь в ней могут быть захвачены переменные, которые необходимо корректно освободить и тп
-
Когда Function<> конструируется из одной IFunction<>, например небольшой лямды или указателя на функцию класса, то включается оптимизация и эта функция без аллокации сохраняется прямо в functionData[capacity].
Если она больше, или функций больше одной, то переключаемся на вариант с std::vector<>
Это работает довольно хорошо, потому что в большинстве случаев мы все-таки в Function<> храним только одну функцию. Чуть затратнее по памяти, однако косты на аллокации срезаются. У меня разница в производительности была заметна на глаз - этап загрузки редактора с кучей коллбеков ускорился значительно. Лишний раз подтвердило что аллокации - зло
struct TypeData
{
Byte padding[payloadSize];
DataType type;
};
struct OneFunctionData
{
static constexpr UInt capacity = payloadSize - sizeof(void*);
Byte functionData[capacity];
void(*destructor)(IFunction<_res_type(_args ...)>*) = nullptr;
};
union Data
{
std::vector<IFunction<_res_type(_args ...)>*> functions;
OneFunctionData oneFunctionData;
TypeData typeData;
};
Рассмотрим по порядку:
-
TypeData
- контейнер типа хранимых данных. Типа хранится позади padding[payloadSize]-
OneFunctionData
- контейнер для одной функции. Внутри:- кусок памяти под саму IFunction -
Byte functionData[capacity]
, - указатель на деструктор этой функции. Он необходим для корректного освобождения хранимой функции. Ведь в ней могут быть захвачены переменные, которые необходимо корректно освободить и тп
-
std::vector<IFunction<_res_type(_args ...)>*> functions
- собственно вектор с объектами функцийКогда Function<> конструируется из одной IFunction<>, например небольшой лямды или указателя на функцию класса, то включается оптимизация и эта функция без аллокации сохраняется прямо в functionData[capacity].
Если она больше, или функций больше одной, то переключаемся на вариант с std::vector<>
Это работает довольно хорошо, потому что в большинстве случаев мы все-таки в Function<> храним только одну функцию. Чуть затратнее по памяти, однако косты на аллокации срезаются. У меня разница в производительности была заметна на глаз - этап загрузки редактора с кучей коллбеков ускорился значительно. Лишний раз подтвердило что аллокации - зло
И, напоследок, небольшой бонус. Как это вообще так определяется шаблонный класс, умеющий принимает в себя аргументы шаблонов в специфичном виде? ))
Просто так объявить такой шаблон не получится... Все дело в магии forward'а этого класса. Если объявить его в таком простом виде, то затем можно сказать компилятору принимать шаблоны в этом специфичном виде
Полный исходник можно посмотреть здесь: https://github.com/o2-engine/o2/blob/master/Framework/Sources/o2/Utils/Function/Function.h
Function<_res_type(_args ...)>
Просто так объявить такой шаблон не получится... Все дело в магии forward'а этого класса. Если объявить его в таком простом виде, то затем можно сказать компилятору принимать шаблоны в этом специфичном виде
template <typename UnusedType>
class IFunction;
template<typename _res_type, typename ... _args>
class IFunction<_res_type(_args ...)>
{ ... };
Полный исходник можно посмотреть здесь: https://github.com/o2-engine/o2/blob/master/Framework/Sources/o2/Utils/Function/Function.h
This media is not supported in your browser
VIEW IN TELEGRAM
навайбкодил. Питон, ни одной строчки руками не набрал. Но я знаю как должно работать внутри, все по моей инструкции. Страшно 🥲 или нет?
CodeGraphViewerTool.zip
166 KB
тулзу на питоне сгенерировал почти без ручных правок кода. Сгенеренный код довольно хорош. В целом llm с питоном и раньше справлялся лучше чем с С++, но тут я бы сам отревьюил код как "хороший"
помогать приходилось обильно, все классические грабли - что-то llm забывает, что-то делает не верно. Но в целом я действительно давал инструкции и у меня получилось слепить тулзу
другое дело что моя идея с физическим разрешением зависимостей мягко говоря провалилась, но об этом чуть позже
приложу архивчик с сорцами, если интересно
помогать приходилось обильно, все классические грабли - что-то llm забывает, что-то делает не верно. Но в целом я действительно давал инструкции и у меня получилось слепить тулзу
другое дело что моя идея с физическим разрешением зависимостей мягко говоря провалилась, но об этом чуть позже
приложу архивчик с сорцами, если интересно
большой проблемой стала проихводительность питона. Дело в том, что я пытался анализировать свой рабочий проект - игру homescapes. Она довольно большая )
и если тестовый пример с десятью сущностями работали прекрасно, то на 10 тысячах стало все прям печально.
хорошая новость в том, что через llm получилось ускорить на порядки. Местами конечно очевидные оптимизации, местами приходилось подсказывать, но например прикрутить jit компиляцию он предложил сам, а так же несколько либ ускоряющих перфоманс. Что порадовало
плохая новость в том, что кадр все равно обсчитывает очень долго - полторы секунды - и тулза совсем бесполезна
поэтому я пошел на следующую итерацию, сделать тулзу на С++
и если тестовый пример с десятью сущностями работали прекрасно, то на 10 тысячах стало все прям печально.
хорошая новость в том, что через llm получилось ускорить на порядки. Местами конечно очевидные оптимизации, местами приходилось подсказывать, но например прикрутить jit компиляцию он предложил сам, а так же несколько либ ускоряющих перфоманс. Что порадовало
плохая новость в том, что кадр все равно обсчитывает очень долго - полторы секунды - и тулза совсем бесполезна
поэтому я пошел на следующую итерацию, сделать тулзу на С++
и здесь я сразу решил с ноги зайти и просто написать что за тулзу я хочу. Да да, одним промтом, вот прям из коробки.
За основу я взял демо-проект imgui со своим прикрученным профайлером
Вот первый промт:
За основу я взял демо-проект imgui со своим прикрученным профайлером
Вот первый промт:
я хочу сделать графическую утилиту, которая сканирует исходники С++ в нескольких папках, строит зависимости, и отображает с помощью imgui в графическом виде эти зависимости. Графически эти зависимости кластеризуют связанные сущности (исходники), с помощью физических законов. Сущности представлены в виде цветных кружков, зависимости (или связи) в виде линий.
Зависимости строятся на 2х уровнях:
- иерархия папок и подпапок: для каждой директории создается сущность директории, дочерняя директория зависит от родительской. Внутри директории есть список сущностей - файлов в ней
- зависимости между исходниками С++ через #include: из каждого исходника парсится список инклюдов, ищутся файлы-исходники по путям и между ними создается связь
Работа физики:
- для этого сущности расположены в 2д мире, у каждого есть позиция
- между ними есть зависимости- связи, которые задают некое расстояние между сущностями
- сущности стараются оттолкнуться от билзлижащих сущностей
- используется интеграция верле и разрешение связей как в научной работе якобсена из hitman codename 47
- при зажимании левой кнопки мыши умеет захватывать сущность чтобы ее перемещать вслед за курсором
Графика:
- используется imgui Для отображения. В текущем исходнике Main.cpp создается дополнительное окно, в котором отображается вся графика: сущности и связи
- сущности - это разноцветные круги
- связи - это линнии между ними
- существует камера, которой можно управлять: скролл (перемещение) с зажатой правой кнопкой мыши, зум с помощью колесика
- наведение курсора на сущность включает ее подсветку: отображается имя исходника, а так же связь с родителем и детьми
- для профилирования используется nano profiler: работа физики и графики
что нужно сделать:
- завести json конфиг с параметрами: исходные директории для сканирования, настройки графики и физики. Работает как отдельный класс
- сделать класс парсера исходников с многопоточностью: берет исходные директории из json конфига и рекурсивно сканирует их в поисках C++ исходников .h/.cpp, парсит #include, связывает сущности, создает сущности для всех директорий
- физический движок, оптимизированный для вычисления 10000 сущностей и 30000 связей. Инициализируется из парсера исходников, сохраняя ссылки на исходные сущности. ПРоводит симуляцию мира и физики: интегрирует позиции, разрешает связи, расталкивает близлижащие сущности
- графическое отображение. Содержит камеру, обрабатывает инпут пользователя, отображает сущности и связи
- главный класс утилиты: читает конфиг, парсит исходники, инициализирует физику и графику, запускает игровой цикл апдейта фрейма - обонвление физики, обработка инпута и отрисовка
Разделяй вышеописанное на классы в разных файлах, рядом с Main.cpp. Используй для всего стандартную библиотеку stl и imgui. Не используй ничего кастомного, никаких скриптов, только С++
GitHub
GitHub - zenkovich/imgui_perfmon: Simple performance widget for ImGUI
Simple performance widget for ImGUI. Contribute to zenkovich/imgui_perfmon development by creating an account on GitHub.
cursor_c.md
767.4 KB
иии... тулза сгенерилась и скомпилилась сразу. Но естественно не совсем так, как хотелось.
Поэтому дальше допиливалось напильником, правился код стайл, в общем вот вся история общения с С++ версией
Проблемы были все те же: что-то забывал, что-то делал не так, но в код руками я почти не лез
Поэтому дальше допиливалось напильником, правился код стайл, в общем вот вся история общения с С++ версией
Проблемы были все те же: что-то забывал, что-то делал не так, но в код руками я почти не лез
This media is not supported in your browser
VIEW IN TELEGRAM
В результате получилась тулза, значительно быстрее по перфомансу, FPS получается в районе 20ти, что неплохо для 10 тыс сущностей, 30к связей и полумиллиона пар расталкивания
Здесь я практически не просил оптимизировать, некоторые алгоритмы llm выдал сразу оптимизированными. Например, для расталкивания сразу применил кластеризацию пространства, чтобы искать близко лежащие пары сущностей
Но сама тулза в итоге бесполезна. Физическая система не справляется с разрешением противоречий в системе зависимостей, в итоге она либо взрывается, либо уплывает куда-то в сторону. Думаю это все можно доработать, но уже не очень много смысла в этом, проще посмотреть структуру папок и директорий чтобы понять структуру.
Идея была в том, чтобы связать и кластеризовать зависящие друг от друга логические куски игры. Но, оказалось что все связано со всем, и все превращается в кашу
Здесь я практически не просил оптимизировать, некоторые алгоритмы llm выдал сразу оптимизированными. Например, для расталкивания сразу применил кластеризацию пространства, чтобы искать близко лежащие пары сущностей
Но сама тулза в итоге бесполезна. Физическая система не справляется с разрешением противоречий в системе зависимостей, в итоге она либо взрывается, либо уплывает куда-то в сторону. Думаю это все можно доработать, но уже не очень много смысла в этом, проще посмотреть структуру папок и директорий чтобы понять структуру.
Идея была в том, чтобы связать и кластеризовать зависящие друг от друга логические куски игры. Но, оказалось что все связано со всем, и все превращается в кашу
Ах да, что в итоге с gpt-5. Честно говоря я не увидел особых отличий в лучшую сторону от claude-4, который использовал все последнее время. Честно говоря даже местами получается хуже. Я пробовал и на других задачах, буквальо спрашивая одно и то же у двух моделей, и claude был понятнее, предсказуемее и... быстрее! gpt-5 очень долго думает, около минуты, чтобы предпринять действие. Честно говоря мне и самому минуты хватает чтобы разобраться, особенно в хорошо знакомом мне коде
Поэтому, пока что откатываюсь к более старой и проверенной claude
Поэтому, пока что откатываюсь к более старой и проверенной claude
Порой в реальных проектах классы становятся сложными, в них много полей и даже бывает что они просто плохо написаны их сложно смотреть при отладке. Тебе нужно увидеть значение какой-нибудь переменной, а до нее листать и листать, или разворачивать кучу вложенностей
Поверх этого еще могут быть оптимизационные ухищрения - кастомные коллекции, union'ы и другие страшные штуки, которые в отладке просто не читаемы
Еще бывают довольно простые типы, например математические вектора (x, y, z), цвет (RGBA), UID и тд, в общем что-то простое, но чтобы узнать содержимое - нужно разворачивать. Хотя данные можно было бы уместить в одну строку удобным образом
Поверх этого еще могут быть оптимизационные ухищрения - кастомные коллекции, union'ы и другие страшные штуки, которые в отладке просто не читаемы
Еще бывают довольно простые типы, например математические вектора (x, y, z), цвет (RGBA), UID и тд, в общем что-то простое, но чтобы узнать содержимое - нужно разворачивать. Хотя данные можно было бы уместить в одну строку удобным образом
Для всего этого есть специальные форматтеры, подключаемые к IDE, чтобы упростить вывод сложных объектов. Они говорят IDE в каком виде представлять ту или иную структуру, условное форматирование и форматирование превью объекта - то что может вывести XYZ или RGBA прямо в строке переменной.
Это и есть те самые natvis и lldb formatters. Есть и другие, но я пока использовал только их, поэтому расскажу про них 😁
- natvis - это инструмент чисто под msvs. Работает как конфиг, оперирующий полями объекта. Синтаксис довольно не простой, но возможности довольно обширные: условное форматирование, разворачивание сложных коллекций а-ля деревья/списки
- lldb formatters - это api для python, который позволяет писать скрипты для форматирования отображения в lldb. Тут все просто - пишешь скрипт под определенное API отладчика, внутри делаешь что хочешь. Но есть проблемы с перфомансом
Это и есть те самые natvis и lldb formatters. Есть и другие, но я пока использовал только их, поэтому расскажу про них 😁
- natvis - это инструмент чисто под msvs. Работает как конфиг, оперирующий полями объекта. Синтаксис довольно не простой, но возможности довольно обширные: условное форматирование, разворачивание сложных коллекций а-ля деревья/списки
- lldb formatters - это api для python, который позволяет писать скрипты для форматирования отображения в lldb. Тут все просто - пишешь скрипт под определенное API отладчика, внутри делаешь что хочешь. Но есть проблемы с перфомансом
Вывод краткого описания
Полезно для каких-то простых структур, содержимое которых можно уместить в одной строке: цвет RGBA, координаты вектора XYZ, интернированные строки и т.п.
в natvis это делается через тег
Так же можно задавать условие отображения того или иного варианта, что весьма удобно. Вот классический пример для умного указателя: если поинтер не пустой, то показываем его, иначе "empty"
в lldb formatter'ах чуть сложнее. Сначала нужно зарегать хендлер для типа:
затем реализовать его:
Относительно xml это выглядит понятнее и гибче
Полезно для каких-то простых структур, содержимое которых можно уместить в одной строке: цвет RGBA, координаты вектора XYZ, интернированные строки и т.п.
в natvis это делается через тег
DisplayString
, внутри которого можно форматировать строку из доступных полей класса:<Type Name="o2::Vertex">
<DisplayString>{{ x: { x } y: { y } z: { z } c: { color } u: { tu } v: { tv }}</DisplayString>
</Type>
Так же можно задавать условие отображения того или иного варианта, что весьма удобно. Вот классический пример для умного указателя: если поинтер не пустой, то показываем его, иначе "empty"
<DisplayString Condition="(mPtr != 0)">{ mPtr }</DisplayString>
<DisplayString Condition="(mPtr == 0)">empty</DisplayString>
в lldb formatter'ах чуть сложнее. Сначала нужно зарегать хендлер для типа:
debugger.HandleCommand('type summary add -F o2_lldb_formatters.vertex_summary "o2::Vertex"')
затем реализовать его:
def get_child_value(valobj, name):
"""Helper to safely get child value"""
try:
child = valobj.GetChildMemberWithName(name)
if child.IsValid():
return child.GetValue()
return "?"
except:
return "?"
def vertex_summary(valobj, internal_dict):
"""Format o2::Vertex"""
x = get_child_value(valobj, "x")
y = get_child_value(valobj, "y")
z = get_child_value(valobj, "z")
color = get_child_value(valobj, "color")
tu = get_child_value(valobj, "tu")
tv = get_child_value(valobj, "tv")
return f"{{ x: {x} y: {y} z: {z} c: {color} u: {tu} v: {tv} }}"
Относительно xml это выглядит понятнее и гибче
Построение дочерних нод и структуры
Когда нужно какие-то поля показать, а какие-то нет. Использовать более читаемые слова или изменить порядок
В natvis это делается с помощью тегов <Expand>, <Synthetic> и <Item>:
-
-
-
-
Рассмотри пару примеров. Здесь просто перечисляем некоторые поля класса. Обратите внимание на само описание типа - оно задается для шаблонного типа, через
Или вот пример посложнее с синтезированными полями. Здесь описан тип меша с кастомными коллекциями вершин и индексов. IDE передаются два параметра: Size - размер коллекции, и ValuePointer - откуда начинать отсчет
В целом, формат хоть и немного упоротый, но с ним можно легко разобраться, особенно в эру ИИ, которые справляются с этим на ура.
Здесь в моем движке лежит список .natvis описаний типов, там есть довольно сложные описания типа актора
https://github.com/o2-engine/o2/blob/master/Framework/Platforms/Windows/Framework.natvis
Когда нужно какие-то поля показать, а какие-то нет. Использовать более читаемые слова или изменить порядок
В natvis это делается с помощью тегов <Expand>, <Synthetic> и <Item>:
-
<Expand>
- список полей при разворачивании-
<Item>
- конкретное поле класса-
<Synthetic>
- сложное синтезированное поле класса, которое может быть коллекцией-
<ExpandedItem>
- развернутые поля определенного дочернего поля классаРассмотри пару примеров. Здесь просто перечисляем некоторые поля класса. Обратите внимание на само описание типа - оно задается для шаблонного типа, через
<*>
(да, вот такой вот незамысловатый формат с кодированием угловых скобок в xml)<Type Name="o2::FieldInfo<*>">
<DisplayString>{{ name={ mName } value={ mFieldRef } }}</DisplayString>
<Expand>
<Item Name="name">mName</Item>
<Item Name="value">mFieldRef</Item>
<Item Name="owner">mOwner</Item>
</Expand>
</Type>
Или вот пример посложнее с синтезированными полями. Здесь описан тип меша с кастомными коллекциями вершин и индексов. IDE передаются два параметра: Size - размер коллекции, и ValuePointer - откуда начинать отсчет
<Type Name="o2::Mesh">
<DisplayString>{{ vertx = { vertexCount }/{ mMaxVertexCount } poly = { polyCount }/{ mMaxPolyCount } tex = { mTexture.mTexture } }}</DisplayString>
<Expand>
<Item Name="texture" ExcludeView="simple">mTexture.mTexture</Item
<Synthetic Name="verticies">
<DisplayString>{ vertexCount }/{ mMaxVertexCount }</DisplayString>
<Expand>
<ArrayItems>
<Size>vertexCount</Size>
<ValuePointer>vertices</ValuePointer>
</ArrayItems>
</Expand>
</Synthetic
<Synthetic Name="indexes">
<DisplayString>{ polyCount }/{ mMaxPolyCount }</DisplayString>
<Expand>
<ArrayItems>
<Size>polyCount*3</Size>
<ValuePointer>indexes</ValuePointer>
</ArrayItems>
</Expand>
</Synthetic
</Expand>
</Type>
В целом, формат хоть и немного упоротый, но с ним можно легко разобраться, особенно в эру ИИ, которые справляются с этим на ура.
Здесь в моем движке лежит список .natvis описаний типов, там есть довольно сложные описания типа актора
https://github.com/o2-engine/o2/blob/master/Framework/Platforms/Windows/Framework.natvis
Для lldb все сильно проще - пишется скрипт, который формирует нужную структуру и отображение. Но у меня не получилось это хорошо завести из-за проблем перфоманса. Эти форматтеры вешали отладчик намертво, в итоге остались только простые summary обработчики
https://github.com/o2-engine/o2/blob/master/Framework/Platforms/o2_lldb_formatters.py
https://github.com/o2-engine/o2/blob/master/Framework/Platforms/o2_lldb_formatters.py