Порт 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.

Докинг в ImGui

В тулките ImGui меня особенно радует поддержка докинга окон — это просто киллер-фича для создания сложных интерфейсов для различных редакторов, причем использовать ее очень легко, нужно только создать ImGuiIO со включенным докингом (io.ConfigFlags |= ImGuiConfigFlags.DockingEnable), а затем добавить в очередь объект DockSpace поверх вьюпорта:

ImGuiViewport* viewport = cast(ImGuiViewport*)igGetMainViewport();
ImGuiID mainDockSpaceId = igDockSpaceOverViewport(viewport, 
    ImGuiDockNodeFlags.PassthruCentralNode);

Затем можно создавать окна, как обычно — при перетаскивании окна появится виджет для его прикрепления к верхнему, нижнему, правому или левому краю вьюпорта. При наслоении окон друг на друга они отображаются в виде вкладок.

if (igBegin("Settings", null, ImGuiWindowFlags.NoCollapse))
{
    if (igCollapsingHeader("Post-processing"))
    {
        //...
    }
    
    igEnd();
}

Состояние докинга сохраняется в файл imgui.ini, и при повторном запуске приложения ваши окна будут закреплены уже изначально, что очень удобно.

Пример ImGui в Dagon

В Dagon 0.14 появилось расширение для работы с популярным UI-тулкитом ImGui — dagon:imgui. Это у меня уже второй инструмент для создания пользовательских интерфейсов после Nuklear, и во многом ImGui оказался проще и удобнее — хотя, конечно, оба тулкита имеют свои особенности, и нельзя сказать, что один однозначно лучше другого. Но ImGui на сегодняшний день является де-факто стандартом, поэтому его поддержка стала довольно важной вехой в развитии движка. Расширение основано на биндинге BindBC ImGui, модуль bindbc.imgui.ogl (ImGuiOpenGLBackend) оказался полностью совместим с Dagon.

(далее…)

Dagon 0.14.0

Выпустил новую версию движка. Основное нововведение — рефакторинг системы текстур: в частности, текстуры теперь могут быть загружены напрямую, без необходимости создавать объект SuperImage, что позволяет создавать текстуры любого формата, поддерживаемого OpenGL — таким образом, достигается лучшая совместимость с DDS. 2D-текстуры и кубические карты объединены в один класс Texture, и работать с ними стало проще. Появилась поддержка формата сжатия ASTC.

(далее…)

ImGui

В следующей версии Dagon появится экспериментальная интеграция популярного UI-тулкита ImGui на основе биндинга bindbc-imgui — очень интересный инструмент, я только начал его изучать, но уже многое нравится. Удивил большой выбор готовых виджетов.

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