我终于搞懂了 PVS 的大致结构,让我们顺着上次的分析继续
本系列已完结,以下为整理目录方便查阅
要看懂 PVS 的绘制方式,需要一些 VGA 十六色四平面的基础知识,关于这些知识我阅读 了不少资料,但是因为我理解能力比较弱或者相关资料年代久远,能够查阅的内容有限而且 比较难懂,收获不是很大,不过也不是没有收获,因为 vga 绘图这个话题很大,所以可能 查到的东西没办法跟我们的实用联系起来,所以挫败感很大。后来我发现,看懂 PVS 这块, 这一篇就足够了,当然 这篇我也没有完全看明白,不过基本够用,总结一下相关知识大概如下:
16 色 VGA 显示被切分为四个平面($2^4$),每个点的色彩索引被分在四个平面上,这样,
平面中的一个字节代表了 8 个像素的颜色信息,所以 80 个字节正好是 640 个点,对应
屏幕宽度,像素的位置按位从高到低排列,比如参考文中的例子,如果画一条 (2,0) 到
(6, 0) 的线,那么第一个字节为 0011 1110
也就是 3e
,当然前提是该平面有这条线,
如果没有,那么这个字节为 0000 0000
即 00
。
如果颜色索引是 10(1010),由于平面是从低位开始,所以四个平面是这样:
Plane 0: 00000000 -> 00
Plane 1: 00111110 -> 3e
Plane 2: 00000000 -> 00
Plane 3: 00111110 -> 3e
每个平面的内存为 64k,但我们没办法直接访问,显卡只开放了 a000:0 开始的 64k 地址 给我们访问,所以如果要写入不同的平面,我们需要使用显卡的一些端口和寄存器来选择 要操作的平面,然后再执行操作,这样写入 a000:0 时,才会操作到对应的平面,访问平面 有四种写模式,两种读模式。很复杂对吧,不过别担心,接下来我们用到的,只有这些:
out 3ce, 05
out 3cf, xx
指令 | 说明 |
---|---|
3ce xx05 |
写入模式xx |
3c4 xx02 |
按位选择平面xx |
其实前面我们就聊过四平面的设计,现在我们知道了,这个不是大宇的构思,是因为这样 比较方便写入显存。那么 VGA 16 色为什么采用四平面的设计,是因为这是一种向下兼容的 模式,比如,如果后面要改成 32 色,只要多加一个平面就好,而且缺失一个平面也不会 影响低端显卡的显示,不过这种设计显然没有考虑到显卡的发展速度,16 色接下来不是 32 而是 256 色,再接下来就是 16 位和 32 位色了
之前我们提到过,程序是以某种方式把 ds:dx
的数据搬运到 a000:0
来绘图的,那么
我们可以把 dosbox 执行的汇编指令 dump 下来,看看是怎么做的。
我们知道如果要向 a000:0
写入数据,ES
要设为 A000
(参考Understanding the DOSBox debug screen ),
这样我们很快就可以从 log 定位到绘图代码的起点:
02B7:00000ECF xor di,di ; di = 0
02B7:00000ED1 lodsb ; al = [ds:si][6b83:0000] = 5
; si = 0001 // si++
02B7:00000ED2 movzx bx,al ; bx = al = 5;
02B7:00000ED5 shl bx,1 ; bx << 1 // bx = 000a
02B7:00000ED7 jmp near word cs:[bx+0E8C] cs:[0E96]=0F3D
右边是伪代码注释,这段代码简单说就是读取一个字节(5),然后跳到 case 5
,
那么我们继续看下去,case 5
很长,我们分解一下:
02B7:00000F3D mov ax,0005
02B7:00000F40 mov dx,03CE
02B7:00000F43 out dx,ax ; out 03ce,0005 写入模式 0
设置写入模式 0,我们不需要关心写入模式是什么,往下看就好:
02B7:00000F44 lodsb ; al = [6b83:0001] = 0d; si = 0002;
02B7:00000F45 movzx cx,al ; cx = al = 000d;
读取一个字节(13),然后设为次数
02B7:00000F48 mov dx,03C4
02B7:00000F4B mov al,02
02B7:00000F4D out dx,al ; out 03c4,02 选择平面索引
02B7:00000F4E inc dx ; dx = 03c5;
02B7:00000F4F mov al,01 ; al = 01
02B7:00000F51 mov bx,di ; bx = di = 0;
02B7:00000F53 out dx,al ; out 03c5, 01 选择平面 0
选择平面 0
(1 = 0001),后续会向平面 0
写入数据,还有一个细节,
bx
被设定为 ES
的当前位置 DI
02B7:00000F54 mov di,bx ; di = bx = 0;
02B7:00000F56 push cx ; cx = 000d = 1101b
02B7:00000F57 push cx
02B7:00000F58 shr cx,02 ; cx >> 2; cx = 11b = 0003
02B7:00000F5B repe movsd ; copy(ds:si, es:di, 4)
; [ES:DI][A000:0000] = [DS:SI][6B83:0002]
; di += 4; si += 4
; repeat 3 times;
02B7:00000F5E pop cx ; cx = 000d
02B7:00000F5F and cx,0003 ; cx = 1101b & 0011b = 1
02B7:00000F62 repe movsb ; copy(ds:si, es:di, 1)
; [ES:DI][A000:000C] = [DS:SI][6B83:000E]
; di += 1; si += 1
; repeat 1 times
有点啰嗦的操作:
bx
(ES
的当前位置)DS
复制 4
个字节到 ES
,重复 3
次DS
复制 1
个字节到 ES
,重复 1
次这可能是优化后的代码,合并一下逻辑:
bx
(ES
的当前位置)DS
复制 13
个字节到 ES
这样我们知道了 case 5
读取的第一个字节的意思,复制的个数,接下来:
02B7:00000F64 pop cx ; cx = 000d
02B7:00000F65 add al,al ; al = 2;
02B7:00000F67 out dx,al ; out 03c5, 02
选择平面 1
(2 = 0010)
02B7:00000F68 mov di,bx ; di = bx = 0
02B7:00000F6A push cx ; cx = 000d
02B7:00000F6B push cx ;
02B7:00000F6C shr cx,02 ; cx = 1101b >> 2 = 3
02B7:00000F6F repe movsd ; copy(ds:si, es:di, 4)
; [ES:DI][A000:0000] = [DS:SI][6B83:000F]
; di += 4; si += 4
; repeat 3 times
02B7:00000F72 pop cx ; cx = 000d
02B7:00000F73 and cx,0003 ; cx = 000d & 0003 = 1
02B7:00000F76 repe movsb ; copy 1 byte for 1 times
bx
(ES
的当前位置)DS
复制 13
个字节到 ES
已经看到相同的逻辑了对吧,平面 2
、3
也是如此:
02B7:00000F78 pop cx ; cx = 000d
02B7:00000F79 add al,al ; al += al = 4
02B7:00000F7B out dx,al ; out 3c5, 4 : select plane 2
02B7:00000F7C mov di,bx ; di = 0
02B7:00000F7E push cx
02B7:00000F7F push cx
02B7:00000F80 shr cx,02
02B7:00000F83 repe movsd
02B7:00000F86 pop cx
02B7:00000F87 and cx,0003
02B7:00000F8A repe movsb
02B7:00000F8C pop cx ; cx = 000d
02B7:00000F8D add al,al ; al += al = 8
02B7:00000F8F out dx,al ; out 3c5, 8 : select plane 3
02B7:00000F90 mov di,bx ; di = 0
02B7:00000F92 push cx
02B7:00000F93 push cx
02B7:00000F94 shr cx,02
02B7:00000F97 repe movsd
02B7:00000F9A pop cx
02B7:00000F9B and cx,0003
02B7:00000F9E repe movsb
接下来是收尾部分:
02B7:00000FA0 pop cx ; cx = 000d
02B7:00000FA1 add al,al ; al += al = 0010
02B7:00000FA3 mov ah,0F ; ax = 0f10
02B7:00000FA5 mov al,02 ; ax = 0f02
02B7:00000FA7 mov dx,03C4 ; dx = 3c4
02B7:00000FAA out dx,ax ; out 3c4, 0f02 : select all planes
02B7:00000FAB lodsb ; al = [ds:si][6b83:0036] = 09
02B7:00000FAC movzx bx,al ; bx = 09
02B7:00000FAF shl bx,1 ; bx << 1 : bx = 0012
02B7:00000FB1 jmp near word cs:[bx+0E8C] ; jump to 109d
收尾做了如下事情:
case 9
02B7:0000109D mov ah,00 ; ax = 0009 (al: case 09)
02B7:0000109F shl ah,03 ; ax = 0009
02B7:000010A2 or ah,01 ; ax = 0109
02B7:000010A5 mov dx,03CE
02B7:000010A8 mov al,05
02B7:000010AA out dx,ax ; out 3ce, 0105: write mode 1
02B7:000010AB mov bx,di ; bx = di = 000d(save di to bx)
02B7:000010AD movzx ax,[si] ; ax = 0004
02B7:000010B0 sub bx,ax ; bx -= ax = 0009
02B7:000010B2 inc si ; si += 1 = 0038
02B7:000010B3 mov al,es:[bx] ; al = [a000:0009] = e8
02B7:000010B6 stosb ; store al to es:di [a000:000d]
02B7:000010B7 lodsb ; al = [ds:si][6b83:0038] = 05
02B7:000010B8 movzx bx,al
02B7:000010BB shl bx,1
02B7:000010BD jmp near word cs:[bx+0E8C] ; jump to case 05
case 9
比较简单:
di
到 bx
(000d)bx
减去 04
(0009)si
加 1
es:bx
读取数据放入 al
(e8)al
保存在 es:di
case 5
很啰嗦对不对,重新理一下逻辑:
offset
es:(di - offset)
读取一个字节放入 es:di
case 5
下一个又是 case 5
了,不过我没有打算继续了,如果你是我这个系列的「忠实读者」,
你可能和我会有相同的想法:感觉和前景、背景的绘图操作差不多,这时可以看一下
这里
和背景图 case 5
和 case 9
的处理方式完全一样
如果一个 case 的处理方式相同是巧合,那么两个 case 相同,我想我们可以试一下之前 的代码了,篇幅已经很长了,我们看个截图休息一下吧: