快速的16色转换算法

File:      Fast16C.txt
Name:      快速的16色转换算法
Author:    zyl910
Blog:      http://blog.csdn.net/zyl910/
Version:   V1.0
Updata:    2006-11-29

下载(注意修改下载后的扩展名)

一、问题描述

  对于存储16色(4位)图像,VGA使用的是位平面方式,而DIB采用的是线性方式。无论用哪一种方式,在访问单一像素时,都需要进行复杂的位拆分运算,导致在该色彩模式下很难高效的编程。特别是这两种颜色模式之间的转换,需要极其复杂的位级拆分/重排操作,非常难以高效实现。本文就是专门讨论高效的16色转换算法的。

  为了便于解说,我们将连续的8个像素(从左到右)分别称为A、B、C、D、E、F、G、H。对描述这些像素的每一位,我们用数字来表示。比如A0代表像素A(从左侧数起:像素0)的D0位(最低位):
Pixel 0: A3A2A1A0
Pixel 1: B3B2B1B0
Pixel 2: C3C2C1C0
Pixel 3: D3D2D1D0
Pixel 4: E3E2E1E0
Pixel 5: F3F2F1F0
Pixel 6: G3G2G1G0
Pixel 7: H3H2H1H0


  对于VGA 16色。它使用的是位平面方式,总共4个位平面,一像素的4个位被分别保存在不同的位平面中,即位平面中的一个字节代表了8个像素的一位数据:
[VGA 16色]
Pixel : 0 1 2 3 4 5 6 7
bit  : 7 6 5 4 3 2 1 0
--------------------------------
Plane 0: A0 B0 C0 D0 E0 F0 G0 H0
Plane 1: A1 B1 C1 D1 E1 F1 G1 H1
Plane 2: A2 B2 C2 D2 E2 F2 G2 H2
Plane 3: A3 B3 C3 D3 E3 F3 G3 H3


  对于DIB 16色。它采用了线性方式,由于一个像素是4位,所以一个字节存放2个像素:
[DIB 16色]
<--- Byte 0 ---> <--- Byte 1 ---> <--- Byte 2 ---> <--- Byte 3 --->
A3A2A1A0B3B2B1B0 C3C2C1C0D3D2D1D0 E3E2E1E0F3F2F1F0 G3G2G1G0H3H2H1H0


  对于VGA,,由于切换位平面靠的是慢速的IO端口操作。所以一般是先一次性将整个扫描行的位图数据转成4个位平面数据,再使用串指令分别复制每一位平面的数据。也就是说,当把像素的4个位平面数据分离后,不能直接输出,得写在不同的缓冲区去,还要考虑将位串连接成字节。

  为了简单起见,我们不考虑非8倍边界问题,所有数据都是按32位对齐的。且图像大小固定为640*480,即扫描线长度固定为480。
  由于我们是直接访问VGA显存,不能在Windows等32位保护模式操作系统下运行,所以最好是16位算法。

  约定:

#define SCR_W 640
#define SCR_H 480

#define SCR_PLANES 4

#define SCANSIZE_DIB ((SCR_W)/2)
#define SCANSIZE_VGA ((SCR_W)/8)

BYTE byVGA[SCR_PLANES][SCANSIZE_VGA];
BYTE byDIB[SCANSIZE_DIB];


  由于我们一般很少需要从屏幕得到位图数据,我们主要是将位图绘制到屏幕上,所以我们应该将精力集中在如何实现DIB转VGA上。

 

二、逐像素算法

  该算法的想法是很简单,每次将一个像素的4个位分别写到4个位平面中:
x = BYTE();
byVGA[0][icurbyte] |= (x & 1) << icurbit;
x = x >> 1;
byVGA[1][icurbyte] |= (x & 1) << icurbit;
x = x >> 1;
byVGA[2][icurbyte] |= (x & 1) << icurbit;
x = x >> 1;
byVGA[3][icurbyte] |= (x & 1) << icurbit;


  由于最左侧的像素在高4位,所以实际的转换程序是这个样子的:
