В предыдущей статье ("VESA: стандарт новый, проблемы старые", "Мир ПК", № 7/98) в основном были описаны особенности версии 1.2 стандарта VESA и работа с ним в реальном режиме процессора. Сейчас мы рассмотрим функции стандарта версии 2.0, не вошедшие в предшествующие версии, причем основное внимание будет уделено использованию этих функций в защищенном 32-разрядном режиме.

Практически все прерывания DOS и BIOS предназначены для работы в реальном режиме. Не составляет исключения и сервис VESA. Однако в последнее время все явственнее ощущается тенденция перехода к работе в 32-разрядном защищенном режиме, а программам, работающим с изображением, как правило, необходим объем оперативной памяти, превосходящий размер видеопамяти, который требуется для изображения, последний же может достигать 2, 4, а иногда и 8 Мбайт. Использование для доступа к видеопамяти маленького окошка (размером не более 64 Кбайт) также довольно неудобно при больших изображениях. В новом стандарте VBE 2.0 (VESA BIOS Extension) введена информационная поддержка для линейного буфера (LFB - Linear Frame Buffer), охватывающего весь объем видеопамяти. На первый взгляд это никак не связано с 32-разрядным защищенным режимом, но на практике использование LFB в защищенном режиме с 16-разрядной адресацией не дает почти никаких преимуществ по сравнению со стандартным оконным режимом, а в реальном режиме работы процессора и вовсе невозможно (за исключением уж слишком экзотических случаев).

Новые функции

Стандарт VBE 2.0 вводит две новые функции.

Функция 9 управляет данными регистров палитры. Функция 8, введенная предыдущей версией стандарта, позволяла изменить разрядность регистров палитры, но ничего не говорила о том, как с ними следует работать. Функция 9 восполняет этот пробел и заменяет собой стандартные подфункции 12h и 17h работы с палитрой функции 10h прерывания 10h.

На входе:
AX = 4F09h,
BL = 00h - установить данные палитры;
     = 01h - возвратить данные палитры;
     = 02h - установить данные дополнительной палитры;
     = 03h - возвратить данные дополнительной палитры;
     = 80h - установить данные палитры во время импульса
обратного хода луча;
CX - количество изменяемых цветов палитры;
DX - номер первого из изменяемых цветов;
ES:DI - адрес таблицы данных для регистров палитры.
На выходе:
AX - статус завершения.

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

На некоторых видеоадаптерах в момент переопределения палитры на экране могут появляться помехи (так называемый "снег"). В этом случае палитру следует менять во время импульса обратного хода луча, установив BL = 80h. Так как прикладная программа сама не может посмотреть на экран, чтобы проверить качество изображения, сообщить ей о "снеге" должен видеоадаптер, использовав бит D2 поля Capabilities в информационном блоке, возвращаемом функцией 0.

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

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

При переопределении разрядности регистров палитры (регистров ЦАП (DAC)) текущая ее установка (т. е. цвета на экране) сохраняется. По-видимому, при этом переключении просто изменяется способ подключения регистров ЦАП к шине данных. Это подтверждается тем, что если записать какое-либо число в регистры в 6-разрядном режиме, переключить ЦАП в 8-разрядный, а потом прочитать содержимое регистров, то оно окажется в 4 раза больше первоначально записанного.

Когда мы устанавливаем новый видеорежим с индексным представлением цвета (16- или 256-цветный), разрядность регистров палитры по умолчанию равняется шести битам. Чтобы использовать 8-разрядный ЦАП (если он поддерживается аппаратно), необходимо вызвать функцию 8.

Функция 0Ah запрашивает интерфейс защищенного режима. Она возвращает указатель на таблицу, содержащую адреса функций 32-разрядного защищенного режима для функций 5, 7 и 9, а также таблицу портов и используемых участков памяти. Функции защищенного режима можно либо скопировать в новый кодовый сегмент (для чего возвращается также длина кода), либо вызывать непосредственно из ПЗУ.

На входе:
AX = 4F0Ah;
BL = 00h.
На выходе:
AX - статус завершения;
ES - сегмент таблицы в адресации реального режима;
DI - смещение таблицы;
CX - длина таблицы, включая длину кода.

Формат таблицы следующий:

ES : DI + 00h - смещение точки входа функции 5;
ES : DI + 02h - смещение точки входа функции 7;
ES : DI + 04h - смещение точки входа функции 9;
ES : DI + 06h - смещение таблицы портов и участков памяти. 

