суббота, 5 ноября 2011 г.

Машина состояний OpenGL. Часть третья, OGLStateEmul.


Итак, передо мной стояло три проблемы:
  1. Реализовать механизм кеширования;
  2. Обеспечить взаимодействие со сценой во избежании конфликтов;
  3. Решить "Социальную" проблему с ленивыми программистами.
"Социальная" проблема стояла очень остро, так как 100% моего кода было написано на чистом OpenGL, без использования старого механизма работы со стэйтами. Таким образом, что бы я не придумал, мне пришлось бы править весь код, потом еще и обучать других, которые только освоили систему кеширования сцены, своей "новой" системе... Само же кеширование это задача тривиальная, но очень емкая, так как нужно описать логику работы каждой команды ОГЛ, меняющей состояния (и не только, но об этом дальше).



Проблему совместимости со сценой было решено обойти путем сохранения наборов состояний - перед началом рендеринга VBOMesh я делал снимок всех реальных состояний OpenGL через glGet*, процедура хоть и долгая, но учитывая тот факт, что VBOMesh добавлялся первым в список рендера сцены, то очередь рендера была пуста и никакого простоя, вызванного принудительной синхронизацией, не было. Далее вся работа уже велась с кешем в котором хранились реальные значения состояний OpenGL(благо дисплейные списки в модуле не используются и нечему было нарушить работу механизма кеширования), а по завершению работы я возвращал все стэйты в исходное состояние, как было при входе в VBOMesh. Таким образом сцена не замечала подмены и все работало как и было запланировано.

После того как я раскритиковал механизм кеширования сцены, было бы не честно не показать результаты работы своей системы кеширования (да, может быть и хвалюсь)), вот они:
 
[Image]
Здесь представлены снимки трех моментов работы приложения, начиная с создания и до завершения рендеринга первого кадра.
На левом скриншоте показано состояние до входа в процедуру рендеринга VBOMesh. Там присутствует огромное количество вызовов функций ОГЛ (331) при целых 90 командах смены состояний, эффективность которых всего 8%. Для сравнения (чтоб вы не искали предыдущий скриншот), при выводе через glDirectOpenGL было всего 111 команд ОГЛ при тех же 87 командах смены состояний, но с эффективностью 21%, причем то был полный кадр рендера, а я только кусочек показал. Полный же кадр состоит из целых 369 команд ОГЛ при уже 119 командах смены состояний, что явно хуже чем у чистого ОГЛ, но если вспомнить то, что сделала сцена (397/331), то еще не все так плохо :)
Тут появляется внимательный читатель и заднего ряда - "а что же у нас по середине?", а вот это самое интересное :)

Как я уже писал выше, в момент начала рендеринга я делаю полный снимок всех состояний OpenGL, отсюда и такое огромное количество команд и такая низкая эффективность работы кэша, это мы и видим на левом скриншоте, то что на правом скриншоте - это то, что сцена добавила после рендеринга VBOMesh и на что я не мог повлиять, а вот то, что в середине - это и есть работа моего модуля и моей системы кеширования.
Чтоб не напрягать вас вычитанием цифр, вот второй кадр рендера со сброшенной статистикой:
[Image]
На левом скриншоте количество команд на входе в рендер VBOMesh (всего 85 команд вместо 331 и 63 смены состояния вместо 90). Правый кадр - общее количество команд за весь второй кадр рендера, в середине - то что наследил рендер VBOMesh.
Как видите, теперь там всего 27 команд, 21 из которых это установка разных состояний, причем из них 15 - уникальных, что уже составило эффективность 55% (сравните с 15% у сцены). Но что же это за 7 команд таких, что не поддаются кешированию? Не знаю как вас, а меня это заинтересовало :)
Чтоб удовлетворить свое и ваше любопытство я сделал еще один скриншот с закладки "State Change":
[Image]
Как видите - это всего лишь установка вершинных буферов, которые по определению нельзя закешировать :)

Опять крик с заднего ряда - "и чем тут гордиться?", действительно, подумаешь написал кеширование, велика заслуга.
К счастью моя заслуга не в этом, и даже больше - сам модуль кеширования собран на скорую руку, с множеством недоработок и ужасными решениями. Над ним еще предстоит порядком по-потеть, прежде чем он станет тем, чем задумывалось. Но не это предмет гордости, сидящие в первом ряду наверное могли заметить как хитро я обошел третью из названных проблем - "Социальную". Казалось бы - что тут можно поделать, не руковыпрямлялку же прикрутить к модулю...

Я тоже так думал, но воистину лень двигатель прогресса :) Мне так сильно не хотелось править весь код модуля что я нашел очень простое и очень элегантное решение - я просто перекрыл вызовы команд OpenGL своими, реализуется это очень просто, если в делфи в двух модулях объявлены функции с одинаковыми именами, то по умолчанию будут вызываться функции того модуля, который записан последним в списке uses. Таким образом и родился модуль "OGLStateEmul", который всегда следует за OpenGL1x, перекрывая его функции. Таким образом это его первое достоинство, относительно схемы, реализованной в GLScene: Пользователь продолжает ничего не подозревая писать код на чистом OpenGL, все кеширование абсолютно невидимо для него и ему нет необходимости выполнять какие-то дополнительные действия или или изучать новый синтаксис или модифицировать имеющийся код (даже просто записывая вместо glEnable - GL.Enable). Дальше все по обычной схеме:
  - объявляем в OGLStateEmul процедуру с именем соответствующей команды ОГЛ:
          procedure glEnable(cap: TGLEnum);
В этом проявляется второе достоинство описанного подхода: в процессе разработки разработчик сам решает какие функции нужно перекрыть, а обработку каких функций оставить на стандартный конвеер. Это позволяет вести разработку частями, при этом модуль всегда будет сохранять работоспособность, в том числе если появятся новые команды.


 - реализуем описанную процедуру:

procedure glEnable(cap: TGLEnum);
var state: TEnStates;
begin
  state:=StateByEnum(cap);
  if state<>sNone then begin
     if not (state in GLStateCache.States) then begin
        Opengl1x.glEnable(cap);
        GLStateCache.States:=GLStateCache.States+[state];
        exit;
     end else exit;
  end;
  Opengl1x.glEnable(cap);
end;

При этом все работает так же как и в рассмотренном нами ранее примере кеширования в GLScene - ищем константу, эквивалентную константе OpenGL. Причина - в множество (а множество = быстро) нельзя поместить 32битное целое, потому нет возможности сразу делать выборку по константе, так же, в отличии от GLScene, мы на вход получаем не внутреннюю константу движка а целочисленную константу ОГЛ, что приводит нас к необходимости произвести конвертирование командой StateByEnum. Так же это позволяет передать управление неизвестными командами самому OpenGL, но об этом ниже.
Далее мы проверяем знаем ли мы что делать с этой константой, если знаем - проверяем ее состояние и если нужно - обращаясь через "OpenGL1x.XXX" вызываем действительную функцию OGL, в примере это "Opengl1x.glEnable(cap)". Если мы нашили соответствия - значит либо пользователь пытается обратиться к неизвестному нам стэйту (мало ли, вышла версия ОГЛ 10.3 а я об этом не знал), либо кто-то просто ошибся в константе. В этом случае управление передается OpenGL, чтоб он сам решал что ему с этим стэйтом делать. В этом заключается третье достоинство описанного подхода: Какие бы новые команды или стэйты или константы не появлялись бы в OpenGL1x они будут правильно обработаны без какой либо доработки OpenGLStateEmul.

На этом преимущества не заканчиваются, четвертое достоинство в том, что перекрыв любую функцию ОГЛ мы можем изменить логику ее работы, к примеру заменив функции незамедлительного исполнения, такие как glBegin/glEnd на современный аналог, к примеру на VBO, пользователь может ничего не подозревая продолжать писать код с использованием glBegin/glEnd, а на выходе получать всю скорость и преимущества работы с VBO.


Пятое преимущество: Можно вводить пользовательские состояния, к примеру можно определить пользовательскую константу "USER_FRAMEBUFFER0", чтоб когда пользователь писал "glEnable(USER_FRAMEBUFFER0)" автоматически биндился буфер FBO, привязанный к 0-му слоту. Или можно переопределить функцию "glGenTextures(n: TGLsizei; textures: PGLuint; FileName: string)", сделав чтоб "стандартная" команда ОГЛ сама грузила текстур из файла и вызывала "glSetTexture*" и многое другое, на что хватит фантазии и времени.


Шестое преимущество заключается в  возможности изменения логики существующих команд, почему повторяюсь с четвертым пунктом - потому что это открывает новую возможность: Замена устаревшего функционала на новый без переписывания кода движка. К примеру ранее я писал о возможностях "bindless graphics", было бы неплохо использовать это в своем движке, но в настоящий момент это вызовет проблему с совместимостью, но если это завтра появится и у ATI/AMD, то мне будет достаточно лишь переписать логику работы функций VBO. То же самое касается и перехода на OpenGL 3.x - когда придет время я просто заменю команды FFP на работу с шейдерами, оставив весь код движка неизменным.

