08 октября 2011

Утилитарный класс для вывода FPS + Memory. Этап 1: начало...

При разработке игровых приложений всегда необходимо знать, какую производительность выдает программа и есть ли запас для добавления тех или иных новшеств в игру. Мерилом скорости работы игровой программы обычно выступает количество кадров в секунду, которые отображаются на экране монитора игрока. На английском - Frames Per Second, сокращенно FPS, или же ФПС, если говорить транслитизацией.
Контролировать производительность игры стоит на всех этапах разработки и для каждого из них допустимым будет свое значение ФПС. Например, в финальной версии игры, где экран полон эффектов, игровых объектов, фоновых изображений, программа может выдавать и 40-50 кадров в секунду, что является довольно адекватным  значением для комфортной игры. Но вначале разработки значение должно быть на порядок, а то и два порядка выше, ведь в кадре не так много элементов, которые нужно выводить. При этом необходимо понимать, что в игру добавятся логические конструкции, звуки, скрипты, менюшки, физика, действия самого игрока и много-много чего еще! Поэтому следить за значением ФПС необходимо всегда, и сегодня мы как раз будем делать небольшую примочку для отображения основных данных о работе программы в real-time режиме.

Постановка задачи
Итак, недавно я поставил себе цель - сделать простой вспомогательный класс для отображения текущего ФПС и графика его изменения со временем. Дело оказалось довольно интересным и занимательным, требующим познаний в работе GlScene, поэтому я решил разбить создание класса на несколько сообщений, чтобы сразу же записывать все мысли и подводные камни. То есть в текущий момент итоговый класс и модуль не завершены и я буду создавать вот такие отчеты, погружая читателя в процесс создания отдельной "примочки". Надеюсь будет интересно :)

Реферанс
Вообще, за эталон я взял As3Profiler, выглядищий вот так:


Пара графиков, несколько чисел. И обновление всего этого добра каждую секунду. Нажал кнопку Show - показалась панелька, нажал Hide - спряталась. С одной стороны не загромождаем экран, с другой - в любой момент можем мониторить скачки ФПС.

Код

Так давайте же программировать! Как всегда унаследуем наш класс от TGLHudSprite. Для определения ФПС перекроем методы DoRender() и DoProgress(), в первом будем увеличивать счетчик количества отрисованных кадров, во втором - считать пройденное время. Поделив первое значение на второе, узнаем корректное значение ФПС. Стоит учесть, что при этом необходимо отключить VSync, иначе мы можем упереться в потолок, заданный монитором.
Итак, наш класс TGlSceneProfiler на данный момент имеет следующее объявление:

TGlSceneProfiler = class (TGLHudSprite)
  protected
    fPointsCount: Integer;
    fFPSGraph: TSimpleGraph;
    fLastFPS: Single;
    fFramesCount: Integer;
    fFPSTime: Single;
    fKeyPressed: Boolean;
    
    fVisible: Boolean;
    Procedure SetVisible(aValue: Boolean);
    Procedure RenderLines(var ARci: TRenderContextInfo; aWidth, aHeight: integer);
  public
    Procedure DoRender(var ARci: TRenderContextInfo; ARenderSelf, ARenderChildren: Boolean); override;
    Procedure DoProgress(const progressTime: TProgressTimes); override;
    Constructor Create(AOwner: TComponent); override;
  end;

Заглянем внутрь DoProgress():

Procedure TGlSceneProfiler.DoProgress(const progressTime: TProgressTimes);
begin
  // увеличиваем счетчик времени
  fFPSTime := fFPSTime + progressTime.deltaTime;
  // каждую секунду нужно расчитывать ФПС, добавляя новое значение на график
  if (fFPSTime > 1) then
  begin
    // по определению "количество кадров в секунду"
    fLastFPS := fFramesCount / fFPSTime;
    fFramesCount := 0;
    fFPSTime := 0;
    // добавляем новое значение на график
    fFPSGraph.Step(fLastFPS);
  end;

  //находим, нажата ли кнопка F1
  NewKeyPressed := IsKeyDown(VK_F1);
  //если кнопку только что нажали - меняем состояние панели
  if NewKeyPressed and not fKeyPressed then
    SetVisible(not fVisible);
  //запоминаем нажатость кнопки F1
    fKeyPressed := NewKeyPressed;
end;

Комментарии встроены в код, поэтому понять смысл каждой строчки нетрудно... По сути мы просто считаем время и обновляем график каждую секунду, добавляя на него новую точку с текущим значением ФПС.

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

