Мир ПК, #01/2002
Постоянный адрес статьи:
http://www.osp.ru/pcworld/2002/01/117.htm
Измерение времени и синхронизация
Сергей Андрианов
18.01.2002
Когда программы работают в режиме реального времени, то для них важно,
чтобы скорость исполнения не зависела от производительности компьютера.
Наиболее простой и один из самых эффективных способов добиться этого
— синхронизация с кадровой разверткой (см. «Мир ПК», № 7/01, с. 87),
однако он не универсален. Исключить влияние мощности ПК на скорость
работы программы можно двумя способами: выводить кадры с фиксированной
частотой, а остальное время находиться в ожидании либо выводить кадры
в максимально возможном темпе, а затем измерять их длительность, согласовывая
с нею изменения, происходящие на экране.
Первый способ оправдывает себя тогда, когда изображение выстраивается
быстро, а приемлемую частоту обновления кадров (FPS — frames per second)
обеспечивает даже «слабый» компьютер. Мощные же ПК при работе такой
программы большую часть времени будут, как правило, простаивать, но
для данной цели это несущественно. Второй способ применяется в тех
случаях, когда построение кадра требует значительных вычислительных
ресурсов. При этом для данной программы принимаются такие минимальные
системные требования, при которых частота обновления кадров была бы
приемлемой для просмотра изображения. А высокопроизводительные ПК
будут работать с еще большей частотой обновления, обеспечивая плавное
перемещение объектов по экрану и, следовательно, лучшее восприятие.
Однако особенно увлекаться этим также не стоит, поскольку изменять
содержимое видеопамяти с частотой более высокой, чем способен отобразить
видеоадаптер на экране, вряд ли целесообразно, ведь это не просто
бессмысленно, а даже вредно — могут появиться помехи, ухудшающие изображение.
Поэтому оптимальной была бы линейная зависимость частоты обновления
экрана от мощности ПК при частотах ниже частоты кадровой развертки
и постоянная частота обновления, равная кадровой на более мощных машинах.
Второе уже можно реализовать, а вот для первого нужно хотя бы научиться
измерять время на ПК. Это делается различными способами, в том числе
и с помощью стандартной процедуры определения времени — GetTime, но
она, к сожалению, имеет определенные недостатки:
- работает довольно долго (причины — ниже);
- вместо одной величины выдает сразу четыре, что затрудняет анализ
и сравнение;
- точность измерения не равна единице младшего разряда, что неудобно,
поскольку вводит в заблуждение относительно реального времени и
способствует возникновению дополнительной погрешности;
- в полночь происходит сброс часов в 0;
- невысокая точность измерения.
Чтобы понять, как со всем этим бороться, следует представить, каким
образом происходит отсчет времени в ПК.
В одной из микросхем на системной плате установлен специальный таймер,
представляющий собой генератор опорной частоты 1,19 МГц и двух-трех
программируемых делителей. Поскольку коэффициент деления может изменяться
от 2 до 65 536 (216), то диапазон возможных выходных частот простирается
примерно от 18 Гц до 600 кГц. Выход одного из делителей связан с динамиком
ПК. (В Borland Pascal звук реализуется процедурой sound.) Другой делитель
как раз и используется для измерения времени, причем его выход связан
с контроллером прерываний. Этот делитель запрограммирован на максимально
возможный коэффициент, поэтому период следования импульсов составляет
примерно 0,055 с, а частота будет равна 18,2 Гц. Среди переменных
BIOS есть и отвечающая за текущее время. Она расположена в ОЗУ по
адресу 0040h:006c. Каждое прерывание на единицу увеличивает ее значение,
которое и будет «основным» временем в компьютере. Когда же мы запрашиваем
GetTime, то системная функция берет это значение и вычисляет часы,
минуты, секунды и сотые доли секунд. Естественно, на все преобразования
требуется определенное время, причем величина 0,055 с в сотых долях
точно не выражается и потому округляется до 0,05 или 0,06 с.
В нашем случае было бы логично использовать именно значение переменной
BIOS, поскольку тогда пропадают сразу три из пяти перечисленных недостатков.
Этот способ самый простой и быстрый. Правда, программисту придется
мыслить не в привычных секундах, а в 1/18 ее долях, что, впрочем,
не слишком высокая плата за скорость и удобство. Думаю, будет лучше,
если время станет отсчитываться не от начала суток, а с момента запуска
программы или с другого существенного для нее события, например с
момента загрузки миссии в игре.
Кроме того, счетчик BIOS обнуляется в полночь. Ведь будет неприятно,
если кто-то, допоздна засидевшись за написанной вами игрой, вдруг
«подвесит» свой компьютер только из-за того, что ваша программа, скажем,
ожидает момента времени 24:00:01. Потому-то и предусмотрен в процедуре
переход на новые сутки, ликвидирующий еще один недостаток. Чтобы определить
время, нужно подключить приведенный в листинге 1
модуль директивой uses и использовать функцию Clock, возвращающую
количество 55-миллисекундных интервалов, прошедших с момента запуска
программы. Эта функция работает в десятки раз быстрее, чем GetTime.
В модуль также включена процедура ResetTime, сбрасывающая показания
счетчиков в 0. Кроме того, присутствует процедура GetFPS, позволяющая
измерять количество кадров в секунду, формируемых вашей программой.
Чтобы эта процедура корректно работала, ее надо вызывать один раз
в течение кадра.
Можно продемонстрировать возможности нового модуля, внеся его в директиву
uses основной программы, описав две дополнительные переменные (S:
string {для вывода сообщений на экран}; FPS: single {темп вывода,
кадров в секунду}) и заменив основную программу текстом из листинга 2.
Мы также ввели новую функцию sign, возвращающую знак аргумента. Вообще-то
непонятно, почему разработчики Turbo Pascal ею пренебрегли, ведь она
является парной к abs.
Варьируя значение аргумента процедуры delay, специально введенной
для снижения частоты формирования кадров, можно увидеть, что средняя
скорость перемещения спрайта по экрану не изменяется, хотя при уменьшении
темпа вывода кадров плавность движения исчезает.
К сожалению, предложенный способ определения времени имеет некоторые
недостатки, основной из которых — низкая точность, обусловленная малой
частотой обновления показаний компьютерных часов. С этим можно справиться,
если выполнить одно из следующих действий:
- перепрограммировать частоту прерываний таймера, изменив коэффициент
деления. Однако это гораздо более сложная процедура, чреватая неприятными
последствиями из-за того, что требуется обеспечивать корректное
функционирование системных часов, а кроме того, ее нельзя использовать
во многих многозадачных операционных системах;
- читать регистры таймера, содержащие данные о времени с точностью
более 1 мкс. Однако обычно это делается очень медленно, а во многих
многозадачных операционных системах попросту невозможно;
- воспользоваться командой процессора rdtsc — пожалуй, часто это
наилучший вариант. Недостатки: работает только на процессорах не
ниже Pentium, требует предварительной калибровки и не реализуется
в 16-разрядных программах, в частности созданных при помощи Turbo
Pascal.
Листинг 1
unit timer18;
interface
function Clock : longint;
{время, прошедшее с запуска программы в 1/18 с}
procedure ResetTime; {сброс всех показаний времени в 0}
function GetFPS : single;
{текущее число кадров в секунду}
{необходимо вызывать 1 раз в каждом кадре}
implementation
var
ZeroClock : longint; {время запуска программы}
LastFPSclock : longint; {->- последнего запроса FPS}
Days : longint; {номер дня}
LastClock : longint; {время предыдущего запроса}
NumberFrames : integer; {счетчик кадров}
FPS : single; {число кадров в секунду}
function Clock : longint;
{время, прошедшее с запуска программы в 1/18 с}
var t : longint;
begin
t := meml[Seg0040:$6c] - ZeroClock;
if t < LastClock then begin {переход на след. сутки}
inc(Days);
end;
LastClock := t;
Clock := t + days*1573040; {1 сутки - 1573040}
end;
procedure ResetTime; {сброс всех показаний времени в 0}
begin
ZeroClock := meml[Seg0040:$6c];
LastFPSclock := 0;
NumberFrames := 0;
FPS := 0;
Days := 0;
LastClock := 0;
end;
function GetFPS : single;
{текущее число кадров в секунду}
var
c : longint;
e : single;
begin
inc(NumberFrames);
c := Clock;
if (c - LastFPSclock) >= 18 then begin
{интервал времени измерения не меньше 1 с}
FPS := NumberFrames / (c - LastFPSclock) * 18.2;
NumberFrames := 0;
LastFPSclock := c;
end;
GetFPS := FPS;
end;
begin
ResetTime;
end.
Листинг 2
function sign(a:single):integer;
begin
if a = 0 then
sign := 0
else
if a > 0 then
sign := 1
else
sign := -1;
end;
begin
GetPal(p[0],0,256);
FadeOut(p);
CreateSprite('sprt01.bmp',0,0,1,1);
r.ax := $13; { устанавливаем режим }
intr($10,r); { 320х200х256 цветов }
scr := ptr(SegA000,0);
BlackPal;
PutBackGround; {рисуем фон}
FadeIn(p);
GetBuffer; {сохраняем фон под спрайтом}
PutSprite; {и рисуем на его месте спрайт}
repeat {теперь спрайт будет двигаться по экрану}
{до тех пор, пока мы не нажмем на клавишу}
PutBuffer; {восстанавливаем фон}
FPS := GetFPS;
if FPS > 1 then begin {изменяем приращение}
Sprt.dx := sign(Sprt.dx)*round(70/FPS);
Sprt.dy := sign(Sprt.dy)*round(70/FPS);
end;
CalcSpritePosition;
GetBuffer; {сохраняем фон}
PutSprite; {рисуем спрайт}
inc(TextColor);
SetTextParm(TextColor div 16, (TextColor + 48) div 16,1);
PutText(56,16,'Демонстрационная');
SetTextParm(TextColor and $F,0,0);
PutText(192,16,'программа');
SetTextParm(1,14,1);
str(Clock:3,s);
PutText(128,172,'Time:'+s);
SetTextParm(15,0,1);
str(FPS:0:1,s);
PutText(120,184,' '+s+' fps ');
delay(100); {для регулирования частоты кадров}
WaitVerticalRetrace;
{ожидаем обратный ход луча кадровой развертки}
until keypressed;
readkey; {чистим буфер клавиатуры}
FadeOut(p);
r.ax := $3;
intr($10,r); {возвращаемся в текстовый режим}
DestroySprite;
end.
Мир ПК, #01/2002
Постоянный адрес статьи:
http://www.osp.ru/pcworld/2002/01/117.htm