|
| |||||||||||||||||
Паскаль для новичков (часть 38)Спрашивали? Отвечаю…
Работаем с графическим режимом 2 (продолжение)Автор: Владислав Демьянишин
![]() Теперь выше сказанное следует подтвердить исходным кодом. Это значит, что подошла очередь блока реализации. Следующие подпрограммы следует заключить в блок включенной директивы дальнего вызова. Данная группа подпрограмм предназначена для преобразования компонент R/G/B в цвет для текущего видеорежима.
Каждая из компонент должна иметь значение в пределах 0..255, так как подпрограммами предполагается, что в каждой компоненте по 8 значащих бит. Если исходные компоненты имеют значения 0..63, то их следует умножить на 4, а уже потом вызывать функцию посредством процедурной переменной RGBToColor.
Первой по очереди идет NilRGBFunc, адрес которой предназначен служить заглушкой для неинициализированной переменой RGBToColor, что позволит без тяжелых последствий вызывать подпрограммы через последнюю. Данная функция всегда будет возвращать нулевой цвет. Подпрограммы RGBTo16Bits и RGBTo24Bits формирует соответственно 16-ти и 24-битный цвет.
implementation
{$F+}
function NilRGBFunc( Red, Green, Blue : byte ): TColor; assembler;
asm
xor dx,dx; xor ax,ax
end;
function RGBTo16Bits( Red, Green, Blue : byte ): TColor;
var Tmp : longint;
begin
Tmp:=(Red shr 3); Tmp:=Tmp shl 11;
RGBTo16Bits := ( Blue shr 3 ) +
(( Green shr 2 ) shl 5 ) + Tmp;
end;
function RGBTo24Bits( Red, Green, Blue : byte ): TColor;
var TmpR, TmpG : longint;
begin
TmpR:=Red; TmpR:=TmpR shl 16;
TmpG:=Green; TmpG:=TmpG shl 8;
RGBTo24Bits := Blue + TmpG + TmpR;
end;
Теперь следует рассказать о доступе к видеопамяти и формировании в ней изображения.
Стандартнейший и самый любимый некогда всеми (и пользователями и программистами) видеорежим 320x200x8бит умещался в пределах видео-окна размером 64000 байт, что позволяло удобно и быстро работать с данными в видеопамяти.
С появлением видеорежимов, при которых размер памяти видео-окна превысил 64Кб, появилась проблема с адресацией видеопамяти, так как под графический видеорежим адаптеру в незапамятные времена был выделен сегмент в 64Кб по адресу $0A000. В свое время эта проблема решалась при помощи расслоения видео-окна на цветовые слои, что требовало для рисования одной точки обращаться сразу к трем ячейкам памяти в каждом из трех слоев. То есть три посылки данных вместо одного.
И совершено естественно, что такой способ был неудобен с точки зрения программирования, и был медлителен.
Для VESA-режимов решили пойти другим путем. А решили поступить следующим образом. Пускай вся память адаптера располагается вне адресного пространства компьютера. Под каждое окно видеорежима BIOS’ом выделяется необходимый размер этой видеопамяти. Для доступа к памяти этого окна используется видеобуфер по адресу $0A000 размером 64Кб, то есть в область видеобуфера аппаратно отображается сегмент аналогичного размера выделенной видеопамяти. Естественно, что в пределах такого маленького видеобуфера могут быть доступны для рисования далеко не все строки окна. А почему бы при необходимости не передвинуть такой видеобуфер по плоскости видеопамяти, допустим на определенное количество килобайт? Нет, конечно, адрес видеобуфера для программиста останется прежним, просто участок видеопамяти, отображенной в нем, будет уже другой, например, следующие 64Кб. Таким образом, буфер сдвинется на 64Кб по видеопамяти. И так далее, пока весь экран не будет обработан.
Допустим, установлен режим 640x400x8бит, тогда размер видео-окна получится 256000 байт, что эквивалентно почти четырем сегментам по 64Кб. То есть, чтобы добраться до самых нижних строк видеоизображения придется переместить видеобуфер почти на 192Кб.
Для перемещения видеобуфера по видеопамяти в VESA BIOS’е предусмотрена функция с номером $4F05, в которой аргумент указывает не на количество килобайт, а на количество страниц, например по 64Кб.
При этом, сдвигая видеобуфер по видеопамяти окна, следует учитывать архитектурные особенности адаптера, так как память адаптера может быть не всегда разделена на 64Кб страницы, по которым можно позиционировать видеобуфер. Например, у многих старых моделей адаптеров память могла быть поделена на странницы по 4Кб. Исходя из этого, чтобы сместить видеобуфер на следующие 64Кб, пришлось бы сдвигать не на одну, а на 16 страниц, то есть 16*4Кб=64Кб. У различных адаптеров размер таких страниц может варьироваться от 4Кб до 64Кб и принимать значения 4Кб, 8Кб, 16Кб, … Такое разделение на страницы решили назвать гранулярностью. А чтобы программист каждый раз не гадал на кофейной гуще, какая же у адаптера гранулярность и каков размер
странниц, в VESA BIOS’е предусмотрена функция с номером $4F01. Последняя возвращает структуру типа TVESAModeInfo с подробной информацией о параметрах видеорежима заданного номера, где есть поле с именем WinGranul, которое указывает минимальный размер страницы, на которую можно переместить видеобуфер.
При установке видеорежима изначально в видеобуфер отображается видеопамять, начиная с нулевой странницы.
Обычно для видеорежима доступны два окна A и B. По умолчанию, через видеобуфер доступно окно A. В данной статье я буду опираться на работу с одним окном A, чтобы не запутать вас окончательно. Я и сам могу запутаться, ведь не такая уж и простая задача все это разложить по полочкам доступным языком, но я постараюсь.
С видеобуфером пока все. Теперь осталось вкратце рассказать, как позиционировать пиксели на экран. Чтобы зажечь на экране точку необходимым цветом, достаточно установить в видеопамяти по соответствующему ей адресу требуемый цвет, то есть значение в 8/16/24 бита. Чтобы вычислить адрес точки с координатами (X;Y) на экране, следует использовать формулу:
адрес=(Y*ширина+X)*цветность,
где ширина – ширина экрана в пикселях, а цветность в битах, деленных на 8. Как видно, формула содержит два умножения, из-за чего будет вычисляться медленно. Ее можно упростить, если ввести такое понятие, как размер скан-линии. Скан-линия – это, по сути, строка изображения во всю ширину экрана. Для удобства при установке видеорежима можно в поле Screen.BytesPerScanline записать размер скан-линии в байтах, вычисляемый по формуле: BytesPerScanline=ширина*цветность или загрузить из поля ModeInfo.BytesPerScanline, где в запись ModeInfo предварительно будут получены характеристики соответствующего режима. Теперь формула будет выглядеть так:
адрес=Y*BytesPerScanline+X*цветность, где произведение X*цветность в последствии можно упростить до элементарного битового сдвига.
Так, для режима 320x200x8бит все весьма просто:
адрес=Y*320+X*8/8=Y*320+X.
И по такому адресу уже можно заносить байт цвета точки. При этом, если координаты точки лежат в пределах (0..319;0..199), то ее адрес всегда лежит в пределах видеобуфера размером в 64Кб и, значит, сдвигать видеобуфер нет необходимости. Следующие две подпрограммы представляют собой оптимизированный код для рисования точек на 8 битный экран 320x200. Процедура PutPixel8BitsVga выводит точку (записывает) цветом Color на экран в позицию координат (X;Y)
procedure PutPixel8BitsVga( X, Y : word; Color : TColor ); assembler;
asm
mov di,x; mov ax,y; shl ax,2 {AX=Y*4}
mov si,ax {SI=Y*4}; shl ax,2 {AX=Y*16}
add ax,si {AX=Y*16+Y*4=Y*(16+4)=Y*20}
add ax,0A000h; mov es,ax; mov al,byte ptr color
mov es:[di],al
end;
Думаю, следует расшифровать данный ассемблерный код. Для оптимальной работы с параметрами величины X и Y загружаются в регистры DI и AX процессора соответственно. Далее необходимо величину Y помножить на 320, то бишь на размер скан-линии. Для умножения можно использовать ассемблерную команду MUL, но поскольку она работает медленно, можно применить ряд манипуляций с битовым сдвигом влево, чтобы осуществить эквивалентное умножение на 320. А чтобы количество таких сдвигов свести к минимуму, можно выполнить вычисление адреса не в байтах, а в 16 байтных параграфах, что потребует умножить Y всего лишь на величину 20, так как 16*20=320. Комментарии в блоке процедуры сами говорят за себя. После выполнения битовых сдвигов в регистре AX будет получен адрес точки в параграфах относительно начала видеобуфера. Добавив к адресу точки адрес видеобуфера командой add ax,0A000h, получим полный адрес точки в параграфах в регистре AX. После чего адрес загружается в сегментный регистр ES. Командой mov al,byte ptr color цвет точки загружается в регистр AL, и точка выводится на экран командой mov es:[di],al.
Безусловно, при работе с изображением может понадобиться функция GetPixel8BitsVga, выполняющая обратное действие, то есть читает цвет точки с координатами (X;Y) с экрана. Вычисление адреса выполняется аналогичным образом. При этом командами xor dx,dx и xor ax,ax для корректности осуществляется инициализация 4-байтного значения цвета, после чего в младший байт AL цвета заносится считываемое значение.
function GetPixel8BitsVga( X, Y : word ): TColor; assembler;
asm
mov di,x; mov si,y; shl si,2; mov ax,si; shl ax,2
add ax,si; add ax,0A000h; mov es,ax; xor dx,dx
xor ax,ax; mov al,es:[di]
end;
Иначе обстоит дело с режимами 640x400, 640x480, 800x600,.., где значение адреса может превысить 64Кб. Как раз тут возникает необходимость вычислять номер страницы, на которую следует позиционировать видеобуфер. Если он уже установлен на требуемую страницу, то перемещать его не нужно. Так, пока видеобуфер находится на нужной странице можно выполнять ряд операций с ее точками и с точками последующих страниц, которые еще умещаются в видеобуфере. Таким образом, для точек, находящихся в пределах видеопамяти, отображенной в видеобуфере, операцию перемещения видеобуфера делать не понадобится.
Как же вычислить номер той самой страницы? Это сделать не очень сложно. Поскольку значение адреса может получиться в пределах типа Longint, то есть 4 байт, то достаточно взять от него старшее слово, в котором будет количество 64Кб-ых сегментов. Затем это количество сдвигаем влево на величину GranulShift, после чего получим номер необходимой страницы, на которую должен быть позиционирован видеобуфер. Что есть GranulShift? GranulShift – это размер битового сдвига влево, эквивалентного умножению на количество страниц в 64Кб-ах. Откуда взялась величина GranulShift? Она получается при помощи вспомогательной функции GetGranulShift каждый раз при установке режима. Стало быть, если гранулярность видеопамяти 64Кб, то размер сдвига будет 0, что приводит очередные 64Кб адреса в соответствие очередной странице в 64Кб. При гранулярности 8Кб размер сдвига будет равен 3, что приводит очередные 64Кб адреса в соответствие очередным 8 страницам (8*8Кб=64Кб).
Итак, для установки 8-битной точки на SVGA-экран составим процедуру PutPixel8BitsVesa:
procedure PutPixel8BitsVesa( X, Y : word; Color : TColor ); assembler;
asm
mov si,X; mov bx,Y; mov ax,Screen.BytesPerScanline
mul bx {mul Y}; add ax,si {add ax,X}; adc dx,0
mov cx,GranulShift; shl dx,cl; mov di,ax
mov ax,SegA000; mov es,ax; cmp dx,LastPage
je @a
mov LastPage,dx; mov ax,4f05h; xor bx,bx; int 10h
@a: mov al,byte ptr Color; mov es:[di],al
end;
где величины X и Y заносятся в регистры SI и BX. Размер скан-линии в регистре AX. В данном случае все же пришлось прибегнуть к применению команды MUL, чтобы Y умножить на размер скан-линии. При этом, физика этого умножения такова, что ей эквивалентна формула DX:AX=AX*BX, то есть умножение двухбайтных регистров AX и BX естественно может привести к переполнению двухбайтного результата, поэтому процессор заносит результат умножения в регистровую пару DX:AX, обеспечивая 32-битный результат. Далее к полученному произведению добавляется X командой add ax,si, что в свою очередь тоже может привести к переполнению регистра AX, который получит результат сложения. В случае переполнения флаг переноса CF установится в единицу. Чтобы при получении полного линейного адреса точки учесть переполнение, далее следует команда adc dx,0, которая, прибавляя нуль к регистру DX адреса точки, также прибавляет единицу флага переноса, если он установлен. Таким образом, оперируя 16-битными регистрами получаем 32-битный адрес точки, где регистр DX содержит количество страниц по 64Кб. Остается лишь умножить его при помощи логического сдвига shl dx,cl, чтобы получить номер искомой страницы видеопамяти, на которую следует позиционировать видеобуфер. Младшие 16 бит адреса точки регистра AX представляют собой смещение в байтах относительно сегмента $0A000 видеобуфера, которые помещаем в регистр DI.
Командой cmp dx,LastPage сравниваем искомую страницу с текущей и, если они не равны, то командой mov LastPage,dx искомую страницу делаем текущей. Командами mov ax,4f05h; xor bx,bx; int 10h заставляем BIOS отобразить в видеобуфере новую странницу видеопамяти экрана. Затем командами mov al,byte ptr Color; mov es:[di],al рисуем точку. Если искомая и текущая страницы равны, то команду вызова функции BIOS пропускаем, переходя по метке @a.
Функция GetPixel8BitsVesa для чтения цвета точки имеет практически тот же код, за исключением концовки, которая содержит команды для инициализации 32-битного цвета точки, и читает значение лишь в младший байт результата.
function GetPixel8BitsVesa( X, Y : word ): TColor; assembler;
asm
mov si,X
…
int 10h
@a: xor dx,dx; xor ax,ax; mov al,es:[di]
end;
Для рисования 16-битной точки процедура PutPixel16BitsVesa имеет аналогичный код, отличаясь лишь тремя командами:
procedure PutPixel16BitsVesa( X, Y : word; Color : TColor ); assembler;
asm
mov si,X; shl si,1 {16 bits/color}; mov bx,Y
…
int 10h
@a: mov ax,word ptr Color; mov es:[di],ax
end;
где команда shl si,1 величину X в пикселях превращает в величину X в байтах, так как размер цвета каждой точки равен двум байтам, а в конце заносит в видеопамять 16-битный цвет точки.
Функция GetPixel16BitsVesa, полагаю, в комментариях не нуждается.
function GetPixel16BitsVesa( X, Y : word ): TColor; assembler;
asm
mov si,X; shl si,1; mov bx,Y
…
int 10h
@a: xor dx,dx; mov ax,es:[di]
end;
Продолжение следует…
© Владислав Демьянишин
Литература3. Interrupt list by Ralf Brawn v.3.3. На нашем сайте можно не только бесплатно скачать игры, но и документацию и книги по программированию на MIDLetPascal, Turbo Pascal 6, Turbo Pascal 7, Borland Pascal, по программированию устройств Sound Blaster, Adlib, VESA BIOS, справочник Norton Guide и много другой полезной информации для программистов, включая примеры решения реальных задач по созданию резидентных программ. Журнал > Программирование > Паскаль для новичков (Turbo Pascal, Assembler) > Паскаль для новичков (часть 38): Работаем с графическим режимом 2 (продолжение)
| ||||||||||||||||||
|
||||||||||||||||||