Игровой движок без блокировки с полным разделением обновлений и рендеринга

Я заранее извиняюсь за этот длинный пост, но, как вы, наверное, видите, я думал об этом в течение довольно долгого времени, и я чувствую, что мне нужно некоторое мнение других людей, прежде чем моя голова взорвется :-)

Я уже некоторое время экспериментирую с различными способами создания игрового движка, удовлетворяющего всем следующим критериям:

  • Полное разделение обновления объекта и рендеринга объекта
  • Полный детерминизм
  • Обновление и рендеринг на индивидуальных скоростях
  • Нет блокировки на общих ресурсах

Полное разделение обновления объектов и рендеринга объектов

Разделение обновления объектов и рендеринга объектов кажется жизненно важным для обеспечения оптимального использования ресурсов при отправке данных в графический API и подкачке буферов. Даже если вы хотите обеспечить полный параллелизм для использования нескольких ядер ЦП, кажется, что этим разделением все равно нужно управлять.

Полный детерминизм

Многие типы игр, и особенно многопользовательские версии, должны обеспечивать полную детерминированность. В противном случае игроки будут испытывать разные состояния одной и той же игры, нарушая игровую логику. Детерминизм требуется и для игровых повторов. И это полезно для других целей, где важно, чтобы каждый запуск симуляции давал один и тот же результат каждый раз при одинаковых начальных условиях и входных данных.

Обновление и рендеринг с разной скоростью

Это действительно необходимое условие для полной детерминированности, поскольку симуляция не может зависеть от скорости рендеринга (например, различных частот обновления монитора, скорости графического адаптера и т. д.). В оптимальных условиях скорость обновления должна быть установлена ​​с определенным фиксированным интервалом (например, 25 обновлений в секунду — возможно, меньше в зависимости от типа обновления), а скорость рендеринга должна быть такой, какая позволяет частота обновления монитора клиента/графический адаптер.

Это означает, что должна быть разрешена скорость рендеринга выше скорости обновления. И хотя это звучит как пустая трата времени, существуют известные приемы, гарантирующие, что добавленные циклы рендеринга не будут пустой тратой (интерполяция/экстраполяция), что означает, что более быстрые мониторы/адаптеры будут вознаграждены более визуально приятным опытом, как и должны.

Тем не менее, скорость рендеринга ниже скорости обновления также должна быть разрешена, даже если это действительно приводит к напрасным тратам циклов обновления — по крайней мере, не все добавленные циклы обновления представлены пользователю. Однако это необходимо для обеспечения плавного многопользовательского режима, даже если рендеринг в одном из клиентов по той или иной причине резко замедляется.

Без блокировки общих ресурсов

Если другие критерии, упомянутые выше, должны быть реализованы, это также должно следовать тому, что мы не можем позволить рендерингу ожидать обновления или наоборот. Конечно, до боли очевидно, что когда 2 разных потока совместно используют доступ к ресурсам, и один поток обновляет некоторые из этих ресурсов, невозможно гарантировать, что блокировка никогда не произойдет. Однако можно свести эту блокировку к абсолютному минимуму — например, при переключении ссылок указателя между очередью обновленного объекта и очередью ранее визуализированных объектов.

So...

Мой вопрос ко всем присутствующим здесь опытным людям: не слишком ли много я прошу?

Я читал об идеях этих различных тем на многих сайтах. Но всегда кажется, что та или иная часть упущена из предложений, которые я видел. И, возможно, причина в том, что вы не можете иметь все это без компромиссов.

Я начал этот, казалось бы, обычный квест давным-давно, когда излагал свои мысли об этом в этой теме: Мысли о стратегиях цикла рендеринга

В то время моим первым наивным предположением было то, что не должно иметь значения, происходят ли обновление и чтение одновременно, поскольку это состояние объекта вариаций было настолько маленьким, что вы не должны были бы замечать, если бы один объект иногда был на шаг впереди другого.

Теперь я немного поумнел, но все еще временами сбит с толку.

