пятница, 14 января 2011 г.

XNA, GarbageCollector, SynchronizeWithVerticalRetrace, IsFixedTimeStep – используйте правила и будет вам счастье …

И так по порядку. Что значат эти термины:

SynchronizeWithVerticalRetrace – public properties GraphicsDeviceManager. Gets or sets a value that indicates whether to sync to the vertical trace (vsync) when presenting the back buffer (MSDN). Эта штука синхронизирует переключение back buffers с частотой обновления экрана монитора. Зная, как технически происходит обновление изображения на мониторе, можно сказать, что включение данного параметра нужно для предотвращения разрезания изображения при смене кадров.

IsFixedTimeStep - public properties Microsoft.Xna.Framework.Game. Gets or sets a value indicating whether to use fixed time steps (MSDN). Параметр, отвечающий за включение или отключение режима, который отвечает за ограничение частоты вызовов метода Update.

GarbageCollector – системный сборщик мусора. Конкретно нас будет интересовать класс System.GC .

Как работают SynchronizeWithVerticalRetrace и IsFixedTimeStep. Есть четыре сочетания данных флагов:
1. SynchronizeWithVerticalRetrace = false и IsFixedTimeStep = false
В этом случае графические буферы меняются по мере их рисования, не дожидаясь синхроимпульса, синхронно вызывается метод Update.
Особенности: обычно в приложении не требуется вызывать метод Update с максимально возможной частотой. При SynchronizeWithVerticalRetrace = false мы имеем возможность измерять значение fps превышающее частоту обновления монитора. Активно пользуюсь при отладке приложения.
2. SynchronizeWithVerticalRetrace = false и IsFixedTimeStep = true
В этом случае мы можем дополнительно контролировать частоту вызова методов Update и Draw и устанавливать фиксированный интервал через свойство TargetElapsedTime.
Особенности: сомнительное удовольствие применять данный подход повсеместно. Он подходит, для каких либо исключительных случаев, в которых его применение полностью обоснованно. Не пользуюсь.
3. SynchronizeWithVerticalRetrace = true и IsFixedTimeStep = false
Тут все просто, обновляем, рендерим, ждем синхронизации, меняем back buffers и по новой.
Особенности: невозможно измерить значение fps выше частоты обновления монитора. Пользуюсь.
4. SynchronizeWithVerticalRetrace = true и IsFixedTimeStep = true
Интересный случай. Если время синхронизации меньше IsFixedTimeStep, то ждем следующей синхронизации и только после этого выполняется Update и Draw. Если же время синхронизации больше IsFixedTimeStep, то по факту выполнения синхронизации не медленно выполняется Update и Draw. Эдакий хромающий ослик без одной ноги).
Особенности: теряюсь в догадках где это можно применить, точно не мой профиль). В некоторых случаях при значениях TargetElapsedTime/ vsyncTime = 1.5f (примерно) можно на глаз заметить не равномерное обновления экрана. Не пользуюсь.

Уже теплее. И так, о главном). Если вы написали приложение|игру используя XNA, применили из вышеописанных приемов 1й или 3й и в вашем приложении наблюдаются периодические задержки, а вы к тому же нечайно вспомнили «слова которые нельзя произносить» - Garbage Collector ))). Главное не паникуйте, при этом не обязательно писать в различных форумах Ваше мнение о .net и XNA, крутости и преимуществах C++. Все это от нервов, нервы от не знания, не знание от лени, лень от глупости, но это лечится.

Как пользуется Garbage Collector’ом рядовой «пользователь» Visual Studio? Да ни как. Он полагается на настройки по умолчанию для Garbage Collector’а. Настройки по умолчанию рассчитаны на так называемые бизнес приложения, для динамичной работы с DirectX нужно доработать.

По умолчанию Garbage Collector выполняет освобождение памяти в любом случае не синхронно с работой Update. Мертвый груз копится и освобождается в непредсказуемый, для логики вашего приложения, момент. Самый простой выход – это делать в начале очередного Update:
System.GC.Collect(1);
Где 1 это номер поколения, значение подобрано экспериментально.

С другой стороны может быть не целесообразно перегибать палку и выполнять сборку мусора 60 раз в секунду при включенной синхронизации (если у монитора такая рабочая частота обновления экрана) или более 60 раз при выключенной синхронизации.
По этому можно сделать так:

using System;
using System.ComponentModel;
using Microsoft.Xna.Framework;

namespace GeneralPresentationFoundation.gSystem.gGarbageCollector
{
    public class GarbageCollector
    {

        private BackgroundWorker bw = new BackgroundWorker();

        private int stepNumber = 0;

        private int sleepStep = 1;
        public  int SleepStep { get { return sleepStep; } set { sleepStep = value; } }

        public GarbageCollector()
        {
            Init();
        }

        public GarbageCollector(int sleepStep)
        {
            this.sleepStep = sleepStep;
            Init();
        }

        private void Init()
        {
            bw.DoWork += new DoWorkEventHandler(bw_DoWork);
        }

        public void Update()
        {
            if(!bw.IsBusy)
            {
                stepNumber++;
                if(stepNumber == sleepStep)
                {
                    stepNumber = 0;
                    bw.RunWorkerAsync();
                }
            }
        }

        private void bw_DoWork(Object sender, EventArgs e)
        {
            GC.Collect(1);
        }
    }
}

Возможно не везде будет полезным применение потока, это просто мой случай.
GarbageCollector gc = new GarbageCollector(20);
т.е. при частоте монитора 60 Гц - 3 раза в секунду.