首页 > 解决方案 > Turbo C / VGA x86 组件:从 ram 复制到 vram

问题描述

我只是喜欢用 turbo c 在 8086/286(用 pcem 模拟)上用 MCGA/VGA 卡绘制“精灵”。

使用 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,这是另一回事。

标签: assemblyx86-16turbo-cvga

解决方案


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

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

至于分段,movsb复制 fromDS:SIES:DI,所以设置 ES:DI 指向显存是正确的。

Turbo C 的调用约定要求/确保函数进入/退出时 DF=0(就像普通的 32 位调用约定一样),因此您不需要 acld来确保movsb朝着正确的方向前进(向前而不是向后)。(如果您在std其他地方使用并且没有将其放回原处,请在此处进行修复以避免违反调用约定。)

Turbo C 的调用约定也有调用破坏的 AX/BC/CX/DX 和 ES。(感谢@MichaelPetch)。如果它的内联汇编类似于 MSVC,编译器将为您保存/恢复 DI 和 SI。但它可能不会为您保存/恢复 DS,因此@MichaelPetch 建议您需要推送/弹出 DS 以自己保存/恢复它。查看编译器生成的 asm 以确保您遵循调用约定。

从您更新的问题中,我们可以看到您的构建选项包括 memory model = large ,它使所有指针都变成远指针,这将大大减慢与手动选择哪些指针需要 FAR 而其他指针只有 16 位。但是,如果您没有任何理由了解 16 位实模式分割和所有不再相关的东西,那么请务必继续使用它。(您可能会选择至少代码可以接近的内存模型,因此 near call/ret 仅推送/弹出 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

请注意使用movsw2 字节块进行复制。根据操作数大小,PPro 之前的 x86 确实一次只复制 1 个字节或 1 个字。

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

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

另请参阅REP 执行什么设置?

以及为现代 x86 上的 memcpy 的 memcpy 增强的 REP MOVSB(不过,主要集中在大副本上。)

另请注意,VRAM 通常是不可缓存或写入组合的,因此,如果您实际上是在优化复制到 VRAM 例程,那么在具有缓存。


推荐阅读