x86 VGA的双缓冲



为了唤起一些记忆,我决定坐下来在VGA模式下编写一个小汇编游戏13h-直到我意识到视觉输出像地狱一样闪烁。

起初我怀疑这可能是我的clearscreen例程。通过使用STOSW而不是写一个字节到显存,闪烁是不那么烦人,但仍然存在。

进一步挖掘,我想起我可能不得不等待垂直回溯,然后更新我的屏幕,但这并没有使事情变得更好。

所以我知道的最终解决方案是这样的:

  • 在单独的内存区域上执行所有图形操作-清除屏幕,设置像素
  • 等待垂直回撤
  • 将内存复制到显存

理论当然很简单,但我只是不知道如何做我的写缓冲区,并最终blit到显存!

下面是我为TASM编写的代码的精简片段:

VGA256      EQU 13h
TEXTMODE    EQU 3h
VIDEOMEMORY EQU 0a000h
RETRACE     EQU 3dah
.MODEL LARGE
.STACK 100h
.DATA 
spriteColor     DW ?
spriteOffset    DW ?
spriteWidth     DW ?
spriteHeight    DW ?
enemyOneA       DB 0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,1,1,0,1,1,0,1,1,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,0,1,0,0,0,0,0,0,0,0,0,1,0,1,0,0,1,0,1,0,0,0,0
spriteToDraw    DW ?
buffer          DB 64000 dup (0) ; HERE'S MY BUFFER
.CODE
Main:
    MOV     AX,@DATA;
    MOV     DS,AX
    MOV     AH,0
    MOV     AL,VGA256
    INT     10h
    CLI
MainLoop:
    MOV     DX,RETRACE
Vsync1:
    IN      AL,DX
    TEST    AL,8
    JZ      Vsync1
Vsync2:
    IN      AL,DX
    TEST    AL,8
    JNZ     Vsync2
        
    CALL    clearScreen
    CALL    updateSprites
    JMP     MainLoop
    mov     AH,1
    int     21h
    mov     AH,0
    mov     AL,TEXTMODE
    int     10h 
; program end
clearScreen PROC NEAR 
    MOV     BX,VIDEOMEMORY
    MOV     ES,BX
    XOR     DI,DI
    MOV     CX,320*200/2
    MOV     AL,12
    MOV     AH,AL
    REP     STOSW
    RET
clearScreen ENDP
drawSprite PROC NEAR
    MOV     DI,0
    MOV     CX,0
ForLoopA:
    PUSH    CX
    MOV     SI,CX
    MOV     CX,0
ForLoopB:
    MOV     BX,spriteToDraw
    MOV     AL,[BX+DI]
    CMP     AL,0
    JE      DontDraw
    MOV     BX,spriteColor
    MUL     BX
    PUSH    SI
    PUSH    DI
    PUSH    AX
    MOV     AX,SI
    MOV     BX,320
    MUL     BX
    MOV     BX,AX
    
    POP     AX
    POP     DI
    ADD     BX,CX
    ADD     BX,spriteOffset
    MOV     SI,BX
    MOV     BX,VIDEOMEMORY
    MOV     ES,BX
    MOV     ES:[SI],AL
    POP     SI
DontDraw:
    INC     DI
    INC     CX
       
    CMP     CX,spriteWidth
    JNE     ForLoopB
    POP     CX
    INC     CX
    CMP     CX,spriteHeight
    JNE     ForLoopA
    RET
drawSprite ENDP
updateSprites PROC NEAR
    MOV     spriteOffset,0
    MOV     spriteColor,15
    MOV     spriteWidth,16
    MOV     spriteHeight,8     
    MOV     spriteOffset,0
    MOV     spriteToDraw, OFFSET enemyOneA
    CALL    drawSprite
    RET
updateSprites ENDP
END Main

第一个问题是您处于真实模式。这意味着您正在使用64 KiB段。对于"320*200,256色"缓冲区需要64000字节;如果你尝试使用包含所有内容的单一数据段,你将只剩下1535字节用于缓冲区之外的内容(精灵,全局变量等)。它的限制太大了(你迟早会想要动画精灵,关卡/地图/背景布景等等)。

下一个问题是,您不希望在可执行文件中有64000字节的零。通常你会用"bss section"为了避免这种情况(为"假设初始化为零"设置了一个特殊区域)。或者"假定未初始化";数据不在可执行文件中)。

解决这两个问题;我会为缓冲区分配内存(例如,可能使用int 0x21, ah = 0x48 DOS函数),并有一个特殊的缓冲区段。在这种情况下,将缓冲区比特化到显存可能看起来像:

    push es
    push ds
    mov ax,VIDEO_MEMORY_SEGMENT
    mov bx,[bufferSegment]
    mov es,ax
    mov ds,bx
    mov cx,320*200/2
    cld
    xor si,si               ;ds:si = bufferSegment:0 = address of buffer
    xor di,di               ;es:di = VIDEO_MEMORY_SEGMENT:0 = address of video memory
    rep movsw
    pop ds
    pop es
    ret

注1:使用mov cx,320*200/4rep movsd一次复制4个字节会更好/更快,但这将需要32位CPU(在80286或更高版本上不起作用)。如果CPU支持,32位指令在16位代码中工作得很好(它只是一个操作数大小的前缀来改变默认大小,你不需要切换使用保护模式)。

注2:cld(设置清除"方向标志")可能不需要。通常情况下,你在程序开始时清除方向标志一次(或依赖标志"保证在程序开始时由操作系统清除"),这样你就不需要确保每次使用字符串指令(例如rep movsw)时它都是清晰的。

对于写入缓冲区,除了将es设置为buffer_segment而不是将es设置为VIDEO_MEMORY_SEGMENT之外,所有代码都将保持不变。

注3:而不是加载es与相同的值在多个地方(在clearScreen,在drawSprite循环的中间(!)等),最好设置es一次在程序初始化和保存/恢复它时,你需要使用es的其他东西(在比特功能);这样你就可以在所有的绘图代码中避免(相对昂贵的)段寄存器加载(例如mov es,bx)。

也;如果你最终想要一个背景图像(从关卡/地图数据生成),你可以使用第三个"背景缓冲"。这基本上是相同的-为背景分配另外64000字节(并有一个background_segment),然后将背景绘制到缓冲区一次(当你加载关卡或一般地图或…);然后复制"已绘制"背景数据从背景缓冲区到主缓冲区,而不是清除缓冲区,并在其上绘制精灵,然后将缓冲区blit到视频。

最新更新