Turbo C / VGA x86 程序集:从 ram 复制到 vram

Turbo C / VGA x86 assembly: Copy from ram to vram

我只是想用 turbo c 在带有 MCGA/VGA 卡的 8086/286(用 pcem 模拟)上绘制 "sprites"。

使用 turbo c 3.0 编译,它应该可以在带有 MCGA 的真实 8086 上工作。我没有使用 VGA 模式 x 因为它有点复杂而且我不需要额外的 vram 来做我想做的事情,即使屏幕上有一些闪烁,也没关系 :).

在 C 中,我有一堆 memcpys 以模式 13 将数据从加载的 sprite 结构移动到 VGA:

byte *VGA=(byte *)0xA0000000L;    
typedef struct tagSPRITE             
{
    word width;
    word height;
    byte *data;
} SPRITE;

void draw_sprite(SPRITE *sprite){
    int i = 0; int j = 0; 
    for(j=0;j<16;j++){
        memcpy(&VGA[0],&sprite->data[i],16);
        screen_offset+=320;
        i+=16;
    }
}

目标是将该代码转换为特定的汇编函数以稍微加快速度。


(编者注:这是原始的 asm 尝试和答案所基于的文本。查看修订历史以了解此问题发生了什么。在上次编辑中已全部删除,仅制作提问者自己的答案有道理,所以这次编辑试图让两个答案都有意义。)

我试着用类似这样的东西在汇编中编写它,我确信它有很大的错误:

void draw_sprite(SPRITE *sprite){
    asm{
        mov ax,0A000h
        mov es,ax           /* ES points to the video memory */

        mov di,0            /* ES + DI = destination video memory */
        mov si,[sprite.data]/* source memory ram ???*/
        mov cx,16           /* bytes to copy */

        rep movsb           /* move 16 bytes from ds:si to es:di (I think this is the same as memcpy)*/

        add di,320          /* next scanline in vram */         
        add si,16           /* next scanline of the sprite*/
        mov cx,16   

        rep movsb           /* memcpy */

        /*etc*/
    }
}

我知道 ram 地址不能存储在 16 位寄存器中,因为它大于 64k,所以 mov si,[sprite.data] 是行不通的。

那么如何将 ram 地址传递给 si 寄存器?(如果可能的话)。

我知道我必须使用 ds 和 si 寄存器在 "ds" 中设置类似 "bank" 的内容,然后 "si" 寄存器可以读取 64k 内存块,(这样movsb就可以把ds:si移动到es:di)。但我就是不知道它是如何工作的。

我还想知道该 asm 代码是否会比 c 代码(在 8086 8 Mhz 或 286 上)更快,因为您不必在每个循环中都重复第一部分。

我暂时不会从 vram 复制到 vram,因为我必须使用模式 X,那是另一回事了。

rep movsb 增加 SI 和 DI 以及减少 CX。它就像一个 memcpy,通过引用获取其 dst、src 并将它们更新到复制区域的末尾。

所以你需要 add di, 320-16,并且 si 已经指向精灵的下一行(因为行步幅与宽度 = 16 匹配)。

就分段而言,movsbDS:SI复制到ES:DI,所以设置ES:DI指向显存是正确的。

Turbo C 的调用约定 requires/ensures 函数 entry/exit 上的 DF=0(类似于普通的 32 位调用约定),因此您不需要 cld 来确保 movsb 朝着正确的方向前进(向前而不是向后)。 (如果你在其他地方使用了 std 并且没有放回去,请在那里修复它以避免违反调用约定。)

Turbo C 的调用约定也有 call-clobbered AX/BC/CX/DX 和 ES。 (). If its inline asm is anything like MSVC, the compiler will save/restore DI and SI for you. But possibly it doesn't save/restore DS for you, so 你需要自己 push/pop DS 到 save/restore 它。看看编译器生成的 asm 以确保你遵循调用约定。