x = BYTE();
byVGA[3][icurbyte] |= (x & 0x80) >> icurbit;
x = x << 1;
byVGA[2][icurbyte] |= (x & 0x80) >> icurbit;
x = x << 1;
byVGA[1][icurbyte] |= (x & 0x80) >> icurbit;
x = x << 1;
byVGA[0][icurbyte] |= (x & 0x80) >> icurbit;
  (注意此时icurbit变量的含义不同)

 

  特别由于该算法是将数据分别写入4个位平面,给地址计算带来很大的麻烦,而且不能很好的利用Cache,使得代码的执行速度低下。

 

三、逐位平面算法

  由于同时访问4个位面的效率太低,是否可以每次只处理一个位面呢?
  一个字节是8位,4个位面共32位数据,所以该算法所做的是一个将分散的8个位拼成一个字节:

x = DWORD() & 0x11111111;    // 000g 000h 000e 000f 000c 000d 000a 000b
x = BSWAP(x);          // 000a 000b 000c 000d 000e 000f 000g 000h
x = (x | (x>>3)) & 0x03030303; // 0000 00ab 0000 00cd 0000 00ef 0000 00gh
x = (x | (x>>6)) & 0x000F000F; // 0000 0000 0000 abcd 0000 0000 0000 efgh
x = (BYTE)(x | (x>>12));    // 0000 0000 0000 abcd 0000 0000 abcd efgh

  再进行仔细分析,可发现并不需要“& 0x000F000F”这个操作:
x = DWORD() & 0x11111111;    // 000g 000h 000e 000f 000c 000d 000a 000b
x = BSWAP(x);          // 000a 000b 000c 000d 000e 000f 000g 000h
x = (x | (x>>3)) & 0x03030303; // 0000 00ab 0000 00cd 0000 00ef 0000 00gh
x = (x | (x>>6));        // 0000 00ab 0000 abcd 0000 00ef 0000 efgh
x = (BYTE)(x | (x>>12));    // 0000 00ab 0000 abcd 00ab 00ef abcd efgh

  对应的汇编代码为:
;x = DWORD();          // 000g 000h 000e 000f 000c 000d 000a 000b
;mov eax, [si];
;and eax, 11111111h;
;x = BSWAP(x);          // 000a 000b 000c 000d 000e 000f 000g 000h
bswap eax;
;x = (x | (x>>3)) & 0x03030303; // 0000 00ab 0000 00cd 0000 00ef 0000 00gh
mov edx, eax;
shr edx, 3;
or eax, edx;
and eax, 03030303h;
;x = (x | (x>>6));        // 0000 00ab 0000 abcd 0000 00ef 0000 efgh
mov edx, eax;
shr edx, 6;
or eax, edx;
;x = (BYTE)(x | (x>>12));    // 0000 00ab 0000 abcd 00ab 00ef abcd efgh
mov edx, eax;
shr edx, 12;
or eax, edx;
;mov [di], al;

 


三、双倍逐位平面算法

  仔细观察逐位平面算法,会发现它只使用了两个寄存器。x86有8个通用寄存器,其中esp、ebp用于栈操作,而esi、edi一般用存储地址。所以我们能使用的寄存器只有eax、ebx、ecx、edx,正好能同时对进行处理两个。
  由于逐位平面算法存在很强的数据相关性,现在同时计算两个,即同时计算两个无关的数据,这使得程序在支持超标量的处理器上能更快地执行。

;x = DWORD();          // 000g 000h 000e 000f 000c 000d 000a 000b
;push ecx
;mov eax, [esi];
;mov cl, iP
;mov ebx, [esi+4];
;shr eax, cl
;shr ebx, cl
;and eax, 11111111h;
;and ebx, 11111111h;
;x = BSWAP(x);          // 000a 000b 000c 000d 000e 000f 000g 000h
bswap eax;
bswap ebx;
;x = (x | (x>>3)) & 0x03030303; // 0000 00ab 0000 00cd 0000 00ef 0000 00gh
mov edx, eax;
mov ecx, ebx;
shr edx, 3;
shr ecx, 3;
or eax, edx;
or ebx, ecx;
and eax, 03030303h;
and ebx, 0 3030303h;
;x = (x | (x>>6));        // 0000 00ab 0000 abcd 0000 00ef 0000 efgh
mov edx, eax;
mov ecx, ebx;
shr edx, 6;
shr ecx, 6;
or eax, edx;
or ebx, ecx;
;x = (BYTE)(x | (x>>12));    // 0000 00ab 0000 abcd 00ab 00ef abcd efgh
mov edx, eax;
mov ecx, ebx;
shr edx, 12;
shr ecx, 12;
or al, dl;
or bl, cl;
mov ah, bl
;mov [edi], ax;


