Опубликован: 28.04.2009 | Доступ: свободный | Студентов: 1763 / 86 | Оценка: 4.36 / 4.40 | Длительность: 16:40:00
Специальности: Программист
Лекция 3:

Усложненные технологии визуализации

3.3.1. Использование события Idle

Итак, использование обычного компонента Timer не увенчалось успехом из-за ряда ограничений системного таймера Windows. Кроме того, использование таймера для визуализации сцены обладает еще одним фундаментальным ограничением. Дело в том, что, задавая определенную частоту визуализации кадров, мы делаем неявное предположение, что абсолютно любой компьютер может визуализировать сцену с требуемой частотой кадров. Если это вдруг окажется не так, приложение будет работать в "замедленном " режиме. Учитывая, что производительность видеоподсистем различных компьютеров может отличаться в десятки раз, возникновение подобной проблемы более чем вероятно.

Что же делать? Раз визуализация сцены с фиксированной частотой кадров чревата возникновением множества проблем, надо просто визуализировать кадры с максимально возможной частотой, для чего достаточно поместить в обработчик события Idle вызов метода Invalidate. Для измерения временных интервалов между вызовами обработчика события Idle можно воспользоваться свойством System.Enviroment.TickCount, возвращающим количество миллисекунд, прошедших с момента загрузки операционной системы. Основные фрагменты нового варианта кода приведены в листинге 3.19.

// Пример Examples\Ch03\Ex08
public partial class MainForm : Form
{
// Значение свойства System.Enviroment.TickCount 
во время последнего вызова обработчика
// события Idle.
int lastTick;
private void MainForm_Load(object sender, EventArgs e)
{ ...
// Запоминаем текущее значение свойства System.Enviroment.TickCount
 lastTick = Environment.TickCount;
Application.Idle += new EventHandler(Application_Idle);
 }
void Application_Idle(object sender, EventArgs e) 
{ 
// Если приложение завершает работу, закрываем 
главную форму. if (closing) 
{
Close(); return; }
int currentTick = Environment.TickCount; 
// Вычисляем время (в секундах), прошедшее между 
двумя вызовами обработчика Idle float
 delta = (float)(currentTick - lastTick) * 0.001f;
// Изменяем положение диска
posX += speedX * delta; posY += speedY * delta;
// Обрабатываем столкновение с стеной вдоль границы экрана 
...
// Запоминаем текущее время с момента загрузки операционной системы
lastTick = currentTick; 
// Перерисовываем экран
Invalidate(); 
} 
... 
}
Листинг 3.19.

Шарик примера Ch03\Ex08 движется ощутимо плавнее, однако небольшие, еле заметные рывки все же остались. Для определения причины рывков вставьте в обработчик события Idle команду Trace.WriteLine(delta), запустите приложение, вернитесь в Visual Studio и посмотрите содержимое окна Output. Скорее всего, его содержимое будет выглядеть примерно следующим образом:

delta	=	0,078
delta	=	0
delta	=	0
delta	=	0,016
delta	=	0,015
delta	=	0
delta	=	0,016
delta	=	0,015
delta	=	0,016
delta	=	0
delta	=	0,016

Обратите внимание на множество нулевых значений параметра delta, соответствующих полной остановке шарика на месте, которая воспринимается пользователем как небольшое подергивание. Почему между некоторыми вызовами события Idle проходит 15-16 миллисекунд, а между другими – меньше одной миллисекунды. Подобный разброс производительности не выглядит правдоподобным, значит дело в чем-то ином. Для выяснения причин этой аномалии придется обратиться к MSDN. Метод Environment.TickCount использует функцию Win32 GeTickCount, точность которой порядка 10 миллисекунд. Соответственно, при попытке измерить временной интервал близкий к 10 миллисекундам метод Environment.TickCount может вернуть два одинаковых значения, что мы и наблюдаем.

3.3.2. Использование высокоточного таймера Stopwatch

Так как точность измерения времени посредством свойства Environment.TickCount зачастую оказывается недостаточной, Microsoft по многочисленным просьбам трудящихся добавил в .NET Framework 2.0 класс System.Diagnostics.Stopwatch, предназначенный для высокоточного измерения временных интервалов. Данный класс весьма прост в использовании. Сначала приложение должно создать экземпляр класса Stopwatch и запустить таймер методом Start. Затем по мере необходимости приложение посредством свойства ElapsedMilliseconds получает количество миллисекунд, прошедшее с момента запуска таймера. Когда же все измерения времени выполнены, приложение останавливает таймер командой Stop.