从你更新的问题中,我们可以看到你的构建选项包括 memory model = large ,它使所有指针都变成远指针,这将是一个显着的减速与手动选择哪些指针需要是 FAR 而其他指针只是16 位。但是,如果您没有任何理由了解 16 位实模式分段和所有不再相关的东西,那么一定要继续使用它。 (您可能会选择至少代码可以靠近的内存模型,所以靠近 call/ret 只有 push/pop 一个 IP 值,而不是一个 CS。)


你可以把代码放在一个循环中,像这样。

我有硬编码宽度/高度与加载它的混合,就像你的问题一样,但如果你计算 BX(320 宽度)中的行步幅,你有足够的寄存器来提升计算。循环分支本身也已经处理了运行时变量精灵大小。

    push  ds

    xor   di,di             // DI=0

    //mov   si,[sprite.data]  /* source memory ram ???*/
    lds   si,[sprite.data]  // with your build options, everything is a seg:off FAR pointer
    lea   ax, [si + 16*16]  // end_src pointer

    mov   dx, [sprite.width]
    shr   dx, 1              // words to copy = bytes / 2
    // if you can't assume even width, then just use movsb
    // or optimize with rep movsb + a test of the low bit for one movsb

@loop:                    // do {
    mov   cx,dx            /* words to copy */

    rep movsw             /* copy 16 bytes from ds:si to es:di */

    add   di, 320-16      /* starting column in next scanline in vram */         
    // add si, 0          // sprite row stride - width = 0

    cmp   si, ax
    jb   @loop           // } while(src < endsrc);

    pop   ds

注意使用 movsw 复制 2 字节块。根据操作数的大小,PPro 之前的 x86 实际上一次只复制 1 个字节或 1 个字。

PPro 和更高版本具有快速字符串微代码,可以复制更大的块。但这有很大的启动开销,所以对于只有 16 个字节的情况,最好在 16 位模式的现代 x86 上使用 4 个 DWORD 整数寄存器 (eax),或者 x87 fild qword/fistp 的 qword , 或 16 字节,带一个 XMM 寄存器。

在实际的 8086 或 286 上,fild/fistp 与整数副本相比会慢得可怕。使用 16 位数据总线,您一次只能复制 2 个字节,因此 rep movsw 在真正的 286 上很好。

另见

用于现代 x86 上的 memcpy(不过主要集中在大型副本上。)

另请注意,VRAM 通常是不可缓存或写入组合的,因此,如果您实际上正在优化复制到 VRAM 例程,那么将多个狭窄存储到同一缓存行对于 UC 来说很糟糕,但对于 WC 来说还不错, 在 CPU 上有缓存。

感谢 Michael Petch、Peter Cordes 和大家。我得到了答案。

复制数据到vga显存的汇编代码如下:

DGROUP          GROUP    _DATA, _BSS
_DATA           SEGMENT WORD PUBLIC 'DATA'
_DATA           ENDS
_BSS            SEGMENT   WORD PUBLIC 'BSS'             
_BSS            ENDS
_TEXT           SEGMENT BYTE PUBLIC 'CODE'
                ASSUME CS:_TEXT,DS:DGROUP,SS:DGROUP

            PUBLIC _draw_sprite       
_draw_sprite    proc    far 
    push bp
    mov bp,sp
    push ds
    push si
    push di
    ;-----------------------------------
    lds     bx,[bp+6]
    lds     si,ds:[bx+4]        ; sprite->data to ds:si
    mov     ax,0A000h
    mov     es,ax                       
    mov     di,0                ; VGA[0] to es:di

    mov     ax,16               ; 16 scan lines
copy_line:  
    mov     cx,8
    rep     movsw               ; copy 16 bytes from ds:si to es:di
    add     di,320-16           ; go to next line of the screen
    dec     ax
    jnz     copy_line
    ;-----------------------------------
    pop di
    pop si
    pop ds
    mov sp,bp
    pop bp
    ret 
_draw_sprite    endp

在c中声明函数为:

    void draw_sprite(SPRITE *spr);

数据存储在spr->data,是一个数字数组(从0到255,存储一个像素的颜色)。

该代码最终在 x = 0、y = 0 的位置绘制了 16x16 位图。

非常感谢!