四、其他算法

4.1 不需要BSWAP的32位算法

x = DWORD() & 0x11111111;    // 000g 000h 000e 000f 000c 000d 000a 000b
x = (x | (x>>3)) & 0x03030303; // 0000 00gh 0000 00ef 0000 00cd 0000 00ab
x = (x | (x>>14)) & 0x00000F0F; // 0000 0000 0000 0000 0000 ghcd 0000 efab
x =  x | (x>>4);        // ---- ---- ---- ---- ---- ---- ghcd efab
// 交换gh与ab
t = (x ^ (x>>6)) & 0x03;    // ---- ---- 0000 00xx. xx = gh XOR ab
x = x ^ t ^ (t<<6);       // ---- ---- abcd efgh. ab XOR xx = ab XOR (gh XOR ab) = gh. gh XOR xx = gh XOR (gh XOR ab) = ab


4.2 16位算法

  16位版:
t = HIWORD() & 0x1111;     // 000g 000h 000e 000f
x = LOWORD() & 0x1111;     // 000c 000d 000a 000b
x = (t<<2) | x;         // 0g0c 0h0d 0e0a 0f0b
x = (x | (x>>3)) & 0x0F0F;   // 0000 ghcd 0000 efab
x = x | (x>>4);         // ---- ---- ghcd efab
// 交换gh与ab
t = (x ^ (x>>6)) & 0x03;    // ---- ---- 0000 00xx. xx = gh XOR ab
x = x ^ t ^ (t<<6);       // ---- ---- abcd efgh. ab XOR xx = ab XOR (gh XOR ab) = gh. gh XOR xx = gh XOR (gh XOR ab) = ab

  对应的汇编代码为:
;t = HIWORD();          // 000g 000h 000e 000f
;mov dx, [si+2]
;and dx, 1111h
;x = LOWORD();          // 000c 000d 000a 000b
;mov ax, [si]
;and ax, 1111h
;x = (t<<2) | x;         // 0g0c 0h0d 0e0a 0f0b
shl dx, 2
or ax, dx
;x = (x | (x>>3)) & 0x0F0F;   // 0000 ghcd 0000 efab
mov dx, ax
shr dx, 3
or ax, dx
and ax, 0f0f
;x = x | (x>>4);         // ---- ---- ghcd efab
mov dx, ax
shr dx, 4
or al, dl
;// 交换gh与ab
;t = (x ^ (x>>6)) & 0x03;    // ---- ---- 0000 00xx. xx = gh XOR ab
mov dl, al
shr dl, 6
xor dl, al
and dl, 03h
;x = x ^ t ^ (t<<6);       // ---- ---- abcd efgh. ab XOR xx = ab XOR (gh XOR ab) = gh. gh XOR xx = gh XOR (gh XOR ab) = ab
xor al, dl
shl dl, 6
xor al, dl
;mov [di], al

 


4.3 位矩阵转置算法

  回头再仔细看看DIB16色与VGA16色的存储方式,会发现转化操作很像一次矩阵转置,这样我们就可以同时对4个位平面进行运算。假设现在有支持位矩阵转置指令的计算机,我们来想象一下在那样的计算机上如何编码。
  由于4*8矩阵不够工整,我们需要的是8*8的方阵,这正好是一个64位寄存器。

  源数据是DIB位图,将其载入64位寄存器:
A3 A2 A1 A0 B3 B2 B1 B0
C3 C2 C1 C0 D3 D2 D1 D0
E3 E2 E1 E0 F3 F2 F1 F0
G3 G2 G1 G0 H3 H2 H1 H0
I3 I2 I1 I0 J3 J2 J1 J0
K3 K2 K1 K0 L3 L2 L1 L0
M3 M2 M1 M0 N3 N2 N1 N0
O3 O2 O1 O0 P3 P2 P1 P0

  尺寸为4位的逆外混洗:
