Classic Computer Magazine Archive COMPUTE! ISSUE 50 / JULY 1984 / PAGE 108

64 EXPLORER

Larry Isaacs

A complete drawing package should allow the user to print characters on the bitmapped display. This month and next I will discuss this topic, and give more examples on the use of the drawing routines presented last month.

There are two methods for printing characters on a bitmapped display. We can POKE the dot patterns of the characters to the bitmapped RAM, or we can draw the characters onto the display.

Let's take a look at the first method, which is faster because no line-drawing routines are required.

POKEing To The Bitmap

The first step in POKEing characters to a bitmapped display is to choose the cell size, or dimensions, of our character set. The choice of the cell size can greatly affect the complexity of the routines which print the characters. If a convenient size is chosen, the routines will be simplified; if you are up for a challenge, you can write the routines to accept a variable cell size.

We will use a cell size of 8 dots high by 8 dots wide, for two reasons. First, a width of 8 dots is the number of dots which can be held in a byte. Second, there already exists a set of 8 x 8 characters in the 64's character ROM.

Actually, the 64's normal character display mode is very similar to what we want to accomplish in a bitmapped mode. The process involves character cells and some method of transferring the character dot patterns to the display. However, in the normal character display mode, the format is determined by the character codes found in a character array called screen memory. In screen memory, you can change only whole 8 x 8 characters, so your effective resolution winds up being 40 columns by 25 lines.

Blending Characters And The Bitmap

When using a bitmap display, you can control each dot. This implies that you can place a character at any X,Y position on the screen. This can certainly be done, though it is more difficult than placing a character code in screen memory. What complicates the task somewhat is that the 64's VIC-II chip organizes a bitmap display as groups of 8 x 8 dot cells.

It's possible for the 8 × 8 dot character pattern to span as many as four of the bitmap cells, two horizontally and two vertically. This doesn't create much of a problem vertically, but horizontally the bytes in the character dot pattern may have to be moved or shifted to span two bytes. In addition, when the bytes are added to the bitmap, the routine must not disturb the dots outside the shifted 8 dots of the character pattern.

Next, we must decide how to transfer the dot patterns so they will be visible against the bitmapped background.

Using Conditional Logic

One way of transferring the dot pattern is to add (logical OR) the dots in the pattern to the dots already in the display. Dots which are on in the character dot pattern are also turned on in the display. Dots which are on in the display remain on. This avoids erasing the background as a character is printed to the bitmapped display, but can result in illegible characters if there are too many dots already turned on in the background.

Another way to transfer the dot pattern is to flip (Exclusive-OR) the dots in the pattern into the bitmapped RAM. Dots in the bitmapped RAM which correspond to on dots in the dot pattern are flipped to the opposite state. The advantage of this technique is that it will make characters visible regardless of whether the background is on or off. However, characters can still be illegible if the background is not predominantly either on or off.

Or the transfer could be accomplished by writing the pattern directly into the bitmapped RAM. This type of transfer replaces the background with the character cell. We will use this technique.

A BASIC Example

Let's first demonstrate how the required routines might be implemented in BASIC. Unfortunately, like the drawing routines presented in earlier columns, the character routines are too slow to be really useful. To enhance their value as an example, we'll try to illustrate modular programming style as well.

One of the main aspects of modular programming is breaking main or primary tasks into smaller, more manageable tasks. Once the tasks have been broken down sufficiently, each may be implemented in a single routine. The more independent each of these separate routines is, the better. This allows you to concentrate on the details involved with the routine as it is written, without being distracted by the details involved with other routines. To show how printing to the bitmapped display might be broken into modules, let's take a look at the logical sub-divisions of this task.

Although this program isn't really complex enough to justify a modular approach, I prefer to keep the functions or tasks in separate routines, so long as the routines don't become embarrassingly simple. This helps while debugging, since the symptoms of the bug often eliminate a majority of the routines from consideration. It also helps keep you from accidentally tangling functions together.

When functions get tangled or intertwined, making one change may require making other changes, leading to a snowball effect. And finally, it is good practice to keep functions divided into separate routines when you write a complex program. How well the tasks are divided up can greatly affect how much effort it takes to write and debug the program.

