Порт dagon:imgui на SDL GPU
Рендер Dagon 2 еще не завершен, но я решил уже сейчас наскоком закрыть одну из самых нестандартных задач всего проекта — написать бэкенд ImGui для SDL GPU. Что интересно, эта работа вскрыла очередную проблему с фрейм-таймингом, на которую я раньше не обращал внимания.

ImGui — потрясающе хорошо спроектированная библиотека. Она рисует интерфейсы при помощи абстрактного низкоуровневого воркфлоу, путем формирования списка примитивов. Чтобы фактически вывести графику ImGui на экран, нужно написать код, который в реальном времени передает эти данные в графический API. Это не самая тривиальная задача, но, как ни парадоксально, для SDL GPU она решается даже несколько проще, чем для OpenGL. Меня вообще этот API зацепил именно тем, что он избавляет от неудобных и довольно запутанных идиом OpenGL, не порождая при этом кучу новых, как это делают другие. На SDL многие распространенные конструкции выглядят в разы короче, чем на OpenGL или Direct3D (и я уж молчу про чистый Vulkan). В общем, оказалось, что ImGui отлично дружит с SDL GPU.
Перебор примитивов, полученных через igGetDrawData, и их трансляция в вызовы отрисовки — довольно простой процесс. Управление вершинным и индексным буферами под это дело уже сложнее: особенность в том, что предельное количество входящих примитивов ImGui заранее не известно, поэтому буферы должны быть динамическими. Точнее, нужно их динамически пересоздавать с запасом, если текущего объема не хватает. Когда пишешь такое, ощущение как от системной разработки — кажется, будто создаешь какой-то свой X11 😅
Но мало написать графический бэкенд, нужна еще прослойка, обрабатывающая ввод из SDL, иначе интерфейс не будет реагировать на события мыши и нажатия клавиш. Ее тоже пришлось написать самостоятельно, чтобы не трогать внутренности cimgui.dll — в Dagon 1.0 я полагался на встроенный в библиотеку бэкенд под SDL2. Фактически эта задача сводится к переводу событий и клавиатурных кодов SDL в формат ImGui, а также небольшому хаку с функциями TextInput для того, чтобы правильно работал ввод текста:
if (io.WantTextInput)
{
if (!SDL_TextInputActive(application.window))
SDL_StartTextInput(application.window);
}
else
{
if (SDL_TextInputActive(application.window))
SDL_StopTextInput(application.window);
}
Наконец, последнее, что я сделал — слегка изменил логику рендера, а именно вынес получение текстуры свопчейна из PresentPass в Renderer, в начало кадра (это нужно, чтобы можно было рендерить в задний буфер несколькими проходами подряд). Не поверите, но это единственное, что пришлось изменить в ядре движка, чтобы добавить поддержку ImGui! Все остальное — исключительно надстройки.
Я было обрадовался, но потом заметил инпут-лаг (причем в Dagon 1.x он тоже есть, хотя и не такой сильный). Когда перетаскиваешь окошко ImGui, видно, что курсор его опережает, хотя, по идее, не должен, если движение синхронизировано со вводом. Это напрямую связано с VSync. Непосредственно в игре задержка ввода не так заметна, как в GUI, но она есть, и в некоторых случаях из-за этого возникает что-то сродни морской болезни. Вокруг этой темы сломано немало копий, холивары о том, зло ли VSync, бытуют еще чуть ли не с 90-х. Иногда, при конфликте с оконным менеджером ОС, включение VSync может привести к еще более неприятному артефакту в виде статтеринга, так что я все же склоняюсь к мысли, что классический VSync больше мешает, чем помогает.
Для решения этой проблемы в современных API придумали Mailbox. Это разновидность тройной буферизации, которая позволяет выводить кадры с минимальной задержкой на синхронизацию с монитором. На моей системе этот режим показывает себя на удивление хорошо, стабилизируя тайминг кадра так, что реакция на ввод срабатывает молниеносно. Естественно, я его добавил в Dagon 2. Он включается опцией vsync: 2 в settings.conf.