Игры в зеркале видеорежимов
Что лучше?
Несколько советов
Литература

Листинг. Фрагмент библиотеки для работы с экраном в режиме 640x400x256


Если вы несколько неудовлетворены видеорежимом 13h и хотите чего-то большего, но не уверены, что сможете обеспечить совместимость программ со всеми существующими видеоадаптерами, а также опасаетесь, что переход к высокому разрешению значительно замедлит скорость вывода на экран, то, возможно, вам подойдет способ программирования видеоадаптера в режиме 640x400x256 цветов, предложенный в работе [1]. Или даже в режиме 800x600x256, описанный в статье [2].

Перед тем как сделать свой выбор, давайте вспомним, с каким разрешением приходилось работать видеоадаптерам IBM-совместимых компьютеров, и попытаемся понять связь между разрешением экрана по вертикали и разрешением по горизонтали. Для этого рассмотрим величину, называемую aspect ratio, представляющую собой отношение размера точки растра по горизонтали к ее размеру по вертикали.

Для различных видеорежимов это отношение может изменяться в довольно широких пределах (см. таблицу).

Свойства экранных режимов

Тип устройства Режим Разрешение Aspect ratio (точек/знакомест)
CGA Графический 320x200 5/6 = 0,83
CGA Графический 640x200 5/12 = 0,42
CGA Текстовый 40x25 5/6 = 0,83
CGA Текстовый 80x25 5/12 = 0,42
MDA Текстовый 80x25 35/54 = 0,65
EGA Графический 640x350 35/48 = 0,73
EGA Текстовый 80x25 35/48 = 0,73
VGA Текстовый 80x25 40/54 = 0,74
VGA Графический 640x480 1/1 = 1
SVGA Графический 640x400 5/6 = 0,83
SVGA Графический 800x600 1/1 = 1
SVGA Графический 1024x5768 1/1 = 1
Нестандартное Графический 320x240 1/1 = 1
Нестандартное Графический 320x400 10/6 = 1,67
Нестандартное Графический 360x480 16/9 = 1,78
Нестандартное Графический 720x350 35/54 = 0,65

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

На ранних этапах развития ПК, когда оптимизировалось использование каждого килобайта видеопамяти (как с точки зрения стоимости видеоадаптера, так и с точки зрения скорости перерисовки изображения), рассматриваемое соотношение было меньше единицы, а иногда даже меньше 1/2. Ранее я уже высказывал предположение, что оптимальным является соотношение, величина которого равна золотому сечению (примерно 0,62), что близко нестандартному режиму 720x350. Однако глубокое перепрограммирование видеоадаптера не входит в наши задачи, так что лучше остановиться на режиме 640x400.

Все современные режимы высокого разрешения имеют aspect ratio, равное 1, что связано главным образом с особенностями Windows, а именно с отсутствием возможности переопределять видеорежим, подбирая оптимальный для каждого приложения. При этом оптимальность приносится в жертву универсальности, да и сама задача подбора в значительной степени утратила актуальность из-за стремительного роста мощности ПК. Однако, несмотря на господство Windows практически во всех областях применения персональных компьютеров, разработчики игр по-прежнему отдают предпочтение DOS, дающей возможность управлять всеми ресурсами компьютера, в том числе и выбором разрешения экрана.

Игры в зеркале видеорежимов

Игры с точки зрения необходимого экранного разрешения можно разделить на четыре группы.

Игры на неподвижном фоне. При оптимальном программировании вполне достижимо разрешение 800x600.

Игры на подвижном фоне. Это большая часть стратегических и аркадных игр с видом со стороны. Разрешение - от 640x400 (для аркадных допустимо и ниже) до 800x600.

Игры со значительной долей анимации (или только анимация). Проблем с выводом на экран нет, могут возникнуть проблемы с местом, отводимым для хранения изображений, скоростями считывания данных с диска и декомпрессии изображения. Рекомендуемое разрешение - не выше 640x480.

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

простейшие, когда все действие происходит на одном уровне, все стены, полы и потолки пересекаются под прямыми углами, все остальные объекты представлены растровыми масштабируемыми картинками. Первая игра подобного рода - Wolfensten3D - работала даже на процессоре 286. Оптимальное разрешение - 640x400 (компьютер как минимум 486 DX2-66 VESA/PCI);

средней сложности, когда пол и потолок всегда горизонтальны, но могут располагаться на различных уровнях, стены всегда вертикальны, но могут пересекаться под произвольным углом, а все остальные объекты представлены растровыми масштабируемыми изображениями. Первая игра такой сложности - Doom. Рекомендуемое разрешение - до 360x240;

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

