First post, by rwos
Hi! I hope I'm in the right place.
I'm currently wasting my free time writing a little CGA game for the 8088, and it's progressing very very slowly, but I thought I'd detail some questionable techniques I've found, because I love reading these kinds of things myself.
The first thing (and the only thing so far), is a little title-screen animation. It looks like this:
(The display mode is CGA's 16-color 40x25 text mode but with 2 scanlines per character - lowres "ANSI from Hell" basically. The background image is placeholder art but that's sort of how it'll look.).
To implement this, I've come up with a horrible, new (probably not, but new to me) technique that I call, uh, "bitmap mask compiled to infinitely looping randomly indexable code". Or something:
So, we're moving a mask in front of a background image that stands still:
First, the background image is plotted on the screen, everywhere, but with the colors set to black/black wherever the "SEND IT" mask is black. So far so normal (though this is slightly hairy for reasons that will become clear shortly).
We wait a bit, then the animation starts. The mask is actually not a bitmap, instead it's a 64 blocks wide map of code, each block being four bytes. Each block corresponds to one screen block/character/color-byte (the mask is wider than the screen).
For each line, the mask contains a MOVSB wherever it changes from black to "transparent", and a STOSB wherever it changes back to black (as seen starting from the left side, as we're moving it to the left). The rest is filled with filler instructions that advance DI and SI. Each line also has a jump back to the start at the end. In effect, each line is a ring-buffer-like construct, one infinite loop of code.
The actual animation starts like this - we compute entry and exit points in and out of the mask for each line, depending on the frame counter (each frame starts one code block later). We then add a RET at the exit point, and just CALL into the entry point. All of this is relatively easy since each 64*4 code block line of the mask is 256 bytes long, which means it all neatly wraps around if we do our math in 8 bits.
Since DS:SI is set up to copy from the background image, and ES:DI is set to the screen memory, the code then MOVSB's the color from the background image, or STOSB's black/black, for every block that changed from the last frame.
All of this is kind of cute, an absolute nightmare to debug, and I'm proud I got it working, though slightly puzzled why I thought this is a good idea. It's also very probably *slower* than doing it in any kind of normal way - especially since the actual mask used has a large section of all-black, which is very inefficient to skip over in inc SI; inc DI increments. But I just think it's neat.
On the 5150 (I only have MartyPC but that should be reasonably accurate), this isn't quite fast enough to update the screen inside the vertical retrace, so there is some screen tearing visible. I also have no real timing code around this, I just wait for three vertical retrace events after drawing one frame. Waiting for a bit at the end also seems to make the screen tearing a bit less noticeable.
The code is something like this (not directly assemble-able, it's all a bit embedded in other things - just to get the idea across):
; assumes:
; DS:SI - start of pic_title + 1
; ES:DI - start of video memory + 1
; CLD
; DL - frame counter, increases by one every frame
draw_title_frame MACRO
; CX - loop counter (lines)
mov cx, TITLEMASK_LENGTH
push dx
__all_lines:
; BX - index into titlemask line (offset from start of the line)
; BL = DL * TITLEMASK_STRIDE (automatically wraps around) = DX * 4
mov bl, dl
shl bl, 1
shl bl, 1
xor bh, bh
; DX - now pointer to titlemask line (start of line)
mov dx, OFFSET titlemask
add dx, bx
; BP - pointer to end of titlemask line (wraps around line-wise)
__end_of_line_segment EQU (DRAW_SCREEN_SIZE_X * TITLEMASK_STRIDE) - 1
mov ax, bx
add ax, __end_of_line_segment
xor ah, ah
mov bp, OFFSET titlemask
add bp, ax
xor ax, ax ; clear AX again, to paint black blocks black
__screen_line:
; draw one line
; first, add a RET at the end
mov byte ptr cs:[bp], RET_CODE
; call into line
call dx
; undo RET adding
mov byte ptr cs:[bp], RET_REPLACEMENT
inc di ; do what the RET had overwritten
; increase DX and BP so they point to the next line
add dx, titlemask_l_1 - titlemask_l_0
add bp, titlemask_l_1 - titlemask_l_0
loop __screen_line
pop dx
ENDM
The mask looks like this (generated from a black/white bitmap by a custom tool):
; DO NOT EDIT - generated by tools/mask from img/titlemask.png
; titlemask is a list of 64*4 byte (sort of) ring buffers, one per output line
; There are TITLEMASK_length lines (55).
; It all consists of:
; - inc si; inc si; inc di; inc di => skipping a block
; - stosb; inc si; inc di; inc di => erasing a block
; - movsb; inc si; xchg ax, ax; inc di => painting a block in
; If a RET is added, this can be CALLed into every 4 bytes.
; The RET can conveniently overwrite the 'inc di' at the end.
RET_CODE EQU 0c3h
RET_REPLACEMENT EQU 47h
MOVSB_CODE EQU 0a4h
STOSB_CODE EQU 0aah
INC_SI_CODE EQU 46h
TITLEMASK_STRIDE EQU 4
TITLEMASK_length EQU 55
titlemask:
titlemask_l_0:
DB 46h,46h,47h,47h, 46h,46h,47h,47h, 0a4h,46h,90h,47h, 46h,46h,47h,47h
DB 46h,46h,47h,47h, 0aah,46h,46h,47h, 46h,46h,47h,47h, 0a4h,46h,90h,47h
DB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h
DB 0aah,46h,46h,47h, 0a4h,46h,90h,47h, 46h,46h,47h,47h, 0aah,46h,46h,47h
DB 46h,46h,47h,47h, 0a4h,46h,90h,47h, 46h,46h,47h,47h, 0aah,46h,46h,47h
DB 0a4h,46h,90h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h
DB 46h,46h,47h,47h, 0aah,46h,46h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h
DB 46h,46h,47h,47h, 46h,46h,47h,47h, 0a4h,46h,90h,47h, 46h,46h,47h,47h
DB 0aah,46h,46h,47h, 0a4h,46h,90h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h
DB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 0aah,46h,46h,47h
DB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h
DB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h
DB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h
DB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h
DB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h
DB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h
titlemask_end_l_0:
jmp titlemask_l_0
titlemask_l_1:
DB 46h,46h,47h,47h, 46h,46h,47h,47h, 0a4h,46h,90h,47h, 46h,46h,47h,47h
etc and so on
The initial painting looks something like this:
draw_initial_title_screen PROC
; from pic_title
mov ax, SEG pic_title
mov ds, ax
mov si, OFFSET pic_title + DRAW_TITLE_SCREEN_OFFSET
; to video memory
mov ax, CGA_SEGMENT
mov es, ax
mov di, DRAW_TITLE_SCREEN_OFFSET
cld
; DH - loop counter (lines)
; BX - offset into titlemask
mov dh, TITLEMASK_LENGTH
mov bx, 0
__all_lines:
; draw one line
; CX - loop counter (words of line)
; AX - current mode (1 if from background, 0 if black)
; DL - scratch
xor ax, ax ; start black
mov cx, DRAW_SCREEN_SIZE_X
__screen_line:
mov dl, byte ptr [titlemask+bx]
cmp dl, MOVSB_CODE
je __switch_to_paint_from_background
cmp dl, STOSB_CODE
je __switch_to_paint_it_black
jmp short __paint
__switch_to_paint_from_background:
mov ax, 1
jmp short __paint
__switch_to_paint_it_black:
xor ax, ax
__paint:
test ax, ax
jz __paint_black
__paint_bg:
movsw
jmp short __screen_line_end
__paint_black:
movsb ; copy pixel data from background
stosb ; but set color data to black
inc si
__screen_line_end:
add bx, TITLEMASK_STRIDE
loop __screen_line
; loop to next line
dec dh
jz __all_lines_end
; jump over the rest of the 64*4 titlemask line
add bx, titlemask_l_1 - titlemask_end_l_0 + (64*TITLEMASK_STRIDE) - (DRAW_SCREEN_SIZE_X*TITLEMASK_STRIDE)
jmp __all_lines
__all_lines_end:
ret
ENDP