Search code examples
assemblyx86tasmblitdouble-buffering

Double buffering in x86 VGA


To bring back some memories I decided to sit down and code a little assembler game in VGA mode 13h - until the point I realized the visual output is flickering as hell.

At first I suspected it might be my clearscreen routine. Indeed by using a STOSW instead of writing a single byte to the video memory a time the flickering is less annoying but still present.

Digging some further I recalled I might have to wait for the vertical retrace and update my screen right after but that didn't make things much better.

So the final solution I'm aware of goes a little like this:

  • do all graphical operations - clearing the screen, setting pixels - on a separate memory region
  • wait for the vertical retrace
  • copy the memory over to the video memory

The theory is of course simple but I just can't figure out how to do my writes to the buffer and ultimately blit it into the video memory!

Here's a striped-down - though working - snippet of my code written for 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

Solution

  • The first problem is that you're in real mode. This means that you're working with 64 KiB segments. For "320*200 with 256 colors" the buffer will need to be 64000 bytes; and if you try to have a single data segment containing everything you'll only have 1535 bytes left for things that aren't the buffer (sprites, global variables, etc). It's too restrictive (sooner or later you're going to want animated sprites, or a level/map/background scenery, or ...).

    The next problem is that you don't want 64000 bytes of zeroes in the executable file. Normally you'd use a ".bss section" to avoid that (a special area for "assumed initialized to zero" or "assumed uninitialized " data that isn't in the executable file).

    To solve both of these problems; I'd allocate memory for the buffer (e.g. maybe using the int 0x21, ah = 0x48 DOS function) and have a special buffer segment. In this case blitting the buffer to video memory might look like:

        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
    

    Note 1: It'd be better/faster to use mov cx,320*200/4 and rep movsd to copy 4 bytes at a time, but this would require a 32-bit CPU (won't work for 80286 or later). If supported by CPU, 32-bit instructions work fine in 16-bit code (it's just an operand size prefix to change the default size and you do not need to switch use protected mode).

    Note 2: The cld (set clear the "direction flag") may be unnecessary. Typically you clear the direction flag once at the start of your program (or rely on the flag being "guaranteed clear by OS at program start") so that you don't need to make sure it's clear every time you use a string instruction (e.g. like rep movsw).

    For writing to the buffer, all your code would remain the same except that you'd set es to buffer_segment instead of setting es to VIDEO_MEMORY_SEGMENT.

    Note 3: Rather than loading es with the same value in multiple places (in clearScreen, in the middle of a loop in drawSprite(!), etc) it'd be better to set es once during program initialization and save/restore it when you need to use es for something else (in the blitting function); so that you can avoid the (relatively expensive) segment register loads (e.g. mov es,bx) in all of the drawing code.

    Also; if you do end up wanting a background image (generated from level/map data, or...) you could use a third "background buffer". This would be mostly the same - allocate another 64000 bytes for the background (and have a background_segment), then draw the background into the buffer once (when you load the level or general the map or ..); then copy the "already drawn" background data from the background buffer to the main buffer instead of clearing the buffer, and draw your sprites on it, and then blit the buffer to video.