Наиболее перспективным и подробным описанием метода, позволяющего реализовать все мои пожелания, было следующее: http://blog.slapware.eu/game-engine/programming/multithreaded-renderloop-part1/ Модель с тремя состояниями, которая гарантирует, что средство визуализации всегда может выбрать новую очередь. для рендеринга без ожидания (за исключением, возможно, микросекунды при переключении указателей). В то же время апдейтер всегда может получить доступ к 2 очередям, необходимым для построения следующего дерева состояний (1 очередь для создания/обновления следующего состояния и 1 очередь для чтения предыдущего - что можно сделать, даже когда рендерер читает его как хорошо).

Недавно я нашел время, чтобы сделать пример реализации этого, и он работает очень хорошо, но есть две проблемы.

  • Одна из них — незначительная проблема, связанная с необходимостью иметь дело с несколькими ссылками на все задействованные объекты.
  • Другой более серьезный (если только я не слишком нуждаюсь). И это тот факт, что экстраполяция — в отличие от интраполяции — используется для поддержания визуально приятного представления состояний при высокой частоте обновления экрана. Хотя оба метода выполняют работу по отображению состояний, отклоняющихся от твердо рассчитанных состояний объекта, мне кажется, что экстраполяция дает гораздо более заметные артефакты, когда предсказания не отражают реальность. Моя позиция, похоже, подтверждается этим: physics/snapshots-and-interpolation/ И, насколько я могу судить, невозможно реализовать интерполяцию в дизайне с тремя состояниями, поскольку для этого требуется, чтобы рендерер всегда имел доступ на чтение к 2 очередям, чтобы вычислить промежуточное состояние между двумя известными состояниями.

Поэтому я играл с расширением модели с тремя состояниями, предложенной в блоге slapware, чтобы использовать интерполяцию вместо экстраполяции - и в то же время попытаться упростить структуру с несколькими ссылками. Хотя мне кажется, что это возможно, мне интересно, не слишком ли высока цена. Для достижения всех моих целей мне нужно иметь

  • 2 очереди (или состояния), находящиеся исключительно в рендерере (они могут использоваться другим потоком только для чтения, но никогда не обновляются и не переключаются во время рендеринга
  • 1 очередь (или состояние) с последним обновленным состоянием, готовым к переключению на средство визуализации, когда оно завершит рендеринг текущей сцены.
  • 1 очередь (или состояние) со следующим кадром, создаваемым/обновляемым средством обновления
  • 1 очередь (или состояние), содержащая копию последнего построенного/обновленного фрейма. Это то же самое состояние, которое в последний раз было отправлено средству визуализации, поэтому эта очередь/состояние должна быть доступна как средству обновления для чтения предыдущего состояния, так и средству визуализации для визуализации состояния.

Таким образом, это означает, что я должен постоянно хранить 4 копии состояний рендеринга, чтобы этот дизайн работал плавно, без блокировки и детерминированно.

Я боюсь, что я слишком много думаю об этом. Так что, если кто-то из вас посоветует вернуть меня на землю или посоветует, что можно улучшить, критика дизайна или, возможно, ссылки на хорошие ресурсы, объясняющие, как эти цели могут быть достигнуты, или почему это так или не так. хорошая идея - пожалуйста, поразите меня ими :-)


person uhre    schedule 03.10.2015    source источник
comment
Почему бы тебе не попробовать RCU? Шаги: 1. В каждом кадре обновления программа обновления создает защищенную RCU копию всех соответствующих состояний для средства визуализации и передает ей указатель. 2. В каждом кадре рендерера рендерер берет указатель в критической секции RCU, делает копию соответствующего состояния, выходит из критической секции и делает все, что делает рендерер.   -  person EOF    schedule 03.10.2015
comment
Итак, для этого потребуется сделать 2 копии всех состояний? Это может быть слишком дорого в системе, которая предназначена для однократной записи и многократного чтения. В любом случае, эта функция «выстрелил и забыл» определенно означала бы, что мне не нужно было бы поддерживать сеть ссылок на объекты, и это, безусловно, одна из моих целей. Однако шаблон RCU кажется более подходящим для систем, где объекты считываются чаще.   -  person uhre    schedule 04.10.2015
comment
Да, RCU вероятно избыточен, если только вы не выполняете рендеринг в несколько потоков. Если у вас есть только один поток для обновлений и рендеринга, вы можете вместо этого сделать одну копию и передать атомарный указатель ссылок.   -  person EOF    schedule 04.10.2015