Ну и седьмое преимущество, являющееся обобщением 4-го и шестого, но  если 4-й пунк предполагал расширение функционала "вширь", 6-й пункт - "вверх", то этот вариант позволит опуститься "вниз", что так же имеет важное практическое значение: Меняя логику программы можно получить адаптивный рендер, который в зависимости от поддерживаемых расширений сам будет подставлять нужные функции. Это больная проблема старого и интегрированного железа, к примеру мне попадались ноутбуки, у которых была заявлена поддержка OpenGL 2.1, но VBO использовалось только через расширения ARB. Казалось бы такая мелочь, только функцию переименовать, но не судьба. Используя перекрытие функций ОГЛ можно автоматически подставлять вызов нужной функции, в зависимости от поддерживаемых расширений.

Вот поэтому я и горжусь этим творением, пускай даже молодым и кривеньким, но с огромным потенциалом :)

7 комментариев:

  1. Сейчас найду OGLStateEmul.pas и утащу куда-нибудь, грязно нарушив копирайты :)

    Ну а если без шуток - залей что ли на гуглокод в ветку branches\exp (например) текущую нестабильную версию модуля - хочу посмотреть твое решение кэша стэйтов, и быть может адаптировать под свои недодвижки (с сохранением "Copyright Fantom", естественно).

    ОтветитьУдалить
  2. Скоро залью, я сейчас попутно готовлю релизную версию нового VBOMesh. Пока можно стащить из раздела демок на форуме сцены/VBOMesh.

    Вообще тут самое главное идея, реализовать кеширование не сложно, просто работа рутинная. Я сам еще не все доделал... В частности не мешало бы как минимум сделать проверку поддерживаемых расширений, я-то все затачивал под ОГЛ 2.1, но даже тут были грабли - оказалось что старые карты ATI не поддерживают расширение ARB_Imaging, которое в ядре примерно с ОГЛ 1.2... И таких моментов может быть много, особенно если использовать ОГЛ 3.3/4.2 да еще и с вендоровскими расширениями...

    Второй момент - давно хочу сделать динамический список кеширования. Сейчас у меня перед началом цикла рендеринга идет чтение всех состояний ОГЛ, а их порядка сотни, в то время, как движок реально использует максимум десятка два из них. Вот и есть мысль на первом кадре рендеринга формировать список "запрошенных" состояний и на втором кадре уже обновлять не все состояния а только те, что попали в этот список, ну и пополнять/обновлять этот список по мере работы программы.

    ОтветитьУдалить
  3. фантом, ты гений!!! я честно когда читал очень удивился решению проблемы!

    ОтветитьУдалить
  4. "Гений" это сильно сказано, просто правильная мысль в правильный момент.

    В большинстве случаев эту "проблему" решают через написания класса "GL: TOpenGL", где в качестве методов выступают все команды ОГЛ. В этом случае можно точно так же менять функционал или кешировать состояния скрыто от пользователя.

    Почему так не сделал - ну во-первых из-за лени, это ж, прежде чем оно начало бы работать, нужно было бы написать обертки над всеми командами ОГЛ... А так - я реализовал кеширование только для нужных мне стэйтов, хотя и там работы было дофига и еще больше осталось. Это же относится и к новым расширениям, достаточно просто обновить заголовочные файлы ОГЛ и можно работать, а в случае использования класса - пришлось бы еще и новые методы описывать. Ну и в таком виде чуть проще копировать код с других демок, так как после копирования нет надобности расставлять везде точки после "GL." :)
    Вообщем решение для лентяев :)

    ОтветитьУдалить
  5. Молодец! Хорошую, нет, глубинную работу провел. Обхитрил компилятор, как только мог))

    P.s. Перед кодом поправь ошибку в предложении: "Это позволяет весь разработку частями, ..."

    ОтветитьУдалить
  6. А такой подход не "поможет" сцене ?

    ОтветитьУдалить
  7. LOLO, нет, не поможет. Я в статье писал почему:
    1. Там уже реализована своя система кеширования и никто ее переделывать не будет (по заявлениям Яра он вообще ничего больше не будет делать в старой сцене, только правка багов).
    2. Сцена завязана на дисплейные списки, это делает попытки кеширования практически бесполезными, так как нет возможности узнать какое состояние установлено внутри дисплейного списка и нет возможности добавить туда проверку.

    ОтветитьУдалить