zoukankan      html  css  js  c++  java
  • 快速的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
     
     

  • 相关阅读:
    nginx
    mysql
    intelij maven
    redis命令大全
    绑定touch事件后click无效,vue项目解决棒法
    新的用法
    img
    vuedragable
    自己总结
    vuex的项目在id中不能运行
  • 原文地址:https://www.cnblogs.com/zyl910/p/2186629.html
Copyright © 2011-2022 走看看