Для оценки точности таймера Stopwatch можно воспользоваться статическим свойством Frequency, возвращающее количество тиков высокоточного таймера, используемого классом Stopwatch для измерения временных интервалов, за 1 секунду. На моем компьютере свойство Frequency равно 1.870.000.0000, что соответствует фантастической точности 1/frac18700000000 = 5?10^{-10}сек.

Так как метод ElapsedMilliseconds по определению не может изменять интервалы меньше 1 миллисекунды, в классе Stopwatch имеется один метод ElapsedTicks, возвращающий количество тиков таймера. Для перевода тиков в секунды значение свойства ElapsedTicks надо поделить на Stopwatch.Frequency. При этом следует всегда помнить о том, что при измерении сверхкоротких интервалов появляются иные погрешности, вызванные переключением задач, кэш промахами, длинным конвейером процессора, накладными затратами на вызовы функций и т.п., в результате чего предельная точность измерений таймера Stopwatch редко достижима на практике.

Код приложения, переписанный с использованием таймера Stopwatch приведен в листинге 3.20.

// Пример Examples\Ch03\Ex09
public partial class MainForm : Form
{
// Высокоточный таймер
Stopwatch stopwatch; 
// Время, прошедшее с момента запуска таймера при
 последнем вызове обработчика события
 // Idle
long lastTime;
private void MainForm_Load(object sender, EventArgs e) 
{ 
... 
// Запускаем таймер
stopwatch = new Stopwatch();
stopwatch.Start();
lastTime = 0;
// Выводим в окно Output точность таймера
Trace.WriteLine("Accuracy = " + 1.0 / Stopwatch.Frequency + " sec");
 Application.Idle += new EventHandler(Application_Idle); 
}
void Application_Idle(object sender, EventArgs e) 
{ 
// Если приложение завершает работу, закрываем главную форму. If
 (closing) 
{
Close(); return; }
// Получаем количество миллисекунд, прошедших с 
момента запуска таймера
double currentTime = (double)stopwatch.ElapsedTicks / 
(double)Stopwatch.Frequency;
// Вычисляем время (в секундах), прошедшее между двумя 
вызовами обработчика события Idle
` float delta = (float)(currentTick - lastTick);
// Изменяем положение диска
posX += speedX * delta; posy
 += speedY * delta;
// Обрабатываем столкновение с краями экрана 
...
// Запоминаем текущее показание таймера
lastTime = currentTime; 
// Перерисовываем экран 
Invalidate(); 
} 
... 
}
Листинг 3.20.

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

Accuracy = 5,3475935828877E-10 sec
delta =	0,035
delta =	0,006
delta =	0,003
delta =	0,002
delta =	0,011
delta =	0,012
delta =	0,012
delta =	0,012
delta =	0,011
delta =	0,013
delta =	0,011

Примечание

Обратите внимание на ощутимую задержку при визуализации первого кадра, обусловленную накладными расходами JIT -компилятора при компиляции метода Paint.

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

Accuracy = 5,3475935828877E-10 sec
delta =	0,035
delta =	0,022
delta =	0,013
delta =	0,032
delta =	0,015
delta =	0,032
delta =	0,014
delta =	0,032
delta =	0,016
delta =	0,032
delta =	0,014
delta =	0,016
delta =	0,019

Все дело в вертикальной синхронизации, которая будет рассмотрена в следующем разделе.

3.3.3 Управление вертикальной синхронизацией

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

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

Для борьбы с этим недоразумением XNA Framework предоставляет программисту возможность управления синхронизацией переключения заднего и экранного буферов с моментом окончания формирования электронным лучом изображения на экране монитора. Эта функциональность доступна посредством свойства PresentationInterval класса PresentationParameters:

public PresentInterval PresentationInterval { get; set; }

В подавляющем большинстве случаев свойству PresentationInterval присваивают одно из следующих трех значений перечислимого типа PresentInterval:

  • PresentInterval.Default - значение по умолчанию, аналогичное PresentInterval.One.
  • PresentInterval.One - буферы переключаются только по окончанию обновления электронным лучом изображения на экране монитора.
  • PresentInterval.Immediate - кадровые буферы переключаются так быстро, насколько это возможно.

Хотя значения PresentInterval.Default и PresentInterval.One формально являются братьями-близнецами, между ними есть одна очень тонкая разница. При использовании PresentInterval.Default синхронизации с обратным ходом луча осуществляется посредством обычного таймера Windows, который, как вы помните, обладает очень низкой точностью. Из-за особенностей организации оконной подсистемы Windows низкая точность таймера приводит к частым задержкам при переключении буферов и, соответственно, "рывкам " при анимации. В полноэкранном режиме последствия, как правило, не столь серьезны.

В отличии от PresentInterval.Default, значение PresentInterval.One использует высокоточный таймер, что позволяет избавиться от рывков. Тем не менее, в некоторых случаях незначительные рывки все же могут остаться: так как видеокарта перед переключением кадровых буферов ожидает завершение визуализации текущего кадра, нетрудно догадаться, что при самом не благоприятном стечении обстоятельств частота смены кадров может оказаться в два раза меньше частоты вертикальной развертки монитора. Да и драйверы видеокарты зачастую оказываются не такими совершенными, как хотелось бы. При возникновении подобных проблем пользователь может попробовать отключить вертикальную синхронизацию, так как резкие скачки частоты смены кадров раздражают гораздо сильнее артефактов из-за отсутствия вертикальной синхронизации.

Итак, однозначно правильного режима смены кадров не существует, поэтому было бы разумным предоставить этот выбор пользователю. Для этой цели я поместил на форму диалогового окна Параметры дополнительный флажок Вертикальная синхронизация ( vsynchCheckBox ), установка которого соответствует использованию режима PresentInterval.One, а снятие - PresentInterval.Immediate (рисунок 3.12). Для сохранения значения режима вертикальной синхронизации в настройки приложения было добавлено дополнительное поле PresentationInterval типа Mcrosoft.Xna.Framework.Graphics.PresentInterval, а сам код приложения подвергся косметической доработке (листинг 3.21). Готовое приложение находится в example.zip в каталоге Examples\Ch03\Ex10.

 Диалоговое окно Параметры с новым флажком Вертикальная синхронизация

Рис. 3.12. Диалоговое окно Параметры с новым флажком Вертикальная синхронизация
// Класс главной формы приложения public
 partial class MainForm : Form 
{
void InitGraphivsDevice() 
{ 
...
presentParams = new PresentationParameters();
presentParams.BackBufferCount = 1;
presentParams.SwapEffect = SwapEffect.Discard;
presentParams.IsFullScreen = settings.FullScreen;
// Устанавливаем режим вертикальной синхронизации 
согласно настройкам приложения
presentParams.PresentationInterval = settings.PresentationInterval; 
...
}
 ...
 }
// Класс диалогового окна  "Параметры "
public partial class SettingsForm : Form
{
// Конструктор диалогового окна
internal SettingsForm(Properties.Settings settings, 
GraphicsAdapter adapter) 
{ 
...
// Устанавливает значение флажка  "Вертикальная 
синхронизация " согласно настройкам
 приложения if (settings.PresentationInterval == PresentInterval.One)
vsynchCheckBox.Checked = true;
 else
vsynchCheckBox.Checked = false; 
}
// Обработчик нажатия кнопки Ok
private void okButton_Click(object sender, EventArgs e) 
{ 
...
// Сохраняем в настройках приложения информацию о
 режиме кадровой синхронизации if
 (vsynchCheckBox.Checked)
settings.PresentationInterval = PresentInterval.One;
 else
settings.PresentationInterval = PresentInterval.Immediate; 
...
} 
}
Листинг 3.21.

Запустите приложение и попробуйте поэкспериментировать с настройками кадровой синхронизации, наблюдая за окном Output. При включенной вертикальной синхронизации интервал между визуализацией кадров будет примерно равен константе 1.0/{частота обновления экрана текущего видеорежима}. При отключении вертикальной синхронизации частота интервал смены кадров уменьшится до величины сопоставимой с 1 мс, что соответствует частоте порядка 1000 fps. При этом и в том и в другом случае движения шарика будут плавными.

Андрей Леонов
Андрей Леонов

Reference = add reference, в висуал студия 2010 не могу найти в вкладке Solution Explorer, Microsoft.Xna.Framework. Его нету.

Олег Корсак
Олег Корсак
Латвия, Рига
Александр Петухов
Александр Петухов
Россия, Екатеринбург