Here is the most crude animation of 80x80 square.
It works like:
- wait for vertical retrace of VGA to start blank period (beam is returning back to start of screen)
- set whole VRAM to zero ("clear whole screen")
- draw 80x80 square at position "bx"
- adjust bx by +-1 and keep it within 0..239 range
- repeat infinitely
Not sure if you can affect speed of qemu like the cycles count in dosbox (and how accurate its VGA emulation is).
Maybe try this in DOSBOX first (just rename the binary to "test.com", the source below will work as COM file too), to understand the pitfalls of graphics programming.
Then in dosbox you can use Ctrl+F11/F12 to subtract/add machine cycles (speed of PC) to see what happens when this crude algorithm is used on very slow PCs.
On fast PC the screen is cleared before the beam returns to first line, so the square is drawn ahead of beam, and everything looks solid.
But my default setting of dosbox is slow ~286/386 PC-like, which will be still clearing the screen while the beam starts drawing first line on monitor, so it will draw the black empty lines. Once the code will start to draw the square, it will eventually catch up to the beam, somewhere around line ~50, so bottom ~30 lines of the square are visible.
If you will play with machine speed, you can see more artefacts, like the square is completely drawn behind beam (not visible to user), or even blinking (when the whole drawing takes longer than single frame refresh (1000/60 = 16.6ms on 60Hz monitor).
BITS 16
MOV ax,13h
INT 10h ; 320x200 256colour VGA mode
MOV ax,0a000h
MOV es,ax ; video RAM segment
XOR bx,bx ; square position = 0
MOV si,1 ; direction of movement
AnimateLoop:
CALL waitforRetrace ; destroys al, dx
; clear whole screen
XOR di,di
XOR eax,eax
MOV cx,320*200/4
REP STOSD
; draw 80x80 pixels square with color 3
MOV eax,0x03030303
MOV di,bx
MOV dx,80 ; height
drawSquareLoop:
MOV cx,80/4
REP STOSD ; draw 80 pixels (single line)
ADD di,320-80 ; next line address
DEC dx
JNZ drawSquareLoop
; move it left/right
ADD bx,si ; move it first
CMP bx,240
JB AnimateLoop ; 0..239 are OK
; too far on either side, reverse the movement
NEG si
ADD bx,si ; fix position to valid range
JMP AnimateLoop
waitforRetrace:
MOV dx,03dah
waitforRetraceEnd:
IN al,dx
AND al,08h
JNZ waitforRetraceEnd
waitforRetraceStart:
IN al,dx
AND al,08h
JZ waitforRetraceStart
RET
times 510-($-$$) db 0 ; PadZeros:
dw 0xaa55 ; MagicNumber
Now I see then INT 8
timer interrupt is actually BIOS provided, so I can rewrite this example to use that timing to show you difference (VSYNC vs timer animation) ... hmm... I'm extremely reluctant to, because timer animations sucks (I mean, even VSYNC animations have to work with timer to make up for skipped frames, but that's too complex for short example, but timer-based animations sucks inherently by design). I will give it ~10min at most and see if I can make it work...
Ok, the INT 08h
timer based version (don't watch if you are also prone to epileptic seizures from blinking images):
BITS 16
MOV ax,13h
INT 10h
XOR ax,ax
; ds = 0 segment (dangerous, don't do this at home)
MOV ds,ax
MOV ax,0a000h
MOV es,ax ; video RAM segment
AnimateLoop:
; clear whole screen
XOR di,di
XOR eax,eax
MOV cx,320*200/4
REP STOSD
; draw square with color 3
MOV eax,0x03030303
; fetch position from BIOS timer-tick value
; (ticking every 55ms by default)
MOVZX di,byte [0x046C] ; di = 0..255 from [0:046C]
MOV dx,80 ; height
drawSquareLoop:
MOV cx,80/4
REP STOSD
ADD di,320-80 ; next line address
DEC dx
JNZ drawSquareLoop
JMP AnimateLoop
times 510-($-$$) db 0 ; PadZeros:
dw 0xaa55 ; MagicNumber
It has two major problems:
int 8
timer by default is ticking in 55ms, while most of the screens were/are_again 60Hz, so 16.6ms tick is needed to be smooth on 60Hz, even less with higher refresh rates. Now the square moves +1 pixel every 3rd-4th display frame.
even if the timer would be 10ms, it would still blink like crazy, because the erasing of screen + drawing square in new position is not in sync with the display beam.
The 1. can be resolved by reconfiguring the 8253/8254 PIT.
The 2. can be resolved by drawing into offscreen buffer first, and then copying the final image to real VRAM (ideally in VSYNC-ed way to prevent "tearing")
Both example are very crude, basically just demonstrating that "clearing screen + drawing in new position" really does animate stuff. And that it is not sufficient enough to achieve even basic quality.
To get anything reasonable you have use more sophisticated logic, but that strongly depends on what you are drawing and animating and how.
A general purpose approach with VGA is to either use offscreen buffer and copy it to VRAM when drawing is finished (wasting machine cycles on 64k bytes copy.. may sounds laughable today, but it was big deal in 1990).
Or use the VGA control registers to set up one of the unofficial "x modes" and set up the VGA memory layout in a way to support double/triple buffering scheme, so you draw new frame directly into VRAM, but into the hidden part, and when the drawing is finished, you switched the displayed part of VRAM to show the newly prepared content. This helped to avoid the 64k copy, but writing to VRAM was actually quite slow, so it was worth the effort only in situations when you had little overdrawing of pixels. When you had lot of overdraw, it was already too slow (no chance for 60FPS), and drawing it offscreen in ordinary RAM made it actually faster, even with the final 64k copy to VRAM.