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

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

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

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

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


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

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

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


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

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

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


person honestann    schedule 27.01.2015    source источник
comment
Если вы не делаете что-то очень простое, вам требуется предварительная обработка (обработка во время выполнения, вероятно, будет использовать больше ЦП, чем она экономит, если только у вас нет простого случая со многими данными) граф объекта вашего кода, чтобы вы знали структуру данных и инструкций и с помощью графика зависимости данных вы можете лучше сгруппировать группы обработки. Говорит, что у вас есть C, которому нужны A и B, вы можете решить вычислить все A, затем все B и, наконец, все C или, возможно, обработать несколько КБ As и B, а затем C в зависимости от них. (потому что у вас есть еще и кэш для инструкций! и еще у вас есть БУС)   -  person CoffeDeveloper    schedule 27.01.2015


Ответы (2)


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

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

person Alex    schedule 27.01.2015
comment
Но разве при таком подходе другим моим потокам не нужно знать, когда они могут вызывать различные функции GLX/OpenGL без блокировки? Похоже, мой код в любом случае не очень эффективен. - person honestann; 27.01.2015
comment
Не используйте вызовы GL в нескольких потоках (вы не повысите производительность и, вероятно, будете глючить в зависимости от реализации драйвера). Только один поток рендеринга должен выполнять все вызовы GL. Тем не менее, другие потоки могут выполнять отсев, анимацию и т. д. для ваших объектов 3D-мира и передавать потоку рендеринга данные, необходимые для каждого вызова отрисовки (например, буферы вершин, шейдеры, юниформ-значения и т. д.). - person Alex; 27.01.2015
comment
SwapBuffers не блокирует. После вызова SwapBuffers только следующий вызов OpenGL, который заставляет ЦП ждать операций с задним буфером, изменяющих его содержимое (таким образом, рисование, но не завершение), будет заблокирован до завершения. - person datenwolf; 27.01.2015

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

Учитывая, что T — это время, которое у вас есть, прежде чем произойдет событие вертикальной синхронизации, вы хотите создать свой кадр, используя 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-кал). Вы по-прежнему можете использовать некоторые дополнительные потоки там, где это необходимо (загрузка файлов или другие тяжелые задачи) для повышения производительности (при необходимости).

person CoffeDeveloper    schedule 27.01.2015
comment
Да, я вижу, как можно использовать преимущества тройной или четверной буферизации, чтобы повысить среднюю производительность и избежать периодических промахов вертикальной синхронизации. Я отверг этот подход, когда начинал этот проект, потому что он увеличивает задержку между человеческим вводом и визуальной реакцией на этот ввод. Однако, поскольку важно, чтобы симуляции и игры работали через Интернет, мне, возможно, придется пересмотреть этот вопрос, хотя я не уверен, каким будет мое окончательное решение. Дело в том, что даже при сверхвысокой пропускной способности интернета задержку нельзя значительно уменьшить... пока. - person honestann; 29.01.2015
comment
Все больше и больше создается впечатление, что авторы драйверов графических процессоров пытаются все скрыть и сделать так, чтобы все выглядело асинхронно для приложений. Например, glXSwapBuffers() не блокируется (хотя очевидно, что по крайней мере glFinish() все еще блокирует). Судя по небольшим подсказкам, я начинаю думать, что у меня, возможно, нет другого выбора, кроме как до некоторой степени доверять разработчикам драйверов графического процессора и просто сосредоточиться на завершении каждого кадра за какое бы то ни было измеренное среднее время кадра (возьмите прошедшее время из 64-часового цикла). бит таймера ЦП на 256 кадров, затем разделить на 256). Затем после этого принимайте решения на основе этого таймера ЦП. - person honestann; 29.01.2015
comment
Меня не интересуют разные процессоры и тому подобные проблемы. Эти проблемы будут автоматически решены путем автоматического отключения функций до тех пор, пока средняя частота кадров не превысит 30 кадров в секунду. Как только набор поддерживаемых функций найден таким образом, с этого момента проблема заключается в том, чтобы узнать, когда определенный кадр имеет больший или меньший спрос, чем в среднем (например, больше или меньше коллизий, с которыми нужно справиться в данном кадре), а затем знать, что, либо выполнять более или менее необязательную работу, которую при необходимости можно отложить на более поздние кадры без ущерба для качества итоговой симуляции или игры. - person honestann; 29.01.2015
comment
О, и да, движок будет постоянно потреблять каждый цикл ЦП на каждом ядре/потоке ЦП, пока не останется ни одного. Я также не беспокоюсь о том, что другие приложения будут вытеснены другими приложениями, потому что приложения, для которых предназначен движок, по своей сути должны быть единственными работающими приложениями (помимо наиболее фундаментальных служб, которые должна выполнять любая ОС). Что я имею в виду под дополнительной работой, которую можно выполнить в этом кадре или когда-нибудь позже? Вычислите и обновите список близких и достаточно массивных объектов, чтобы создать достаточную гравитационную силу для любого объекта, чтобы его можно было вычислять в каждом кадре. - person honestann; 29.01.2015
comment
даже если вы запускаете 1 приложение, операционная система ВСЕГДА прервет ваше приложение. например, на моем компьютере (Win Vista) иногда процесс приостанавливается примерно на 13-15 мс, независимо от того, что это единственное запущенное приложение. Если вы хотите избежать случайных (сотни раз в минуту) пропущенных событий вертикальной синхронизации, у вас нет другого выбора, кроме буферизации. Кстати, вы будете рады узнать, что задержка не воспринимается, потому что драйверы уже делают это, вы просто помогаете им быть более энергоэффективными (используя для симулятора полета) - person CoffeDeveloper; 29.01.2015
comment
в то время как для работы это зависит от вашей кодовой базы, если у вас простой дизайн ECS (система компонентов сущностей), вам просто нужно обновлять определенные системы на более низких частотах (очень часто используется во многих коммерческих играх). Если у вас есть дизайн, основанный на задачах, вы можете просто иметь приоритетную очередь задач (планировщик), где приоритет задачи устанавливается заранее, а приоритет также увеличивает сверхурочное время (поздние задачи). Если у вас нет проблем с отключением определенных функций для достижения 30 кадров в секунду, у вас наверняка не возникнет проблем с реализацией чередующихся обновлений или приоритетной очереди. Кстати, ECS и задачи усложняют отладку некоторых взаимодействий. - person CoffeDeveloper; 29.01.2015
comment
буферизируя, вы просто достаточно заполняете очередь команд графического процессора, прежде чем прекратить это делать на некоторое время. Водители будут делать с этой очередью все, что захотят. Теоретически в этом есть задержка, на практике я этого не замечал. - person CoffeDeveloper; 29.01.2015