VESA 2.0: программируем в защищенном режиме
С. А. АндриановВ предыдущей статье ("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; {преобразование физического адреса в линейный} 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