• Please review our updated Terms and Rules here

CGA Text Blit

neilobremski

Experienced Member
Joined
Oct 9, 2016
Messages
55
Location
Seattle, USA
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:

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).
 
Back
Top