在第六章里我们已经说过,屏幕上显示的内容与视频缓冲存储器内的数据具有单一的对应关系。在字符模式下屏幕上某个位置显示出的一个字符与视频缓存内特定地址存放的ASCII码与属性字节是相对应的。修改视频缓存中某个字节的内容,屏幕上相应位置的字符就会随之改变,或者变成其它字符,也可能改变了颜色。
图形模式下也是这样,但在图形模式下这种对应关系"细化"了,不再是"字符--字节"之间的对应关系,而是变成"点--位"之间相对应。近一步说,是屏幕上的一个点与视频缓存中某个字节的某几个位相对应。
为了搞清点与位之间的对应关系,我们还是要把DEBUG请出来。首先,我们先编个小程序将显示模式设置为CGA模式4:
C:\ASM\>DEBUG[Enter] -a100[Enter] 0E6A:0100 mov ax,0004 0E6A:0103 int 10 0E6A:0105 int 20 0E6A:0107 -g=100
键入G命令后屏幕被置成图形模式,此时我们即可修改视频缓冲区了,还记得我们前面讨论过的"E"命令吗?
在开始改变视频缓冲区之前,我们需要先将屏幕上乱糟糟的文字清除掉,免得影响观察。方法比较"土",连续按住回车键不放,屏幕上的文字自然会"滚"出屏幕。而且此时光标也到了屏幕最低端。
CGA模式4所需的显示缓存段地址同样是0B800H,这和文本模式相同。现在我们将显示缓存中第一个字节修改为"0AAH":
-EB800:0 AA[Enter]
打一个空格之后注意观察屏幕左上角,看到有一道彩色的短横线吗?
如果我们连续地修改显示缓存,就会看到这条短横线不断伸长,当然,我们无法使它最终延长到屏幕右侧,因为随着屏幕的滚动这条短线将被推到屏幕之外。看来若要在屏幕上画出较长的线来还要想别的方法。
好在我们的老朋友"DEBUG"还是很体谅人的,它提供了一个专用于"填充内存块"的命令--F(Fill)。这个命令的使用方法如下:
F
<内存地址>
<填充的字节数>
<所填入的数据>
现在我们再次将屏幕清理干净,并使用"F"命令将显示缓冲区前50H个字节填成"0AAH":
-FB800:0 4F AA[Enter]
于是屏幕最顶端出现了一条直线。当然,我们可以多填充一些字节,将刚才键入的命令做些变化,把命令中的4F变成50,看看有什么不同之处。
-FB800:0 50 AA[Enter]
区别还是有的:这条直线长了一些,而且长出的这一段在下面的一行最左端。由此看来显示缓冲区中每50H个字节对应着屏幕上的一个整行。
前面我们已经讨论过,在CGA模式4下屏幕水平方向上可以显示320个点,而一行又恰好对应了显示缓冲区中80个字节,所以可以算出显示缓冲区内每一个字节对应了屏幕上的4个点。
再结合前面使用"E"命令时看到的现象,我们可以推断出这条直线最左端显示的点对应了显示缓存中0B800:0处的一个字节,而最右端的4个点对应了0B800:4F处的一个字节。这个规律可以用"E"命令加以验证。
仔细观察长出的一小段直线,还可以看到这一小段直线与上面的直线之间是有间隙的,这又说明了什么问题呢?
我们所能做出的唯一推断就是假如显示缓存中最前面的50H个字节对应屏幕上的第一行直线的话,那么紧接着的50H个字节不会对应屏幕上第二行直线,否则是不应该出现间隙的。
如果我这个推断正确,那么屏幕上第二行直线究竟与显示缓存中哪些字节对应呢?带着这个问题,我们继续进行试验:
首先清除屏幕,然后键入以下命令:
-FB800:0 4AF FF[Enter]
可以看到屏幕上出现了一组15条直线,每两条直线之间都有间隙。我们所以要画15条直线是因为屏幕上显示的内容要向上滚动,只画一两条直线难免在后面的操作中被推到屏幕之外去,这样我们就不知道屏幕上第一行直线在哪儿了。
继续键入以下命令:
-FBA00:0 4AF FF[Enter]
这一下一切都清楚了,间隙中的直线都与显示缓存中0BA00段处的内容相对应。
图形模式4的显示缓存组织有些特别,它是按"隔行扫描"方式组织的,说明确了,共16KB的显示缓存分成了两部分,前8KB存储器对应屏幕上的第0、2、4、6、···、198等偶数线,而后8KB对应第1、3、5、···、199等奇数线。
每条线对应显示缓存中80个字节,每个字节对应屏幕上4个点。一个字节有8个bit,所以屏幕上每个点对应2个bit。最左面的点对应这个字节中的最高的2个bit,最右面的点对应最低的2个bit。2个bit共有4种组合--00、01、10、11,对应着4种颜色。
以上就是编制直接写屏程序所需要了解的知识,根据上面所述我们现在来考虑一下下面这两种情况应如何处理:
(1)在屏幕上(100,100)处画出一个白色点。假定现在使用第一组彩色。
① 由于Y坐标为100,是偶数,所以确定含有此点的字节位于显示缓存的前8KB;
② 由于每一行占用50H个字节,所以第100行的起始位置应该是50H (100/2)=0FA0H;把100除以2的含意很清楚,它虽然是屏幕上的第100线,实际上在所有偶数线中它是第50线。
③ 由于每个字节可以表示屏幕上连续4个点,所以Y坐标=100处的点应该位于这一行内偏移量为100/4=25的那个字节内,总的偏移量是0FA0H+19H=0FB9H;
④ 把Y坐标除以4后取余数,由于余数恰好为0,所以这个点将由字节中的最高两个bit表示,又因为我们要画一个白色点,所以写入显示缓冲区的一字节数据应该是11000000B;
结论:画出此点的方法是向内存0B800H:0FB9H处写入0C0H即可。
(2)在屏幕上(53,67)处画一个品红色点。假定使用第一组彩色。
① 由于Y坐标是奇数,所以含有此点的内存单元位于显示缓冲区的后8KB中;
② 第67线的起始位置等于50H*67/2=0A50;
③ 含有第53点的一个字节在这一行内的偏移量为53/4=0DH;总的偏移量为 2000H+0A50H+0DH=2A5DH;
④ 由于53除以4的余数是1,所以这个点在字节内占用bit5、bit4两位,这个字节应该是00100000B。
结论:画出此点的方法是向内存0B800H:2A5DH处写入20H即可。
通过上面所述的这些步骤大家就可以总结出计算显示缓冲区偏移量的方法,在此笔者不再总结。下面给出的一个程序可以在屏幕上由(0,0)向(199,199)处画一条绿色直线,程序中的画点子过程就是应用了直接写屏的方法。
code segment assume cs:code,ds:code org 100h main proc near mov ax,0004 ;设置图形显示模式4 int 10h mov ah,0bh ;选择彩色组0 mov bh,1 mov bl,0 int 10h mov cx,0 ;直线起始点坐标为(0,0) mov dx,0 lineloop: call pixel ;画出一个绿色点 inc cx ;X坐标加1 inc dx ;Y坐标加1 cmp dx,199 ;画到第199个点了吗? jnz lineloop ;若没画完直线,转LINELOOP继续 mov ah,0 ;等待键盘输入 int 16h mov ax,0003h ;设置字符显示模式3 int 10h mov ah,4ch ;结束进程 int 21h main endp pixel proc near ;画点子程序,使用直接写屏方法 push ax ;保存寄存器 push bx push cx push dx push di push es mov ax,0b800h ;ES:DI指向图形显示缓冲区首 mov es,ax mov di,0 mov ax,dx ;Y坐标送入AX寄存器 shr ax,1 ;判断Y坐标是否为奇数 jnc even_line ;若移出的位是0,说明所画线位于偶数行 mov di,2000h ;所画线位于奇数行,DI指向偏移2000H处 even_line: mov bx,80 ;计算"行数 x 80" mul bx add di,ax ;"行数 x 80"累加入DI寄存器 mov ax,cx ;计算一行内的偏移量 mov bl,4 div bl push ax ;暂存余数 mov ah,0 ;将商转换成16位累加入DI寄存器 add di,ax pop cx ;将余数送入CH寄存器 xchg ch,cl ;交换CH,CL寄存器 shl cl,1 ;余数 x 2作为移位计数 mov al,01000000b ;最高两位为颜色值 shr al,cl ;将颜色值移到指定位置 or byte ptr es:[di],al ;将含有颜色值的字节送入显示缓冲区 pop es ;恢复寄存器 pop di pop dx pop cx pop bx pop ax ret ;返回主过程 pixel endp code ends end main