CC65, the Atari, and Graphics: Part 1 of 10,000.
This seems to be a relatively common issue for folks. It certainly was for me when I started playing around with CC65. Here's an example, CC65 linker config file, that should work out of the box.
The following is a short example on how to use graphics mode 8 from CC65. Before we get too far we need to create a few typedefs and defines. These elements make later code easier to read (IMHO). If you're familiar with ACTION you'll recognize what BYTE and WORD represent. I leave it as an exercise to the reader as to the utility of upper casing BYTE and WORD.
typedef unsigned char byte; typedef unsigned int word; #define SAVMSC *((word *) 0x0058)
SAVMSC is a pointer to the lowest addressable area of screen memory. It's a zero page variable maintained by the operating system of the Atari 8bit. There are plenty of resources elsewhere discussing the importance of zero page variables, why you want to use them, as well as their relative lack of availability from within CC65 (you can use them, but the OS and the CC65 runtime consume a lot of page zero).
With that out of the way we can now call a CC65 library routine to enter graphics mode.
word gfxbuffer; _graphics( 8 + 16 ); gfxbuffer = SAVMSC; RAMTOP = RAMTOP - 32;
_graphics() is an Atari 8bit specific routine that is part of the relatively small Atari specific library that comes with CC65. The single parameter operates in a similar fashion to the Atari Basic GRAPHICS command. In this example the parameter being passed to _graphics() is 24 (8, the graphics mode, plus 16 for full screen). The next line of code saves the current value of SAVMSC to a global word variable named gfxbuffer. Finally I modify RAMTOP by reducing it 32 pages to protect the ~8k of space used by the graphics 8 screen. What's a page? In the 6502 world a page of memory is 256 bytes. Do the math...256 * 32 = 8192 bytes.
So. We have our defines. We have code to enter graphics mode 8. How do we use it? The resolution of a graphics 8 screen is 320 x 192 pixels. How does the Atari manage that much space with only 8k of ram? Graphics 8 screen memory is laid out as a colletion of 192 rows of 40 bytes each. Each byte contains 8 bits - so 40 bytes * 8 bits per byte = 320 pixels. Each pixel on the graphics 8 screen is represented by a single bit. This is why, by default, you can only use one color on a graphics 8 screen. Yes. You can do artifacting on tube monitors to get more colors and YES you can use DLIs and the like to force color changes on certain lines but those are advanced topics. We're going to keep things simple for the time being. Instead we're going to talk about plotting pixels on the screen or in other words how do you turn on the pixel at coordinate (100, 100)? This is where bitmasks come in.
In my early days of using CC65 I wrote code like the following.
static const byte bitmasks[ 8 ] = {
0b10000000,
0b01000000,
0b00100000,
0b00010000,
0b00001000,
0b00000100,
0b00000010,
0b00000001
};
void plot( word buf, word x, word y ) {
byte bitmask_to_use = (byte)x % 8;
word b = buf + ( y * 40 ) + x / 8;
byte current_byte;
if( x > 319 || y > 191 ) return;
current_byte = PEEK( b ) | bitmasks[ bitmask_to_use ];
POKE( b, current_byte );
}
The function is pretty straight forward - it accepts a buffer to graphics memory and the X and Y coordinate of where to plot a pixel. I calculate the bitmask to use by using the modulo (%) operator on the supplied X coordinate. Remember that there are eight graphics 8 pixels packed into each byte - so, for example, if the passed X coordinate had the value of 3 the result of X % 8 would equal 3 - or the 4th bitmap in the bitmasks array.
The next line creates a variable B which points to the byte that is to be modified. In this example we, naturally, want to multiply the Y coordinate by 40 (remember - each row in graphics 8 is 40 bytes long) and then add the result of the X coordinate divided by 8.
Afterwards we do basic clip checking to ensure the coordinates can be plotted and then we OR the bitmask with whatever value is presently in RAM.
*BOOM* We plot a pixel.
If you're coming back to the 8bit world after about 30 years you may have forgotten the very basic fact that the 6502 doesn't have instructions to multiply and divide so the CC65 library has functions to do that sort of math for you. Resulting in slow math. Slow drawing. Pain. Suffering...
Here's a better version.
word indexes[ 192 ]; void init_graphics( void ) { byte i; word index = 0; _graphics( 8 + 16 ); for( i = 0; i < 192; i++, index += 40 ) indexes[ i ] = index; } void plot( word buf, word x, word y ) { byte current_byte, bitmask_to_use; word b; if( x > 319 || y > 191 ) return; bitmask_to_use = (byte)x % 8; b = buf + indexes[ y ] + ( x >> 3 ); current_byte = PEEK( b ) | bitmasks[ bitmask_to_use ]; POKE( b, current_byte ); }
In this example we define a global array of words named indexes. Each element in this array contains a memory offset for each row of a graphics 8 screen. In the init_graphics() function we initialize the array.
In this second version of plot we perform the clipping check first and then proceed to the math. We still leverage modulo (%) to determine which bitmask to use but our calculation to determine the byte offset is dramatically different. In this case we leverage our index to quickly determine the offset for the specified Y coordinate and instead of dividing X by 8 we instead shift the value of X three bits to the right. This has the effect of dividing the X coordinate by 8 - but quickly.
How quickly? The first plot() function which used multiplication and division is able to plot ~13.6 pixels per jiffy (or about 816 pixels per second) while the second, faster, version can do ~15.5 pixels per jiffy (or about 930 pixels per second). There are, of course, other optimizations and the use of assembly language to get even faster.
Here's a complete example. Your results may vary but with the emulators I use the general plot routine does about 35 pixels per jiffy (2100 pixels per second) while the optimized version does 3.072 pixels per jiffy (or 184,320 pixels per second). Anyway - I hope this is helpful to someone. I have examples coming up of how to print text on graphics 8, plotting sprites, some graphics 7 stuff, double buffering in graphics 8 and 7, ....
Etc....etc...etc... Anyway - here's the example. I've also attached the source code and the linker config at bottom.
/* * build with: * cl65 --static-locals -t atari -Osir -C atari.cfg -o gr8simple.xex gr8simple.c */ #include <atari.h> #include <peekpoke.h> #include <string.h> #include <stdio.h> #include <stdlib.h> #include <stdbool.h> typedef unsigned char byte; typedef unsigned int word; /* * important memory locations in the Atari OS */ #define SAVMSC *((word *) 0x0058) #define COLOR0 *((byte *) 0x02c4) #define COLOR1 *((byte *) 0x02c5) #define COLOR2 *((byte *) 0x02c6) #define COLOR3 *((byte *) 0x02c7) #define COLBK *((byte *) 0x02c8) #define RTCLOK *((byte *) 0x0012) #define RT2 *((byte *) 0x0013) #define RT3 *((byte *) 0x0014) /* * bitmasks for plotting gr.8 pixels */ static const byte bitmasks[ 8 ] = { 0b10000000, 0b01000000, 0b00100000, 0b00010000, 0b00001000, 0b00000100, 0b00000010, 0b00000001 }; static word indexes[ 192 ]; /* * Returns the current jiffy count. A jiffy is approx. 1/60th of a second. */ long jiffies( void ) { return RTCLOK * 65536 + RT2 * 256 + RT3; } /* * routines to change the default colors - nothing fancy. */ void clear_screen( word buf ) { bzero( (byte *)buf, 7680 ); } void regular_border( void ) { COLBK = 0x00; } void regular_drawing_color( void ) { COLOR1 = 0x0e; } void regular_background_color( void ) { COLOR2 = 0x00; } /* * go into full screen graphics mode 8, change colors * to white on black, and calculate our indexes. */ void init_graphics( void ) { byte i; word index = 0; _graphics( 8 + 16 ); regular_border(); regular_drawing_color(); regular_background_color(); for( i = 0; i < 192; i++, index += 40 ) indexes[ i ] = index; } /* * generic pixel plotter */ void plot( word buf, word x, word y ) { byte *b; if( x > 319 || y > 191 ) return; b = (byte *)(buf + indexes[ y ] + ( x >> 3 )); *b |= bitmasks[ (byte)x % 8 ]; } /* * optimized to plot a 320 pixel line at a * specific Y coordinate */ void plot_optimized( word buf, word y ) { byte *b = (byte *)(buf + indexes[ y ]); byte i = 40; while( i-- ) *b++ = 0xff; } void main( void ) { word row, col; long s1,s2,f1,f2; init_graphics(); clear_screen( SAVMSC ); s1 = jiffies(); for( row = 0; row < 192; row++ ) for( col = 0; col < 320; col++ ) plot( SAVMSC, col, row ); s2 = jiffies(); clear_screen( SAVMSC ); f1 = jiffies(); for( row = 0; row < 192; row++ ) plot_optimized( SAVMSC, row ); f2 = jiffies(); _graphics( 0 ); printf( "General Plot = %6ld\n", s2 - s1 ); printf( "Pixels/Jiffy = %6ld\n\n", (long)(192 * 320) / (s2 - s1 )); printf( "Optimized Plot = %6ld\n", f2 - f1 ); printf( "Pixels/Jiffy = %6ld\n\n", (long)(192 * 320) / (f2 - f1 )); while( true ) ; }
- 2
0 Comments
Recommended Comments
There are no comments to display.