Для летных имитаторов и других игр, в которых угол крена может отличаться от нуля, вероятно, лучше выбрать разрешение 320x240.

Что лучше?

Итак, каков же итог? Давайте примем, что мы будем рассматривать лишь режимы с 256 цветами. Все рассматриваемые режимы с разрешением 320x200, 320x240, 320x400, 360x480, 640x400, 640x480 и 800x600 можно запрограммировать, используя технику, описанную в работах [1-3].

Режим 320x240, получивший имя собственное ModeX, широко используется в играх и легко получается из хорошо известного 320x200. Он может быть реализован на любом VGA-адаптере, т. е. относится не к SVGA, а к нестандартным VGA-режимам, следовательно, его рассмотрение выходит за рамки настоящей статьи, поэтому мы его непосредственно касаться не будем, но практически все рекомендации, изложенные ниже, применимы и к нему.

Режимы 320x400 и 360x480 могут вызвать, скорее всего, лишь академический интерес, хотя, если как следует задуматься, то можно, наверное, подобрать такие приложения, для которых эти режимы окажутся оптимальными. Разрешение 640x400, на мой взгляд, является наиболее перспективным и обладает следующими преимуществами по сравнению с 640x480x256:

- требуется меньше видеопамяти (256 Кбайт, а не 512 Кбайт);

- осуществляется на 20% меньше вычислений при практически том же качестве (из-за неоптимальной величины aspect ratio при разрешении 640x480 улучшение незаметно);

- отсутствует необходимость в отслеживании межсегментного перехода при 16-разрядном коде, что еще более повышает производительность;

- можно не использовать видеосегмент B000h, что снижает требования к условиям успешной работы программы [2];

- позволяет организовать две страницы видеопамяти (при использовании видеосегмента B000h), а не одну, как при разрешении 640x480;

- обеспечивает частоту регенерации экрана не ниже 70 Гц, тогда как при 640x480 частота может составить только 60 Гц.

Несколько советов

Режим 800x600 может оказаться полезен для стратегических игр на большой карте, а также для игр типа Strip Poke:). Практически все сказанное ниже относится и к нему. Некоторые особенности этого режима описаны в работе [2].

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

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

Внутри графической библиотеки в процедурах работы с объектами также следует избегать вызовов процедуры рисования точки. Все операторы, делающие это, должны находиться непосредственно в теле процедуры. Это, конечно, приведет к многократному дублированию одних и тех же фрагментов кода, но зато существенно повысит скорость работы (см. таблицу в работе [2]).

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

Теневой экран (теневая страница) - часть видеопамяти. Если позволяет объем видеопамяти, то можно организовать несколько страниц, на каждой из которых можно поместить изображение, однако в любой момент времени видимой является только одна, а все остальные - теневые. Любую из таких страниц можно сделать видимой.

Если вы создали теневые страницы, то используйте их только для записи, так как скорость чтения из видеопамяти обычно более чем на порядок ниже скорости чтения из оперативной памяти. При подготовке изображения на экране обычно сначала рисуют фон, а затем поверх него все объекты, переходя от дальних к ближним. Таким образом, некоторые точки прорисовываются по два раза и более. Если коэффициент перекрытия превышает 1,5-2, то предпочтение следует отдать виртуальному экрану, так как скорость записи в видеопамять, как правило, меньше скорости записи в обычную память более чем в 2 раза. Следовательно, работа с теневыми экранами не исключает организации виртуальных.

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

Вы ведь хотите, чтобы программа работала на любом компьютере, а если это невозможно, то ей следует вместо таинственных оптических эффектов выдавать вразумительные сообщения, чередующиеся с извинениями и обещаниями исправить все в следующей версии. Для этого желательно предусмотреть специальную программу настройки, которая тестирует оборудование один раз при установке программы, а не при каждом запуске, и создает специальный файл конфигурации, в котором отражает обнаруженные ею особенности. Что должна делать основная программа при отсутствии файла конфигурации - запускать программу настройки автоматически или выдавать на экран просьбу сделать это вручную - дело ваше. В файл конфигурации целесообразно вписать номер нужного видеорежима, так как при отсутствии поддержки VESA может понадобиться довольно длительное тестирование аппаратуры [1].

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

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

Фрагмент библиотеки для работы с экранами приведен в листинге.