Breaking Down The Task

Putting a character in a bitmapped display will involve transferring bytes into bitmapped RAM, so we will need a routine which does the transferring. We need another routine to calculate the character's position. We also need to know what to write; this will require two routines. We need a routine which will find and read the appropriate bytes in the character ROM. However, the dot patterns are organized based on screen codes, which are different from the Commodore 64 ASCII codes you normally print. This means we need a routine to convert the ASCII code to the corresponding screen code.

Finally, we need a routine to do the horizontal shifting necessary when the character byte needs to span two bytes in the bitmapped RAM. This gives us five routines to be implemented:

  1. Convert the character to screen code
  2. Read the character's dot pattern
  3. Calculate its position in the bitmap and amount of shift
  4. Shift a dot-pattern byte
  5. Put the dot-pattern byte in the bitmap

By dividing the tasks into well-defined and independent sections, it will be a little easier to implement them than if you tried to throw it all together in one routine. For example, converting the ASCII character code to a screen code can be done without concerning ourselves with where the ASCII code came from, or for what the screen code will be used. The shift section does not need to account for where the shift amount came from or what will be done with the shifted bytes.

Combining The Modules

Once we build the character print routine from these five sections, it is simple to build a string print routine using the character print routine. The result might be a BASIC program like the one that accompanies this article. The program uses the machine language routines discussed in this column in previous issues. Before running this program, you must run the BASIC loader presented in the May 1984 issue. The subroutine at line 100 converts ASCII code CH to the equivalent screen code SC. The subroutine at line 200 uses screen code CH to read the associated dot pattern into the array DP(). This subroutine also uses CP which points to the base of the character dot patterns in ROM.

The subroutine at line 300 uses the coordinates in X and Y to calculate an offset OF into the bitmap, and the shift amount SH. The subroutine at line 400 uses the shift amount SH to right-shift the byte in BY partially into B2. This means shifting dots out of the right end of the byte and into the left end of the other byte. This shift routine also makes the mask bytes, M1 and M2.

Finally, the subroutine at line 500 writes the bytes into the bitmap base of the offset OF, calculated earlier. This routine also uses the mask bytes to keep the necessary old bits from the bitmap bytes, before adding the new dot pattern bits. The subroutine at line 600 prints the character at the current coordinates specified by X and Y, and the subroutine at 700 prints a string at X,Y.

Logical Math

I have used logical operators (OR and AND) rather than division and the INT functions. For example, in line 320, the term (X AND -8) gives the same result as INT(X/8)*8. In the subroutine at line 200, the POKEs are required to turn off interrupts and make the character ROM accessible to the BASIC program.

The main routine uses the string-printing routine at line 700 to label the vertical axis for the plot of a sine wave. As you will see, the character printing is pretty slow. This part of the program would be much more useful written in machine language. Next month I will discuss the drawing method of putting characters in the bitmapped display, and present machine language routines for both.

Characters On A Bitmapped Display

