assembly - 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,这是另一回事。
解决方案
rep movsb
增加 SI 和 DI 以及减少 CX。它就像一个 memcpy,它通过引用获取其 dst、src 并将它们更新到复制区域的末尾。
所以你需要add di, 320-16
, 并且si
已经指向精灵的下一行(因为行步幅与宽度 = 16 匹配)。
至于分段,movsb
复制 fromDS:SI
到ES: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
请注意使用movsw
2 字节块进行复制。根据操作数大小,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 例程,那么在具有缓存。
推荐阅读
- python - Wikipedia scraper 查找文章列表并将它们附加到数据框 Python
- javascript - 将带有数据属性的 json 反序列化为 json 数组中的对象列表
- javascript - 使用 VueJS 扩展 Tiptap Vuetify 时在编辑器内容中保留样式和类
- javascript - putState() 和 putPrivateData() 可以在同一个链码中使用吗?
- swiftui - 与事实源向后兼容的自定义绑定初始化
- oracle - 使用额外的列将所有值从一个表复制到另一个表的简单方法
- scikit-learn - 如何在 scikit-learn 的高斯过程回归中重新调整归一化标准差?
- javascript - 使用 hitSlop 增加可触摸区域(React-native-popup-menu)
- apex - 有条件地更改颜色 Apex/Visualforce Lightning
- postgresql - postgresql:如何选择不同格式的时间戳