Отображение из виртуального экрана в видеопамять лучше производить последовательно, сегмент за сегментом, операторами пересылки строки (процедура ToVgaScreen). Может быть организовано несколько виртуальных экранов, один из которых является активным. Перед работой процедура проверяет, имеется ли активный экран, а затем пересылает в видеопамять поочередно все сегменты виртуального экрана. Дескрипторы сегментов хранятся в массиве Screens, куда заносятся при отведении памяти для виртуального экрана. Для общности в тексте программы предполагается, что ассемблер "не знает" 32-разрядных команд, а если это не так, то комбинацию db $66 movsw можно заменить на movsd. Естественно, библиотека должна содержать и 16-разрядный вариант этой подпрограммы для видеоадаптеров, не поддерживающих возможность передачи данных двойными словами.

Крупные и неподвижные прямоугольные объекты целесообразно выводить на экран тем же методом, каким происходит пересылка данных между экранами. Для этого изображения должны быть заранее (лучше на стадии написания программы) разбиты на четыре области, каждая из которых соответствует своей плоскости видеопамяти. Мелкие и перемещающиеся изображения, а также изображения со сложной границей (спрайты) лучше выводить по столбцам (процедура PtBl), и, кроме того, они должны таким же образом размещаться и в памяти. Приведенная процедура служит для отображения прямоугольных изображений, изображения сложной формы выводятся сходным образом.

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

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


Литература

1. Андрианов С.А. Как программировать SVGA без головной боли//Мир ПК. 1997. # 5. С. 70.

2. Андрианов С.А. Видеоадаптер: как выйти за предел 256 Кбайт//Мир ПК. 1997. # 9. С. 70.

3. Фролов А.В., Фролов Г.В. Программирование видеоадаптеров CGA, EGA и VGA М.: Диалог-МИФИ, 1992.


Андрианов Сергей Андреевич - к.т.н., тел.: (254) 6-19-62, FIDO: 2:50/430.40@fidonet

Листинг. Фрагмент библиотеки для работы с экраном в режиме 640x400x256

unit HiResLib;            
{Работа с экраном в режимe 640*400*256}
{Позволяет организовать до 4 виртуальных}
{экранов (это 1Mбайт)}
{Все процедуры отображения, кроме ToScreen}
{работают только с активным виртуальным экраном}
interface

Function ScreenInit(var scr:word):boolean;
{Создает новый виртуальный экран, возвратить дескриптор}
Function ScreenDone(scr:word):boolean;
{Удаляет виртуальный экран}
Function ActiveScreen(scr:word):boolean;
{Делает экран активным}
Procedure PutPixel(X,Y:word;Color:byte);
{Рисует точку}
Procedure FillScreen(Color:byte);
{Заполняет экран цветом "Color"}
Procedure ptbl(var massiv:byte;X,Lenght,Y,Height:integer);
{Блок на активный экран}
Procedure ToSCreen(scr1,scr2:word);
{Пересылка виртуальных экранов из scr1 в scr2}
Procedure ToVgaScreen;
{Пересылка из активного экрана на видеоэкран}

implementation
type
   Screen4Type = array[0..63999]of byte;
const
   SegA000 = $a000;
   SeqP    = $3c4;
   NumberScreens:word = 0;
   {Количество открытых виртуальных экранов}
   ScreenPresent: array [0..3] of boolean = (FALSE,FALSE,FALSE,FALSE);
                  {Факт инициализации экранов}
var
   Screens: array [0..3,0..3] of word;
   {Дескрипторы[экранов, сегментов экрана]}
   ScreenAN: integer;{Номер активного экрана}

{Инициализация нового виртуального экрана}
Function ScreenInit(var scr:word):boolean;
{Возвращает номер экрана}
var ptr1:^Screen4Type;  i:integer;
begin
   if (NumberScreens < 4) and
     (Memavail > longint(Sizeof(Screen4Type))*4) then begin
       NumberScreens := NumberScreens + 1; {Число экранов}
         if not ScreenPresent[0] then ScreenAN := 0              {Присвоение}
         else if not ScreenPresent[1] then ScreenAN := 1          {номера}
          else if not ScreenPresent[2] then ScreenAN := 2       {новому}
           else if not ScreenPresent[3] then ScreenAN := 3;   {экрану}
         for i := 0 to 3 do begin   {Отведение памяти}
{под все 4 сегмента}
            new(ptr1);
            Screens[ScreenAN,i] := seg(ptr1^);
         end;
         ScreenPresent[ScreenAN] := TRUE;
         ScreenInit := TRUE;
         Scr := ScreenAN; {Возвращает номер экрана}
      end
   else begin
      ScreenInit := FALSE;
      writeln('Слишком много экранов:',NumberScreens);
   end;
end;