Все смещения даются относительно адреса начала таблицы.

Следует отметить, что формат параметров функции 7 защищенного режима несколько отличается от такового для реального режима. При вызове 32-разрядной функции в регистре CX следует передавать младшее слово полного 32-разрядного смещения от начала видеопамяти, а в DX - старшее.

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

Если программа осуществляет построение изображения непосредственно в видеопамяти (что, кстати, довольно нерационально с точки зрения скорости работы программы, см. С.А. Андрианов, "SVGA: быстрый вывод на экран", "Мир ПК", № 11/97), то вывод каждого графического примитива может сопровождаться переключением (и, возможно, не одним) банков. Поэтому экономия времени на нем могла бы оказаться весьма существенной, если бы не другое новшество, введенное стандартом версии 2.0, - LFB, при использовании которого видеопамять представляет собой один большой нефрагментированный массив, расположенный в адресном пространстве процессора. Следовательно, потребность в переключении банков отпадает сама собой, так же как и необходимость отслеживать их границы, что весьма сказывается на эффективности кода. Правда, поддержка стандарта VBE 2.0 еще не гарантирует аппаратной реализации LFB, но существуют программные средства (например, драйвер UniVBE), позволяющие программно эмулировать его наличие, так что для прикладной программы уже не нужно ни переключать банки видеопамяти, ни даже отслеживать их границы.

Таким образом, наибольший практический интерес вызывает именно использование LFB при работе в 32-разрядном защищенном режиме.

Следует только отметить, что при аппаратной реализации LFB для обеспечения возможности работы с ним необходимо установить соответствующий (D14) бит в номере видеомоды при ее инициализации. Некоторые видеоадаптеры, правда, позволяют в одном и том же видеорежиме работать как с оконным режимом адресации видеопамяти, так и с LFB.

Пример программы

В качестве примера приведен вариант программы, которая была опубликована в упомянутой в преамбуле статье, переписанный для защищенного 32-разрядного режима процессора. Для отладки использовался транслятор TMT Pascal, свободно распространяемую версию которого можно найти на узле http://www.tmt.com или ftp.tmt.com.

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

Листинг 1. Процедуры выделения и освобождения нижней памяти

unit low_mem;
interface
procedure GetLowMem(var LowSeg,LowSel:word;var Len:dword);
                                   {выделение буфера в
нижней памяти}
procedure FreeLowMem(LowSel:word); {возвращение нижней памяти
в систему}
implementation
procedure GetLowMem(var LowSeg,LowSel:word;var Len:dword);
   {выделение буфера в нижней памяти}
   {LowSeg - сегмент адреса буфера реального режима}
   {LowSel - селектор адреса буфера защищенного режима}
   {Len - длина запрашиваемого буфера}
var j:word;
begin
   j := (len + 15) div 16; {длина блока в параграфах}
   asm
      push ebx
      push edx
      mov ax,$0100
      mov bx,j
      int $31       {запрашиваем память для буфера}
{      rcl flagCF,1     {запоминаем CF}
      mov edi,LowSel
      mov [edi], dx    {сохраняем селектор}
      mov edi,LowSeg
      mov [edi], ax    {сохраняем сегмент}
      shl ebx,4
      mov edi,Len
      mov [edi],ebx
      pop edx
      pop ebx
   end;
end;

procedure FreeLowMem(LowSel:word); {возвращение нижней памяти
в систему}
begin
   asm
      push edx
      mov ax,$0101
      mov dx,LowSel
      int $31
      pop edx
   end;
end;
end.

В процедуре выделения памяти отсутствует проверка на ошибку. Если такая проверка необходима, следует "раскомментировать" строку, содержащую rcl, и описать соответствующую переменную.

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

Листинг 2. Прерывание с использованием адреса в сегментных регистрах

unit dos_int;
interface
type
   dosseg = record
      ESSeg : word;    {сюда помещается содержимое регистра ES}
      DSSeg : word;    {а сюда - DS}
   end;
var
   segs : dosseg;

procedure DOSint(IntN:byte);  {IntN - номер вызываемого прерывания}
implementation
procedure DOSint(IntN:byte); assembler;
asm
        push    dword ptr 0            {вместо  SS, SP}
        lea     esp,[esp - 8]          {пропускаем  CS, IP ,FS, GS}
        push    segs                   {DS и ES}
        pushf
        pushad
        mov     edi,esp
        mov     ax,0300h
        xor     cx,cx
        movzx   ebx,IntN               {номер прерывания}
        int     31h                    {эмуляция прерывания DOS}
        popad
        popf
        pop     segs                   {DS и ES}
        lea     esp,[esp+12]           {пропускаем SS,SP,CS,IP,FS,GS}
end;
end.

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

Многие DOS-экстендеры (программы, позволяющие использовать 32-разрядную адресацию при работе в DOS) придерживаются линейной модели памяти, при которой вся нижняя память имеет адреса, совпадающие с реальным режимом. Однако это не означает, что линейная адресация памяти полностью совпадает с физической. Следовательно, чтобы воспользоваться физическим адресом LFB в своей программе, следует предварительно включить его в общую линейную адресацию, осуществляемую DOS-экстендером. Для этого служит функция LinAddr, которой необходимо передать физический адрес и длину буфера, а в нашем случае - размер видеопамяти.

Несколько изменилась по сравнению с реальным режимом функция управления логической длиной строки: она получает переменные по адресу, а не по значению, адреса же в плоской модели памяти не имеют сегментной части.

Фрагмент модуля, осуществляющего доступ к сервису VESA, приведен в листинге 3.

Листинг 3. Доступ к сервису VESA

unit vesa_as; {сервис VESA, вариант TMT Pascal}
Interface
type
   CType = array[0..255]of char;
   CPtr  = ^CType;
   WType = array[0..255]of word;
   WPtr  = ^WType;
   VesaInfoBlock = record
      VESASignature : array[0..3]of char; {"VESA"}
      VESAVersion   : word;  {номер версии VESA}
      OEMStringPtr  : CPtr; {указатель на строку с названием производителя (OEM) }
      Capabilities  : dword; {флаги графических возможностей}
      VideoModePtr  : WPtr; {указатель на список поддерживаемых видеорежимов}
      TotalMemory   : word;  {количество видеопамяти в 64-килобайтных блоках}
      Reserved      : array[0..235]of byte;
{зарезервировано}
   END;
   ModeInfoBlock = record
        ModeAttributes      : word;  {+00 - атрибуты видеорежима}
        WinAAttributes      : byte;  {+02 - атрибуты окна A}
        WinBAttributes      : byte;  {+03 - атрибуты окна B}
        WinGranularity      : word;  {+04 - величина granularity}
        WinSize             : word;  {+06 - размер окна}
        WinASegment         : word;  {+08 - начальный сегмент окна A}
        WinBSegment         : word;  {+10 - начальный сегмент окна B}
        WinFuncPtr          : pointer; {+12 - указатель на оконные функции}
        BytesPerScanLine    : word;  {+16 - количество байтов в строке растра}
        XResolution         : word;  {+18 - горизонтальное разрешение}
        YResolution         : word;  {+20 - вертикальное разрешение}
        XCharSize           : byte;  {+22 - ширина знакоместа}
        YCharSize           : byte;  {+23 - высота знакоместа}
        NumberOfPlanes      : byte;  {+24 - количество плоскостей видеопамяти}
        BitsPerPixel        : byte;  {+25 - количество бит на точку}
        NumberOfBanks       : byte;  {+26 - количество банков}
        MemoryModel         : byte;  {+27 - тип модели памяти}
        BankSize            : byte;  {+28 - размер банка в Кбайт}
        NumberOfImagePages  : byte;  {+29 - количество экранных страниц}
        ReservedPage        : byte;  {+30 - зарезервировано для оконных функций}
        RedMaskSize         : byte;  {+31 - глубина красного цвета в битах 
                          (для режима с непосредственным представлением цвета)}
        RedFieldPosition    : byte;  {+32 - смещение маски для красного цвета}
        GreenMaskSize       : byte;  {+33 - глубина зеленого цвета в битах}
        GreenFieldPosition  : byte;  {+34 - смещение маски для зеленого цвета}
        BlueMaskSize        : byte;  {+35 - глубина синего цвета в битах}
        BlueFieldPosition   : byte;  {+36 - смещение маски для зеленого цвета}
        RsvdMaskSize        : byte;  {+37 - зарезервировано для глубины цвета}
        RsvdFieldPosition   : byte;  {+38 - зарезервировано  для смещения маски цвета}
        DirectColorModeInfo : byte;  {+39 - атрибуты режима с непосредственным представлением цвета}
        PhysBasePtr         : dword; {+40 - физический  адрес линейного буфера (LFB)}
        OffScreenMemOffset  : pointer; {+44 - указатель на свободную часть видеопамяти}
        OffScreenMemSize    : word;  {+48 - размер свободной части видеопамяти в Кбайт}
        Reserved            : array[0..205]of byte;
{+50 - зарезервировано}
   END;

function GetVESAInfo(var Buffer:VesaInfoBlock):boolean; {информация о VESA}
function GetModeInfo(Mode:word;Buffer:pointer):boolean; {информация о моде}
function SetVESAMode(Mode:word):boolean; {установка видеомоды}
function SetVESALenLine(var PLength,BLength,NLines:dword):boolean;  
                                  {установка логической длины линии растра}
function SetVESAStart(XStart,YStart:word):boolean; 
                                  {управление положением экранного окна в видеопамяти}
function LinAddr(PhysAddr:dword;SizeBlock:dword) : dword; 
                                  {преобразование физического адреса в линейный}
Impleentation
uses low_mem,dos_int;

function GetVESAInfo(var Buffer:VesaInfoBlock):boolean; {информация о VESA}
var
   Seg,Sel : word;    {переменные для селектора и сегмента временного буфера}
   RetCode : word;    {переменная для статуса завершения прерывания}
   SizeBl : dword;    {длина запрашиваемого блока}
begin
   SizeBl := 256;
   GetLowMem(Seg,Sel,SizeBl);  {выделяем временный буфер в нижней памяти}
   segs.ESSeg := Seg;
   asm
      push edi
      mov eax,$4f00
      mov edi,0
      push dword ptr $10
      call DosInt          {получаем информацию во временный буфер}
      mov RetCode,ax
      pop edi
   end;
   if RetCode = $004F then begin
      move(Mem[Seg*16],Buffer,256);  {копируем информацию из временного буфера}
      GetVesaInfo := TRUE;
      with buffer do begin
         VideoModePtr := pointer(((dword(VideoModePtr) and
$FFFF0000) shr 12) + (dword(VideoModePtr) and $FFFF));
         OemStringPtr := pointer(((dword(OemStringPtr) and
$FFFF0000) shr 12) + (dword(OemStringPtr) and $FFFF));
      end;
   end else begin
      writeln("GetVesaInfo Error RetCode=",RetCode);
      GetVesaInfo := FALSE;
   end;
   FreeLowMem(Sel);  {уничтожаем временный буфер}
end;

function GetModeInfo(Mode:word;Buffer:pointer):boolean;
{информация о моде}
var
   Seg,Sel : word;    {переменные для селектора и сегмента временного буфера}
   RetCode : word;    {переменная для статуса завершения прерывания}
   SizeBl : dword;    {длина запрашиваемого блока}
begin
   SizeBl := 256;
   GetLowMem(Seg,Sel,SizeBl);  {выделяем временный буфер в нижней памяти}
   segs.ESSeg := Seg;
   asm
      push ecx
      push edi
      mov eax,$4f01
      mov cx,mode
      mov edi,0
      push dword ptr $10
      call DosInt          {получаем информацию во временный буфер}
      mov RetCode,ax
      pop edi
      pop ecx
   end;
   if RetCode = $004F then begin
      move(Mem[Seg*16],Buffer^,256);  {копируем информацию из временного буфера}
      GetModeInfo := TRUE;
   end else GetModeInfo := FALSE;
   FreeLowMem(Sel);  {уничтожаем временный буфер}
end;
function SetVESALenLine(var PLength,BLength,NLines:dword):boolean;
    {установка логической длины линии растра}
    {Plength - длина строки в точках растра}
    {Blength - длина строки в байтах}
    {Nlines  - максимальный номер строки}
var RetCode:word;
begin
   asm
      push di
      push bx
      push cx
      push dx
      mov ax,$4f06
      mov edi,Plength
      mov cx,[edi]
      xor bx,bx
      int $10
      mov edi,Plength
      mov [edi],cx
      mov edi,Blength
      mov [edi],bx
      mov edi,Nlines
      mov [edi],dx
      mov RetCode,ax
      pop dx
      pop cx
      pop bx
      pop di
   end;
   SetVESALenLine := RetCode = $004f;
end;
function LinAddr(PhysAddr:dword;SizeBlock:dword) : dword;
   {преобразование физического адреса в линейный}
   {PhysAddr - физический адрес}
   {SizeBlock - длина блока}
var
   LinAddr2:dword;
begin
   if PhysAddr > $100000 then begin
      asm
         push ebx
         push ecx
         mov cx,word ptr PhysAddr
         mov bx,word ptr PhysAddr+2
         mov di,word ptr SizeBlock
         mov si,word ptr SizeBlock+2
         mov ax,$800
         int $31
         mov word ptr LinAddr2,cx
         mov word ptr LinAddr2+2,bx
         pop ecx
         pop ebx
      end;
      LinAddr := LinAddr2;
   end else LinAddr := $FFFFFFFF;
end;

end.

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

Листинг 4. Проверка модуля vesa_as

program tves_as4;
uses vesa_as,crt;
var
   LenLineP:dword;  {длина строки растра в точках}
   LenLineB:dword;  {длина строки растра в байтах}
   MaxLines:dword;  {максимальное число строк растра}
   LFBPtr : dword; {адрес начала LFB}
procedure putpixel(X,Y,Color:dword); {вывод точки на экран}
begin
   asm
      mov ebx,Y
      imul ebx,LenLineB
      add ebx,X
      mov eax,Color
      add ebx,LFBPtr
      mov [ebx],al
   end;
end;
Procedure WaitRetrace;  {ожидание вертикального обратного хода луча}
Begin
   While(Port[$3DA]and $08)=0 do;
End;
var
   i,j:dword;          {переменные цикла}
   b1:VesaInfoBlock;  {информационный блок VESA (для функции 0)}
   b2:ModeInfoBlock;  {информационный блок видеомоды (для функции 1)}
begin
{выясняем наличие VESA и выводим основные параметры}
   if GetVesaInfo(b1) then begin
      for i := 0 to 3 do write(b1.VesaSignature[i]);
      write(", Ver ",hi(b1.VESAVersion),".",lo(b1.VESAVersion));
      writeln(", ",b1.TotalMemory*64,"Kb videomemory on
 board ");
      i := 0;
      while b1.OEMStringPtr^[i] <> #0 do begin
         write(b1.OEMStringPtr^[i]);
         inc(i);
      end;
      writeln;
      i := 0;
      writeln("Modes:");
      while b1.VideoModePtr^[i] <> $FFFF do begin
         write(b1.VideoModePtr^[i]," "); {список видеомод}
         inc(i);
      end;
      writeln;
   end else writeln("Error VesaInfo");
{получаем характеристики одной из видеомод}
   if GetModeInfo($4103,@b2) then begin
      writeln("Mode 4103h, Granularity:",b2.WinGranularity,"Kb, Window Size:",
          b2.winsize,"Kb,
",b2.XResolution,"x",b2.YResolution,", ",b2.BitsPerPixel,
          " bits per pixel");
      if (b2.ModeAttributes and $81) = $81 then begin
         LFBPtr := LinAddr(b2.PhysBasePtr,b1.TotalMemory*65536);
      end else begin
         writeln("LFB not supported");
         halt;
      end;
   end else writeln("Error ModeInfo");
   readkey;
{устанавливаем видеомоду, характеристики которой мы получили}
   if SetVesaMode($4103) then begin
      LenLineB := b2.BytesPerScanLine;
{закрашиваем каждый 64-килобайтный сегмент своим цветом}
      for i := 0 to b1.TotalMemory-1 do
fillchar(mem[LFBPtr+i*65536],65536,char(i+1));
   end else writeln("Error");
   readkey;
{поточечно рисуем диагональную многоцветную полосу}
   for i := 0 to 199 do
      for j := 0 to 599 do PutPixel(j+i,j,j);
   readkey;
   LenLineP := 1024;
{пытаемся изменить логическую длину строки}
   SetVESALenLine(LenLineP,LenLineB,MaxLines);
   readkey;
{снова рисуем полосу}
   for i := 0 to 199 do
      for j := 0 to 1023 do PutPixel(j+i,j,j);
   readkey;
{производим панорамирование ...}
   for i := 0 to (1023-800) do begin
      SetVESAStart(i,0);{}
      WaitRetrace;
   end;
   readkey;
{... и скроллинг экрана}
   for j := 0 to (1023-600) do begin
      SetVESAStart(i,j);
      WaitRetrace;
   end;
   readkey;
{возвращаем видеоадаптер в текстовый режим}
   asm
      mov ax,3
      int $10
   end;
end.

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

ОБ АВТОРЕ: Андрианов Сергей Андреевич - канд. техн. наук, e-mail: andriano@divo.ru, Fidonet: 2:50/435.40