Как вычислить в игровом цикле до последнего возможного момента

В рамках оптимизации своего движка 3D-игр / симуляторов я пытаюсь сделать движок самооптимизирующимся.

По сути, мой план таков. Во-первых, запустите движок для измерения количества циклов ЦП на кадр. Затем измерьте, сколько циклов ЦП потребляют различные подсистемы (минимальное, среднее, максимальное).

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

Идея состоит в том, чтобы как можно больше опережать игру при выполнении кропотливой работы, чтобы каждый возможный цикл ЦП был доступен для обработки "требовательных кадров" (например, "много коллизий во время одного кадра"), которые можно было обработать, не вызывая glXSwapBuffers() вовремя обменять задние / передние буферы до самого последнего возможного момента для vsync).


Приведенный выше анализ предполагает замену задних / передних буферов, что является фундаментальным требованием для обеспечения постоянной частоты кадров. Я видел утверждения, что это не единственный подход, но я не понимаю логику.

Я запечатлел тактовые циклы 64-битного ЦП непосредственно перед и после glXSwapBuffers(), и обнаружил, что кадры меняются примерно на 2 000 000 тактов! Похоже, это связано с тем, что glXSwapBuffers() не блокируется до vsync (когда он может обмениваться буферами), а вместо этого немедленно возвращается.

Затем я добавил glFinish() непосредственно перед glXSwapBuffers(), что уменьшило вариацию примерно до 100 000 тактовых циклов ЦП... но затем glFinish() заблокировался в диапазоне от 100 000 до 900 000 тактовых тактов ЦП (предположительно, в зависимости от того, сколько работает драйвер nvidia пришлось завершить, прежде чем он мог поменять буферы). С такими различиями в том, сколько времени может занять glXSwapBuffers() для завершения обработки и замены буферов, мне интересно, есть ли надежда у любого "умного подхода".


Суть в том, что я не уверен, как достичь своей цели, которая кажется довольно простой и, кажется, не требует слишком много базовых подсистем (например, драйвера OpenGL). Тем не менее, я все еще вижу около 160000 циклов изменения "времени кадра", даже с glFinish() непосредственно перед glXSwapBuffers(). Я могу усреднить измеренные частоты "тактовых циклов ЦП на кадр" и предположить, что среднее значение дает фактическую частоту кадров, но при таком значительном изменении мои вычисления могут фактически заставить мой двигатель пропускать кадры, ошибочно предполагая, что это может зависеть от этих значений.

Я буду признателен за понимание специфики различных задействованных функций GLX/OpenGL или общих подходов, которые на практике могут работать лучше, чем то, что я пытаюсь.

PS: тактовая частота моего процессора не меняется при замедлении или ускорении ядер. Поэтому это не источник моей проблемы.

2 ответа

Это мой совет: в конце рендеринга просто вызовите функцию буфера подкачки и, если необходимо, разрешите ее блокировать. На самом деле у вас должен быть поток, который выполняет все ваши вызовы API OpenGL, и только это. Если нужно выполнить другое вычисление (например, физику, игровую логику), используйте другие потоки, и операционная система позволит этим потокам работать, пока поток рендеринга ожидает vsync.

Кроме того, если некоторые люди отключают vsync, они хотели бы увидеть, сколько кадров в секунду они могут достичь. Но с вашим подходом кажется, что отключение vsync просто позволит fps около 60 в любом случае.

Я попытаюсь переосмыслить вашу проблему (чтобы, если я что-то пропустил, вы могли мне сказать, и я мог бы обновить ответ):

Учитывая, что T - это время, которое у вас есть до того, как произойдет событие Vsync, вы хотите сделать свой кадр, используя 1xT секунд (или что-то около 1).

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

0.96xT

0.84xT

0.99xT


Вы должны иметь дело с некоторыми фактами:

  1. Вы не знаете T (вы пытались это проверить, и кажется, что это икота: это зависит от водителей!)
  2. Сроки имеют ошибки
  3. Различные архитектуры ЦП: вы измеряете циклы ЦП для функции, но на другом ЦП эта функция требует меньше или больше циклов из-за лучшего / худшего предпочтения или конвейерной обработки.
  4. Даже при работе на том же процессоре другая задача может загрязнить алгоритм предпочтения, поэтому одна и та же функция не обязательно приводит к одинаковым циклам ЦП (зависит от функций, вызываемых ранее, и алгоритма предпочтения!)
  5. Оперативная система может помешать в любое время, приостановив ваше приложение для запуска некоторого фонового процесса, который увеличит время ваших "заполняющих" задач, эффективно заставляя вас пропустить событие Vsync (даже если ваше "предсказанное" время разумно, например, 0,85xT)

В некоторых случаях вы все еще можете получить время

1.3xT

в то же время вы не использовали всю возможную мощность ЦП (когда вы пропускаете событие Vsync, вы в основном тратите впустую время кадра, что приводит к потере мощности ЦП)


Вы все еще можете обойтись;)

Буферизация фреймов: вы сохраняете рендеринг вызовов до 2/3 фреймов (не более! Вы уже добавляете некоторую задержку, а некоторые драйверы графического процессора будут делать то же самое для улучшения параллелизма и снижения энергопотребления!), После чего вы используете игровой цикл для простаивать или делать поздние работы.

При таком подходе разумно превышать 1xT. потому что у вас есть несколько "буферных кадров".

Давайте посмотрим на простой пример

  • Вы запланировали задачи для 0,95xT, но поскольку программа работает на компьютере с другим ЦП, чем тот, который вы использовали для разработки программы, из-за другой архитектуры, ваш кадр занимает 1,3xT.
  • Нет проблем, вы знаете, что за кадром позади, поэтому вы все равно можете быть довольны, но теперь вам нужно запустить задачу 1xT - 0.3xT, лучше использовать также некоторый запас безопасности, чтобы запускать задачи для 0.6xT вместо 0.7xT.
  • Что-то действительно пошло не так, кадр снова занял 1,3xT, теперь вы исчерпали свой запас кадров, вы просто делаете простое обновление и отправляете вызовы GL, ваша программа предсказывает 0,4xT
  • Удивительно, что ваша программа заняла 0,3xT для следующих кадров, даже если вы запланировали работу более 2xT, у вас снова 3 кадра в очереди в потоке рендеринга.
  • Поскольку у вас есть несколько кадров, а также есть поздние работы, вы запланируете обновление для 1,5xT

Вводя небольшую задержку, вы можете использовать полную мощность процессора, конечно, если вы измеряете, что в большинстве случаев в вашей очереди буферизуется более 2 кадров, вы можете просто сократить пул до 2 вместо 3, чтобы сэкономить некоторую задержку.


Конечно, это предполагает, что вы выполняете всю работу синхронно (за исключением отсрочки GL). При необходимости вы можете использовать дополнительные потоки (загрузка файлов или другие сложные задачи) для повышения производительности (если требуется).

Другие вопросы по тегам