Function ScreenDone(scr:word):boolean;
{Уничтожение виртуального экрана}
var ptr1:^Screen4Type;  i:integer;
begin
   if ScreenPresent[scr] then begin
         NumberScreens := NumberScreens - 1; 
{Число экранов}
         for i := 3 downto 0 do begin
            ptr1 := ptr(Screens[scr,i],0);
            dispose(ptr1);
         end;
         ScreenPresent[ScreenAN] := FALSE;
         ScreenDone := TRUE;
         if ScreenPresent[0] then ScreenAN := 0 
{Делаем другой активным}
          else if ScreenPresent[1] then ScreenAN := 1
            else if ScreenPresent[2] then ScreenAN := 2
              else if ScreenPresent[3] then ScreenAN := 3;
      end
   else
      ScreenDone := FALSE;
end;

Function ActiveScreen(scr:word):boolean;{Сделать экран}
{активным}
begin
   if ScreenPresent[scr] then begin
      ScreenAN := scr;
      ActiveScreen := TRUE;
   end
   else ActiveScreen := FALSE;
end;

{Вывод пиксела с координатами X,Y цветом "Color"}
Procedure PutPixel(X,Y:word;Color:byte);
Begin
  mem[Screens[ScreenAN,X and 3]:Y * 160 + X shr 2] := Color;
End;
{Заполнение активного виртуального экрана цветом "Color"}
Procedure FillScreen(Color:byte);
var i:integer;

Begin
   for i := 0 to 3 do FillChar(mem[Screens[ScreenAN,i]:0],64000,Color);
End;

{Отображение прямоугольной области на активный экран}
Procedure ptbl(var massiv:byte;X,Lenght,Y,Height:integer);
var
   W1:array[0..3]of word; {Дескрипторы активного экрана}
   k,l,l4 : word;
   NachSt : word; {Смещение начала текущего столбца}
   w2 : word absolute w1;
   pw1 : pointer;
const
   w159 : word = 159; {160-1 для спуска на строку}
begin
   if not ScreenPresent[ScreenAN] then begin
      writeln('Экран не определен');
      halt;
   end;
   pw1 := @w1;
   for k := 0 to 3 do w1[k] := Screens[ScreenAN,k];
   l := X;
   l4 := word(Y)*160; {l + (l4 * 4) - координата начала столбца на экране 640x}
   k := Lenght;    {k - оставшееся число столбцов}
   asm
      mov  dx,159     {dx=160-1}
      cld
      les si,massiv   {si - смещение во входном массиве}
  @l1:
      les  bx,pw1     {ds:bx - адрес начала w1}
      mov  ax,l
      and  ax,3
      shl  ax,1       {Смещение от начала w1}
      add  bx,ax      {ds:bx - адрес нужного дескриптора }
      mov  es,es:[bx]  {es   - дескриптор сегмента активного экрана}
      mov  cx,Height   {cx}
      mov  di,l        {l - начало столбца на экране 640x}
      shr  di,2   {di     - начало столбца на экране 160x}
      add  di,l4
      push ds
      lds  ax,massiv  {ds}
  @l2:               {Для цикла нужно : ds,si,es,di,cx,dx}
        movsb
        add di,dx{159}
        loop @l2
      pop ds
      inc  l
      dec k
      jnz @l1
   end;
end;

Procedure ToSCreen(scr1,scr2:word);
{Пересылка виртуальных экранов из scr1 в scr2}
var i:integer; w1,w2:word;
begin
   if (ScreenPresent[scr1] and ScreenPresent[scr2]) then
      for i := 0 to 3 do begin
         w1 := Screens[scr1,i];
         w2 := Screens[scr2,i];
         asm
            push    ds
            mov     ax, w2
            mov     es, ax
            mov     ax, w1
            mov     ds, ax
            xor     si, si
            xor     di, di
            mov     cx, 16000
            rep
              db $66 {Так выглядит инструкция "movsd"}
                movsw
            pop     ds
         end;
      end;
end;

Procedure ToVgaScreen;
{Пересылка из активного экрана на видеоэкран}
var i:integer; w1:word;
begin
   if (ScreenPresent[ScreenAN]) then
      for i := 0 to 3 do begin
         w1 := Screens[ScreenAN,i];
         PortW[SeqP] := 2 + $100 shl  i;
         asm
            push    ds
            mov     ax, SegA000
            mov     es, ax
            mov     ax, w1
            mov     ds, ax
            xor     si, si
            xor     di, di
            mov     cx, 16000
            rep
              db $66   {Так выглядит инструкция "movsd"}
                movsw
            pop     ds
         end;
      end;
end;

END.