A3 A2 A1 A0 I3 I2 I1 I0
B3 B2 B1 B0 J3 J2 J1 J0
C3 C2 C1 C0 K3 K2 K1 K0
D3 D2 D1 D0 L3 L2 L1 L0
E3 E2 E1 E0 M3 M2 M1 M0
F3 F2 F1 F0 N3 N2 N1 N0
G3 G2 G1 G0 O3 O2 O1 O0
H3 H2 H1 H0 P3 P2 P1 P0

  位矩阵转置:
A3 B3 C3 D3 E3 F3 G3 H3
A2 B2 C2 D2 E2 F2 G2 H2
A1 B1 C1 D1 E1 F1 G1 H1
A0 B0 C0 D0 E0 F0 G0 H0
I3 J3 K3 L3 M3 N3 O3 P3
I2 J2 K2 L2 M2 N2 O2 P2
I1 J1 K1 L1 M1 N1 O1 P1
I0 J0 K0 L0 M0 N0 O0 P0

  尺寸为8位的外混洗:
A3 B3 C3 D3 E3 F3 G3 H3
I3 J3 K3 L3 M3 N3 O3 P3
A2 B2 C2 D2 E2 F2 G2 H2
I2 J2 K2 L2 M2 N2 O2 P2
A1 B1 C1 D1 E1 F1 G1 H1
I1 J1 K1 L1 M1 N1 O1 P1
A0 B0 C0 D0 E0 F0 G0 H0
I0 J0 K0 L0 M0 N0 O0 P0

 


测试结果
~~~~~~~~


dos版:使用Borland C++ 3.1 for DOS 编译
vc版:使用Microsoft Visual C++ 6.0 编译


<1> AMD Athlon XP 1700+(实际频率:1463 MHz (11 x 133))

dos版:
[DOS实模式]
D2V_Pixel   :         113.5238
D2V_Plane16 :         178.1790
D2V_Plane   :         156.0575
D2V_DPlane  :         624.9337
[Win98]
D2V_Pixel   :         112.7193
D2V_Plane16 :         176.9724
D2V_Plane   :         155.0519
D2V_DPlane  :         620.9116
[WinXP]
D2V_Pixel   :         113.2221
D2V_Plane16 :         177.2740
D2V_Plane   :         155.4541
D2V_DPlane  :         623.2243


vc版:
[Win98]
D2V_Pixel   :         283.6433
D2V_Plane   :         605.9394
D2V_PlaneASM:         684.7000
D2V_DPlane  :         734.9000
D2V_Plane16 :         493.4000
[WinXP]
D2V_Pixel   :         296.2000
D2V_Plane   :         606.5000
D2V_PlaneASM:         689.9000
D2V_DPlane  :         737.4000
D2V_Plane16 :         493.1000

 

<2> Intel Celeron-S, 1000 MHz (10 x 100)

dos版:
[WinXP]
D2V_Pixel   :          41.4276
D2V_Plane16 :         133.4331
D2V_Plane   :         114.2276
D2V_DPlane  :         320.9635

vc版:
[WinXP]
D2V_Pixel   :         187.6250
D2V_Plane   :         355.2224
D2V_PlaneASM:         378.1487
D2V_DPlane  :         350.7597
D2V_Plane16 :         164.0180


可以看出,双倍逐位平面算法(D2V_DPlane)的性能非常优越,特别是在DOS下,比其他方法要快得多。但是该算法在Windows下的表现并没有那么出众,甚至有时比基本的逐位平面算法还要慢。其原因可能是现代的32位编译器能更好的为现代CPU生成代码,而BC3.1只是一个过时的16位编译器。但是我思考DIB转VGA的算法就是为了实现快速的VGA绘图操作,所以坚决使用双倍逐位平面算法。

 


参考文献
~~~~~~~~
[1] [美]Henry S. Warren,Jr. 著, 冯德 译. 高效程序的奥秘(Hacker's Delight). 机械工业出版社, 2004.5
 
 

你可能感兴趣的:(c,算法,Microsoft,dos,byte,Borland)