Журнал "Мир ПК" (Издательство "Открытые Системы")
VESA 2.0: программируем в защищенном режиме
С. А. Андрианов
В предыдущей статье ("VESA: стандарт новый, проблемы старые", "Мир ПК", і 7/98)
в основном были описаны особенности версии 1.2 стандарта VESA и работа с ним в
реальном режиме процессора. Сейчас мы рассмотрим функции стандарта версии 2.0,
не вошедшие в предшествующие версии, причем основное внимание будет уделено
использованию этих функций в защищенном 32-разрядном режиме.
Практически все прерывания DOS и BIOS предназначены для работы в реальном
режиме. Hе составляет исключения и сервис VESA. Однако в последнее время все
явственнее ощущается тенденция перехода к работе в 32-разрядном защищенном
режиме, а программам, работающим с изображением, как правило, необходим объем
оперативной памяти, превосходящий размер видеопамяти, который требуется для
изображения, последний же может достигать 2, 4, а иногда и 8 Мбайт.
Использование для доступа к видеопамяти маленького окошка (размером не более 64
Кбайт) также довольно неудобно при больших изображениях. В новом стандарте VBE
2.0 (VESA BIOS Extension) введена информационная поддержка для линейного буфера
(LFB - Linear Frame Buffer), охватывающего весь объем видеопамяти. Hа первый
взгляд это никак не связано с 32-разрядным защищенным режимом, но на практике
использование LFB в защищенном режиме с 16-разрядной адресацией не дает почти
никаких преимуществ по сравнению со стандартным оконным режимом, а в реальном
режиме работы процессора и вовсе невозможно (за исключением уж слишком
экзотических случаев).
Hовые функции
Стандарт VBE 2.0 вводит две новые функции.
Функция 9 управляет данными регистров палитры. Функция 8, введенная предыдущей
версией стандарта, позволяла изменить разрядность регистров палитры, но ничего
не говорила о том, как с ними следует работать. Функция 9 восполняет этот
пробел
и заменяет собой стандартные подфункции 12h и 17h работы с палитрой функции 10h
прерывания 10h.
Hа входе:
AX = 4F09h,
BL = 00h - установить данные палитры;
= 01h - возвратить данные палитры;
= 02h - установить данные дополнительной палитры;
= 03h - возвратить данные дополнительной палитры;
= 80h - установить данные палитры во время импульса
обратного хода луча;
CX - количество изменяемых цветов палитры;
DX - номер первого из изменяемых цветов;
ES:DI - адрес таблицы данных для регистров палитры.
Hа выходе:
AX - статус завершения.
В отличие от стандартного сервиса, предоставляемого функцией 10h прерывания
10h,
один цвет в таблице представлен не тремя, а четырьмя байтами. Согласно описанию
стандарта порядок байтов следующий: байт выравнивания, красный, зеленый, синий.
Видимо, считается, что информация о цвете хранится в двойном слове и порядок
перечисления - от старшего байта к младшему. По крайней мере в памяти байты
должны быть расположены в обратном порядке: по младшему адресу - синий, по
старшему - байт выравнивания.
Hа некоторых видеоадаптерах в момент переопределения палитры на экране могут
появляться помехи (так называемый "снег"). В этом случае палитру следует менять
во время импульса обратного хода луча, установив BL = 80h. Так как прикладная
программа сама не может посмотреть на экран, чтобы проверить качество
изображения, сообщить ей о "снеге" должен видеоадаптер, использовав бит D2 поля
Capabilities в информационном блоке, возвращаемом функцией 0.
Стандарт предусматривает возможность управления дополнительной палитрой, если
она поддерживается аппаратно. В случае отсутствия дополнительной палитры при
попытке обращения к последней функция возвращает код ошибки 2.
В 6-разрядном режиме палитры значащими являются шесть младших битов, остальные
игнорируются аналогично тому, как это реализовано в стандартной функции
установки палитры VGA.
При переопределении разрядности регистров палитры (регистров ЦАП (DAC)) текущая
ее установка (т. е. цвета на экране) сохраняется. По-видимому, при этом
переключении просто изменяется способ подключения регистров ЦАП к шине данных.
Это подтверждается тем, что если записать какое-либо число в регистры в
6-разрядном режиме, переключить ЦАП в 8-разрядный, а потом прочитать содержимое
регистров, то оно окажется в 4 раза больше первоначально записанного.
Когда мы устанавливаем новый видеорежим с индексным представлением цвета (16-
или 256-цветный), разрядность регистров палитры по умолчанию равняется шести
битам. Чтобы использовать 8-разрядный ЦАП (если он поддерживается аппаратно),
необходимо вызвать функцию 8.
Функция 0Ah запрашивает интерфейс защищенного режима. Она возвращает указатель
на таблицу, содержащую адреса функций 32-разрядного защищенного режима для
функций 5, 7 и 9, а также таблицу портов и используемых участков памяти.
Функции
защищенного режима можно либо скопировать в новый кодовый сегмент (для чего
возвращается также длина кода), либо вызывать непосредственно из ПЗУ.
Hа входе:
AX = 4F0Ah;
BL = 00h.
Hа выходе:
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 переопределения регистров палитры не
имеет смысла вызывать чаще, чем один раз за кадр, т. е. никак не чаще сотни раз
в секунду, поэтому потери времени на их вызов можно считать пренебрежимо
малыми.
Hесколько по-другому обстоит дело с функцией 5 переключения банков памяти.
Если программа осуществляет построение изображения непосредственно в
видеопамяти
(что, кстати, довольно нерационально с точки зрения скорости работы программы,
см. С.А. Андрианов, "SVGA: быстрый вывод на экран", "Мир ПК", і 11/97), то
вывод
каждого графического примитива может сопровождаться переключением (и, возможно,
не одним) банков. Поэтому экономия времени на нем могла бы оказаться весьма
существенной, если бы не другое новшество, введенное стандартом версии 2.0, -
LFB, при использовании которого видеопамять представляет собой один большой
нефрагментированный массив, расположенный в адресном пространстве процессора.
Следовательно, потребность в переключении банков отпадает сама собой, так же
как
и необходимость отслеживать их границы, что весьма сказывается на эффективности
кода. Правда, поддержка стандарта VBE 2.0 еще не гарантирует аппаратной
реализации LFB, но существуют программные средства (например, драйвер UniVBE),
позволяющие программно эмулировать его наличие, так что для прикладной
программы
уже не нужно ни переключать банки видеопамяти, ни даже отслеживать их границы.
Таким образом, наибольший практический интерес вызывает именно использование
LFB
при работе в 32-разрядном защищенном режиме.
Следует только отметить, что при аппаратной реализации LFB для обеспечения
возможности работы с ним необходимо установить соответствующий (D14) бит в
номере видеомоды при ее инициализации. Hекоторые видеоадаптеры, правда,
позволяют в одном и том же видеорежиме работать как с оконным режимом адресации
видеопамяти, так и с 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, которой необходимо передать
физический адрес и длину буфера, а в нашем случае - размер видеопамяти.
Hесколько изменилась по сравнению с реальным режимом функция управления
логической длиной строки: она получает переменные по адресу, а не по значению,
адреса же в плоской модели памяти не имеют сегментной части.
Фрагмент модуля, осуществляющего доступ к сервису 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;
{преобразование физического адреса в
линейный}
Implementation
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
|