Класс TSimpleGraph
Как понятно из названия, данный класс необходим для построения простых графиков зависимости некоторой величины от времени. Для наглядности приведу описание TSimpleGraph:

  TSimpleGraph = class
    fX0, fY0: Single;
    fScaleX, fScaleY: Single;
    fPointsCount: Integer;
    fPoints: array of Single;
    Function GetX(aXValue: Single): Single;
    Function GetY(aYValue: Single): Single;
    Function GetMaxValue: Single;
    Procedure Step(aNewValue: Single);
  public
    property Count: Integer read fPointsCount;
    Procedure RenderGraph(aCanvas: TGlCanvas);
    Procedure SetPoint(aIndex: Integer; aValue: Single);
    Function GetPoint(aIndex: Integer): Single;
    Constructor Create(const aPointsCount: Integer);
  end;

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

Function TSimpleGraph.GetX(aXValue: Single): Single;
begin
  //сдвигаем на fX0 и масштабируем в fScaleX раз
  result := fX0 + aXValue * fScaleX;
end; 

Так как экранная ось OY направлена вниз, то в формуле для координаты Y появляется знак минус, отвечающий за переворот данной оси:

Function TSimpleGraph.GetY(aYValue: Single): Single;
begin
  result := fY0 - aYValue * fScaleY;
end;

Пока класс сырой, но пользоваться в базовом варианте можно. Последовательность такая:

  //создаем экземпляр, указывая количество точек на графике
  fFPSGraph := TSimpleGraph.Create(60);
..................
  //добавляем новую точку на график, при этом все остальные смещаются на одну позицию
  fFPSGraph.Step(fLastFPS);
..................
  //рисуем график на GL-канве
  fFPSGraph.RenderGraph(glc);

Также имеется метод GetMaxValue(), возвращающий пиковое значение графика, необходимое для нормирования графика. В принципе, базовые вещи рассказал. Возможно, стоит сделать дополнительную отдельную демку с построением графика для более подробного описания возможностей класса TSimpleGraph.


Использование
Лично я обожаю хорошо спроектированные классы, использование которых прозрачно, функционально и очень удобно. Сам я пока проектировать такое не умею, но буду учиться и стараться!
Поэтому я решил попробовать сделать модуль, который было бы удобно использовать всем. Показалось разумным создать синглтон fGlSceneProfiler, который создается и инициализируется вызовом функции:

Function InitGlSceneProfiler(aParent: TGLBaseSceneObject): TGlSceneProfiler;

Все просто! Вызвали в любом месте программы InitGlSceneProfiler() и можем смело наблюдать за колебаниями ФПС в программе.
А так как сам класс TGlSceneProfiler является потомком TGLHudSprite, то наш объект-синглтон должен уничтожиться при освобождении графа сцены и нам заботиться о финализационной секции нет необходимости, что очень радует :)

Промежуточный итог
Итак, сейчас по нажатию кнопки F1 срабатывает показ/скрытие панели с отображаемым графиком текущего ФПС. Выглядит это чудо так:


Кх-м-м-м... Да, цвет подобрал не лучшим образом, надо будет поработать над этим...

Взгляд в будущее
Теперь осталось добавить всякие цифры, подписи, немного вычистить код, оттюнинговать цвета и размеры - и можно смело добавить заветную строку InitGlSceneProfiler() во все мои текущие демонстрационные приложения!
Также хочется вставить второй график, отображающий использование оперативной памяти компьютера, но все это - в следующих этапах разработки маленького, но очень полезного класса TGlSceneProfiler!

Добавление данной утилиты попало на 13ую ревизию. Так отложим суеверия в сторону и постараемся во что бы то ни стало доделать данный класс!

Сообщения, схожие по тематике:

4 коммент.:

  1. Молодец!

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

    ОтветитьУдалить
  2. Как раз вчера думал о графиках в реальном времени для мониторинга фпс и памяти, использованной приложением. Может пригодится при написании ландшафта с многопоточной подгрузкой данных.

    ОтветитьУдалить
  3. perfect daemon,
    благодарю!
    по поводу пригодности - на первом скрине (Реферанс) видны четкие просаживания ФПС при освобождении памяти флешкой. как бы делаем вывод, что необходим переход на аккумуляторы и т.д.
    в сцене же, согласен, сам график, возможно, не даст большой информации, а вот текущее значение ФПС пригодится. хотя, по графику можно быстро сделать сравнение производительности в разных уровнях игры/комнатах/техниках...


    Марцелл,
    Супер! С тобой мы вообще зачастую в одном направлении мыслим... Очень радует!
    Возможно, подскажешь какие-то тонкости, которые ты хотел реализовать - можно сделать вместе, чтобы не повторять одну и ту же работу каждому по отдельности...

    ОтветитьУдалить
  4. Lampogolovii, я бы, наверное, сделал универсальный класс для отображения 2д графика, которому передавалась бы ссылка на Data source (можно унаследовать от CadenceableObject, вроде так называется)этого графика. Таким образом можно полностью разнести рендеринг и просчет. В Data source данные можно хранить, как очередь.

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