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 framedraw_title_frame MACRO; CX - loop counter (lines)mov cx, TITLEMASK_LENGTHpush dx__all_lines:; BX - index into titlemask line (offset from start of the line); BL = DL * TITLEMASK_STRIDE (automatically wraps around) = DX * 4mov bl, dlshl bl, 1shl bl, 1xor bh, bh; DX - now pointer to titlemask line (start of line)mov dx, OFFSET titlemaskadd dx, bx; BP - pointer to end of titlemask line (wraps around line-wise)__end_of_line_segment EQU (DRAW_SCREEN_SIZE_X * TITLEMASK_STRIDE) - 1mov ax, bxadd ax, __end_of_line_segmentxor ah, ahmov bp, OFFSET titlemaskadd bp, axxor ax, ax ; clear AX again, to paint black blocks black__screen_line:; draw one line; first, add a RET at the endmov byte ptr cs:[bp], RET_CODE; call into linecall dx; undo RET addingmov byte ptr cs:[bp], RET_REPLACEMENTinc di ; do what the RET had overwritten; increase DX and BP so they point to the next lineadd dx, titlemask_l_1 - titlemask_l_0add bp, titlemask_l_1 - titlemask_l_0loop __screen_linepop dxENDM
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 0c3hRET_REPLACEMENT EQU 47hMOVSB_CODE EQU 0a4hSTOSB_CODE EQU 0aahINC_SI_CODE EQU 46hTITLEMASK_STRIDE EQU 4TITLEMASK_length EQU 55titlemask:titlemask_l_0:DB 46h,46h,47h,47h, 46h,46h,47h,47h, 0a4h,46h,90h,47h, 46h,46h,47h,47hDB 46h,46h,47h,47h, 0aah,46h,46h,47h, 46h,46h,47h,47h, 0a4h,46h,90h,47hDB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47hDB 0aah,46h,46h,47h, 0a4h,46h,90h,47h, 46h,46h,47h,47h, 0aah,46h,46h,47hDB 46h,46h,47h,47h, 0a4h,46h,90h,47h, 46h,46h,47h,47h, 0aah,46h,46h,47hDB 0a4h,46h,90h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47hDB 46h,46h,47h,47h, 0aah,46h,46h,47h, 46h,46h,47h,47h, 46h,46h,47h,47hDB 46h,46h,47h,47h, 46h,46h,47h,47h, 0a4h,46h,90h,47h, 46h,46h,47h,47hDB 0aah,46h,46h,47h, 0a4h,46h,90h,47h, 46h,46h,47h,47h, 46h,46h,47h,47hDB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 0aah,46h,46h,47hDB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47hDB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47hDB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47hDB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47hDB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47hDB 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47h, 46h,46h,47h,47htitlemask_end_l_0:jmp titlemask_l_0titlemask_l_1:DB 46h,46h,47h,47h, 46h,46h,47h,47h, 0a4h,46h,90h,47h, 46h,46h,47h,47hetc and so on
The initial painting looks something like this:
draw_initial_title_screen PROC; from pic_titlemov ax, SEG pic_titlemov ds, axmov si, OFFSET pic_title + DRAW_TITLE_SCREEN_OFFSET; to video memorymov ax, CGA_SEGMENTmov es, axmov di, DRAW_TITLE_SCREEN_OFFSETcld; DH - loop counter (lines); BX - offset into titlemaskmov dh, TITLEMASK_LENGTHmov bx, 0__all_lines:; draw one line; CX - loop counter (words of line); AX - current mode (1 if from background, 0 if black); DL - scratchxor ax, ax ; start blackmov cx, DRAW_SCREEN_SIZE_X__screen_line:mov dl, byte ptr [titlemask+bx]cmp dl, MOVSB_CODEje __switch_to_paint_from_backgroundcmp dl, STOSB_CODEje __switch_to_paint_it_blackjmp short __paint__switch_to_paint_from_background:mov ax, 1jmp short __paint__switch_to_paint_it_black:xor ax, ax__paint:test ax, axjz __paint_black__paint_bg:movswjmp short __screen_line_end__paint_black:movsb ; copy pixel data from backgroundstosb ; but set color data to blackinc si__screen_line_end:add bx, TITLEMASK_STRIDEloop __screen_line; loop to next linedec dhjz __all_lines_end; jump over the rest of the 64*4 titlemask lineadd bx, titlemask_l_1 - titlemask_end_l_0 + (64*TITLEMASK_STRIDE) - (DRAW_SCREEN_SIZE_X*TITLEMASK_STRIDE)jmp __all_lines__all_lines_end:retENDP