neilobremski
Experienced Member
I went back and forth over whether or not to write my own text rendering routine in assembler. DOS supplies a decent print string function [SUP][1][/SUP] but it turned out to be insufficient to my needs. My requirements include the ability to change color mid-stream as well as repeat lines of pixel data twice to create a "large" font. Here's a screenshot from a test of the C functions I created back in January:
There were three functions back then, one for each font size with the code being extremely specialized for the tiny font. I won't be covering the tiny font in this post. Now this new method will do something the old one didn't and that's being able to change the color mid-stream. Here's the most recent test of that:
Rather than specifying a length and supporting clipping, the new method looks for a double NULL [SUP][2][/SUP] at the end of the string. A single NULL causes a check for color change which includes both foreground and background. This has the side effect that strings can override the caller-specified color.
I went through several code iterations before settling on bit streaming and shifting. The main problem to solve is that the font data is monochromatic with only a single bit per pixel either lit or unlit, while the output is color that the caller can specify (or is specified in the string). So initially I thought I'd create a couple of 256 byte tables to translate the font bits into pixel bytes only to realize that I would have to duplicate these two tables for every color combination. Sadly then, because I've been wanting to try out XLAT in something, I threw this idea out. Next I figured I'd have all the shifts and masks unrolled rather than looping but I ran into the issue of lots of branching depending on whether or not the foreground or background color is being written.
Finally, as I mentioned, I went with a bit stream. I had this nifty idea to reuse AX for both the font data and resulting pixel data. Remember that the font data is in a single byte that gets translated to two bytes; CGA mode 4 is 2 bits per pixel so I'm merely doubling the bits from 8 to 16. If I kept adding the color at the beginning of AX and then shifting left to make room for more pixels, I could continue reading right-to-left from the 8 bit font data as it shifted left.
Let me elaborate. The font data is loaded into AL and the highest bit is the first pixel (AX & 0080h). I shift twice left to make room for that pixel and shift the bit mask left once to move RIGHT to the next highest bit. Because I am moving from highest to lowest when reading the font data, that data is still available up until the very last pixel is being masked into AX.
Check it out in the table below showing the binary value of AX and the changes it goes through. The numbers indicate which pixel number a bit is for, "?" is data I don't care about, and "P" is where the pixel bits are added:
And here is the routine tying it all together:
Footnotes:
[SUP][1][/SUP]. INT 21h, AH = 09h will write the string pointed to by DS:DX.
[SUP][2][/SUP]. NULL in my definition is a zero byte (00h).
There were three functions back then, one for each font size with the code being extremely specialized for the tiny font. I won't be covering the tiny font in this post. Now this new method will do something the old one didn't and that's being able to change the color mid-stream. Here's the most recent test of that:
Rather than specifying a length and supporting clipping, the new method looks for a double NULL [SUP][2][/SUP] at the end of the string. A single NULL causes a check for color change which includes both foreground and background. This has the side effect that strings can override the caller-specified color.
I went through several code iterations before settling on bit streaming and shifting. The main problem to solve is that the font data is monochromatic with only a single bit per pixel either lit or unlit, while the output is color that the caller can specify (or is specified in the string). So initially I thought I'd create a couple of 256 byte tables to translate the font bits into pixel bytes only to realize that I would have to duplicate these two tables for every color combination. Sadly then, because I've been wanting to try out XLAT in something, I threw this idea out. Next I figured I'd have all the shifts and masks unrolled rather than looping but I ran into the issue of lots of branching depending on whether or not the foreground or background color is being written.
Finally, as I mentioned, I went with a bit stream. I had this nifty idea to reuse AX for both the font data and resulting pixel data. Remember that the font data is in a single byte that gets translated to two bytes; CGA mode 4 is 2 bits per pixel so I'm merely doubling the bits from 8 to 16. If I kept adding the color at the beginning of AX and then shifting left to make room for more pixels, I could continue reading right-to-left from the 8 bit font data as it shifted left.
Let me elaborate. The font data is loaded into AL and the highest bit is the first pixel (AX & 0080h). I shift twice left to make room for that pixel and shift the bit mask left once to move RIGHT to the next highest bit. Because I am moving from highest to lowest when reading the font data, that data is still available up until the very last pixel is being masked into AX.
Check it out in the table below showing the binary value of AX and the changes it goes through. The numbers indicate which pixel number a bit is for, "?" is data I don't care about, and "P" is where the pixel bits are added:
Code:
Pixel 0: AX = ???? ???? 0123 4567 & 0080h -> ???? ??01 2345 67PP
Pixel 1: AX = ???? ??01 2345 67PP & 0100h -> ???? 0123 4567 PPPP
Pixel 2: AX = ???? 0123 4567 PPPP & 0200h -> ??01 2345 67PP PPPP
Pixel 3: AX = ??01 2345 67PP PPPP & 0400h -> 0123 4567 PPPP PPPP
Pixel 4: AX = 0123 4567 PPPP PPPP & 0800h -> 2345 67PP PPPP PPPP
Pixel 5: AX = 2345 67PP PPPP PPPP & 1000h -> 4567 PPPP PPPP PPPP
Pixel 6: AX = 4567 PPPP PPPP PPPP & 2000h -> 67PP PPPP PPPP PPPP
Pixel 7: AX = 67PP PPPP PPPP PPPP & 4000h -> PPPP PPPP PPPP PPPP
And here is the routine tying it all together:
Code:
a 1C00
; ----------------------------------------------------------------------------
; TextBlit() :TEXTBLIT
;
; [input] [output] [within]
; AX X (trashed) pixel bits
; BX Y (trashed) BH=FgC&03, BL=BgC&03
; CX CH=Color (trashed) CH=XCount, CL=YCount
; DX DL=LineHeight (trashed) DH=Repeat, DL=LineRep
; SI pString+Offs pString+Len BIOS font data
; DS pString pString BIOS font data
; ES vidptr vidptr vidptr
; DI pixel location pixel location
; BP (trashed) pixel mask
;
MOV DI, AX ; C00
SHR DI, 1 ; C02
SHR DI, 1 ; C04 start DI at X1 / 4
TEST AL, 01 ; C06 is this an odd scanline?
JZ 1C0E ; C08 >TEXT_DI_OFFS
ADD DI, 2000; C0A add 0x2000 for odd scanline
MOV AX, BX ; C0E :TEXT_DI_OFFS
AND AL, FE ; C10 clear bit 0
SHL AX, 1 ; C12
SHL AX, 1 ; C14
SHL AX, 1 ; C16
ADD DI, AX ; C18 add *64 to DI
SHL AX, 1 ; C1A
SHL AX, 1 ; C1C
ADD DI, AX ; C1E add *16 to DI (*64 + *16 = *80 bytes per line)
;
MOV AL, CH ; C20 load color
JMP 1C80 ; C22 >TEXT_COLOR
;
; TEXTBLIT: OUTER LOOP (string characters)
;
LODSB ; C24 read next character from string :TEXT_LOOP_CHR
OR AL, AL ; C25
JZ 1C7B ; C27 >TEXT_NULL
;
PUSH DI ; C29
PUSH SI ; C2A
PUSH DS ; C2B
CALL 0370 ; C2C >BIOSCHAR
MOV CL, 08 ; C2F CL = YCount (scanlines remaining)
CLD ; C31 read/write forward
; ------------------------------------------------------------
; TEXTBLIT: INNER LOOPS (Y|scanlines and X|pixels)
;
LODSB ; C32 read next byte of font pixel map :TEXT_LOOP_Y
MOV CH, 08 ; C33 CH = XCount (pixels remaining)
MOV BP, 80 ; C35 BP = font pixel mask (moves from left to right)
MOV DH, DL ; C38 DH = Line Repeats (reset to line height)
;
TEST AX, BP ; C3A :TEXT_LOOP_X
JNZ 1C4E ; C3C if (next bit set) >TEXT_PSET_FG
;
SHL AX, 01 ; C3E :TEXT_PSET_BG
SHL AX, 01 ; C40
OR AL, BL ; C42 add BACKGROUND COLOR to AX
SHL BP, 01 ; C44 shift font mask to align properly on AX
DEC CH ; C46
JNZ 1C3A ; C48 if (--XCount) >TEXT_LOOP_X
XCHG AL, AH ; C4A fix byte order for little endian CGA
JMP 1C5C ; C4C >TEXT_LINE_DRW
;
SHL AX, 01 ; C4E :TEXT_PSET_FG
SHL AX, 01 ; C50
OR AL, BH ; C52 add FOREGROUND COLOR to AX
SHL BP, 01 ; C54 shift font mask to align properly on AX
DEC CH ; C56
JNZ 1C3A ; C58 if (--XCount) >TEXT_LOOP_X
XCHG AL, AH ; C5A fix byte order for little endian CGA
;
NOP ; C5C debugging breakpoint :TEXT_LINE_DRW
STOSW ; C5D plot 8 pixels for this scanline
ADD DI, 1FFE; C5E
CMP DI, 4000; C62
JB 1C6C ; C66 >TEXT_LINE_REP
SUB DI, 3FB0; C68
DEC DH ; C6C :TEXT_LINE_REP
JNZ 1C5C ; C6E if (--Repeat) >TEXT_LINE_DRW
DEC CL ; C70
JNZ 1C32 ; C72 >TEXT_LOOP_Y
; ------------------------------------------------------------
;
POP DS ; C74 :TEXT_CHR_NEXT
POP SI ; C75
POP DI ; C76
INC DI ; C77
INC DI ; C78
JMP 1C24 ; C79 >TEXT_LOOP_CHR
;
LODSB ; C7B read character after NULL :TEXT_NULL
OR AL, AL ; C7C
JZ 1C8E ; C7E >TEXTBLIT_RET
MOV BL, AL ; C80 :TEXT_COLOR
SHL BX, 01 ; C82
SHL BX, 01 ; C84
MOV BL, AL ; C86
AND BX, 0303; C88 mask color bits so only first two remain
JMP 1C24 ; C8C >TEXT_LOOP_CHR
RET ; C8E :TEXTBLIT_RET
Footnotes:
[SUP][1][/SUP]. INT 21h, AH = 09h will write the string pointed to by DS:DX.
[SUP][2][/SUP]. NULL in my definition is a zero byte (00h).