10 REM PRINT CHARACTERS TO BIT-MAP				: rem 63
20 JV = 49152 : REM JUMP TABLE				: rem 6
30 CP = 53248 : REM LOC. OF CHAR. PATTERNS			: rem 181
40 POKE 785, PEEK (JV + 28) : REM SETUP USR ()		: rem 8
50 POKE 786, PEEK (JV + 29)					: rem 17
60 GOTO 1000						: rem 96
100 REM CONVERT CHAR. TO SCREEN CODE                            : rem 96
110 IF CH > 31 AND CH < 64 THEN SC = CH : RETURN                : rem 249
120 IF CH > 63 AND CH < 96 THEN SC = CH - 64 : RETURN           : rem 155
130 IF CH > 127 AND CH < 128 THEN SC = CH - 32 : RETURN         : rem 200
140 IF CH > 127 AND CH < 192 THEN SC = CH - 64 : RETURN         : rem 251
150 SC = CH - 128 : RETURN					: rem 214
200 REM GET CHARACTER DOT PATTERN				: rem 232
210 POKE 56334, PEEK (56344) AND 254				: rem 221
220 POKE 1, PEEK (1) AND 251				: rem 50
230 FOR IX = 0 TO 7					: rem 100
240 DP(IX) = PEEK (CP + SC * 8 + IX) : NEXT			: rem 202
250 POKE 1, PEEK (1) OR 4					: rem 159
260 POKE 56334, PEEK (56334) OR 1				: rem 69
270 RETURN						: rem 121
300 REM CALC OFFSET AND SHIFT COUNT				: rem 43
310 TY = 199 - Y : SH = X AND 7				: rem 27
320 OF = (TYAND - 8) * 40 + (XAND - 8) + (TYAND7)		: rem 106
330 RETURN						: rem 118
400 REM SHIFT BYTE TO CORRECT POSITION			: rem 84
410 B2 = 0 : M1 = 0 : M2 = 255 : IF SH = 0 THEN RETURN		: rem 13
420 FOR K = 1 TO SH : B2 = B2/2				: rem 52
430 IF BY AND 1 THEN B2 = B2 OR 128				: rem 85
440 BY = BY/2 : M1 = (M1/2) OR128 : M2 = M2/2 : NEXT		: rem 28
450 RETURN						: rem 121
500 REM PUT BYTE AT X, Y					: rem 24
510 GOSUB 300 : REM CALCULATE OF & SH			: rem 171
520 GOSUB 400 : REM SHIFT OVER				: rem 131
530 AD = 57344 + OF : REM GET ADDRESS FOR BY			: rem 167
540 POKE AD, USR(OF) AND M1 OR BY				: rem 230
550 IF SH = 0 THEN RETURN					: rem 64
560 POKE AD + 8, USR(OF + 8) AND M2 OR B2			: rem 136
570 RETURN						: rem 124
600 REM PUT CHARACTER AT X, Y				: rem 114
610 GOSUB 100 : REM CONVERT CH				: rem 114
620 GOSUB 200 : REM READ DOT PATTERN				: rem 233
630 Y = Y + 8 : REM PUT CHAR. FROM TOP DOWN			: rem 173
640 FOR IX = 0 TO 7: Y = Y-1: BY = DP (IX)			: rem 136
650 GOSUB 500 : REM PUT BYTE				: rem 251
660 NEXT : RETURN						: rem 245
700 REM PUT STRING S$ AT X, Y				: rem 52
710 FOR SP = 1 TO LEN(S$)					: rem 218
720 CH = ASC (MID$(S$, SP,1))				: rem 123
730 GOSUB 600 : REM PUT THE CHARACTER			: rem 53
740 X = X + 8 : NEXT					: rem 100
750 RETURN						: rem 124
1000 REM MAIN ROUTINE					: rem 240
1010 SYS JV : SYS JV + 6, 0 : SYS JV + 9, 0, 1		: rem 237
1020 FOR I = 0 TO 10					: rem 100
1030 LB = -1 + I*.2 : S$ = STR$(LB)				: rem 213
1050 X = 5 : Y = 46 + 10* I : GOSUB 700 : NEXT		: rem 147
1060 SYS JV + 12, 32, 50 : SYS JV + 18, 32, 150		: rem 214
1070 SYS JV = 12, 32, 100 : SYS JV + 18, 319, 100		: rem 54
1080 FOR I = 0 TO 10					: rem 106
1090 X = 30 : Y = 50 + 10 * I				: rem 246
1100 SYS JV + 12, X, Y : SYS JV + 18, X + 4, Y		: rem 205
1110 NEXT							: rem 2
1120 SYS JV + 12, 32, 100 : PI = 3.1416			: rem 124
1130 SX = 256/(2*PI) : SY = 50				: rem 71
1140 FOR I = 0 TO 2*PI STEP 2*PI/100				: rem 236
1150 SYS JV + 18, 32 + I*SX, 100 + SIN(I)*SY			: rem 22
1160 NEXT							: rem 7
9000 GET Z$ : I Z$ = "" THEN 9000                		: rem 231
9010 SYS JV + 3						: rem 199