‹- Posts

More graphics programming for 8-bit 6502-based computers in C using CC65

Published on: September 29, 2025

Introduction

About 9 months ago I wrote a post about graphics programming for 6502 based computers, if you haven't read it I encourage you do so to have the context for understanding this post. I decided to come back to this, and do some more with it. This is my follow up post, things are not perfect, and I've still got improvements to make.

When I was writing my original post, I plotted the coordinates by hand on a 320x200 plane. While this works for static graphics, it does not work when you need to dynamically update the screen.

I was drawing shapes at set coordinates, and their position on the plane was hardcoded. What if I want to update their positions or scaling? This is where I came up with a new approach.

For line shapes, I created an array of lines. These lines start at (0,0) which in this instance is the top left of the screen.

/* Array of a shape to be drawn
 * {x1, y1, x2, y2}
 * {x1, y1, x2, y2}
 * {x1, y1, x2, y2}
 * ...
 */

The base coordinates of a 37x37 square are:

const unsigned int square[4][4] = {
    {0, 0, 0, 37},
    {0, 0, 37, 0},
    {37, 0, 37, 37},
    {37, 37, 0, 37}
};

The tgi_line() function begins drawing at (0,0) and keeps going to (0,37). Then it begins drawing at (0,0) and stops at (37,0). Then it begins drawing at (37,0) and stops at (37,37). Lastly it begins drawing at (37,37) and stops at (0, 37).

Square at the 0,0 base coordinates

The way to position objects on the screen is by adding an x and/or y offset to the base coordinates.

I will show a visualization using a program called Pixen available on macOS. Using this program, I can draw on a 320x200 space representative of the Commodore 64 display, and the bottom tells me which coordinates a pixel is on. This is great for prototyping what you want to draw on the screen.

Pixen with the same square drawn on a 320x200 grid

How would we draw this in code? We can create a loop like such which plots the pixel pairs on the screen and loops through the 2d array:

int i;
int x = 0;
int y = 0;
for (i = 0; i < 4; i++)
		tgi_line((square[i][0]) + x, (square[i][1]) + y, (square[i][2]) + x, (square[i][3]) + y);

This will draw the square at (0, 0) which is the top left corner of the screen in this case.

I created draw functions for doing this:

/* Take the x and y coordinates where you to begin drawing your shapes on the screen, add those to the line coordinates of the shape.
 * Take in a scale factor, multiple the shape coordinates by it. For example, to upscale the square by 2 multiply by 2
 */
void draw_square(const unsigned int x, const unsigned int y, unsigned int scale) {
    unsigned int i;
    for (i = 0; i < 4; i++)
        tgi_line((square[i][0] * scale) + x, (square[i][1] * scale) + y, (square[i][2] * scale) + x, (square[i][3] * scale) + y);
}

Drawing more complex graphics

I decided to go ahead and draw a neocat using this method, first I draw the neocat in pixen. I included the file here.

Neocat drawn using pixen

Next I used my mouse cursor to hover over and record the coordinates, and created a 2d array in my C program with the coordinates:

const unsigned int neocat[51][4] = {
	// Outline
	{8, 5, 5, 8},
	{5, 9, 5, 24},
	{4, 25, 4, 37},
	{5, 38, 5, 40},
	{6, 41, 11, 46},
	{12, 47, 15, 47},
	{16, 48, 37, 48},
	{38, 47, 40, 47},
	{41, 46, 44, 43},
	{45, 42, 45, 41},
	{46, 40, 46, 24},
	{45, 23, 45, 17},
	{46, 16, 46, 7},
	{45, 6, 44, 6},
	{43, 5, 40, 5},
	{40, 5, 33, 9},
	{33, 9, 18, 9},
	{18, 9, 15, 7},
	{15, 7, 13, 7},
	{13, 7, 10, 5},
	{10, 5, 8, 5},
	// Eyes
	{13, 20, 10, 22},
	{10, 22, 10, 26},
	{10, 26, 12, 28},
	{10, 5, 8, 5},
	{12, 28, 16, 28},
	{16, 28, 18, 26},
	{18, 26, 18, 22},
	{18, 22, 16, 20},
	{16, 20, 13, 20},
	
	{28, 20, 26, 22},
	{26, 22, 26, 23},
	{26, 23, 25, 24},
	{25, 24, 25, 25},
	{26, 25, 26, 27},
	{27, 28, 32, 28},
	{32, 28, 34, 26},
	{34, 26, 34, 22},
	{34, 22, 31, 20},
	{31, 20, 28, 20},

	// Whiskers
	{8, 31, 9, 31},
	{10, 32, 12, 32},
	{12, 35, 8, 37},
	
	{40, 30, 35, 33},
	{35, 35, 40, 37},
	
	// Mouth
	{15, 32, 19, 36},
	{19, 36, 21, 32},
	{22, 32, 25, 36},
	{25, 36, 30, 32}
};

Finally I wrote a function for drawing it:

void draw_neocat(const unsigned int x, const unsigned int y, unsigned int scale) {
	unsigned int i;
	for (i = 0; i < 51; i++)
		tgi_line((neocat[i][0] * scale) + x, (neocat[i][1] * scale) + y, (neocat[i][2] * scale) + x, (neocat[i][3] * scale) + y);
}

Neocat

Issues

I've noticed that the more complex the shape is, the more error prone it is to upscale. For instance, you can see with the neocat that when upscaling it, not all the lines appear connected anymore.

Neocat with scaling

Additionally, the more complex a shape is the harder it is to draw. This becomes apparent if you want to do any sort of animation, where it has to redraw graphics again and again.

Animation and Interaction

You can move the graphics by adding and subtracting the offset you want to move from the base coordinates.

An example of this is moving a square around using the wasd keys:

while (running) {
	input = cgetc();
	switch (input) {
		// WASD move the square around
		case 'w': {
			tgi_clear();
			y -= 10;
			draw_square(x, y, scale_factor);
			break;
		}
		case 'a': {
			x -= 10;
			tgi_clear();
			draw_square(x, y, scale_factor);
			break;
		}
		case 'd': {
			x += 10;
			tgi_clear();
			draw_square(x, y, scale_factor);
			break;
		}
		case 's': {
			y += 10;
			tgi_clear();
			draw_square(x, y, scale_factor);
			break;
		}
	}
}

This is an example of an animation of a square moving around on the screen:

while (playing) {
    for (x = 0, y = 0; x < 320 && y < 200; x++, y++) {
        draw_square(x, y, 2);
        tgi_clear();
        if (kbhit()) {
            goto clr;
        }
    }
    for (y = 200; y > 0; y--) {
        draw_square(x, y, 2);
        tgi_clear();
        if (kbhit()) {
            goto clr;
        }
    }
    for (x = 200, y = 0; x > 0 && y < 200; x--, y++) {
        draw_square(x, y, 2);
        tgi_clear();
        if (kbhit()) {
            goto clr;
        }
    }
    for (x = 0, y = 200; x < 320 && y > 0; x++, y--) {
        draw_square(x, y, 2);
        tgi_clear();
        if (kbhit()) {
            goto clr;
        }
    }
    for (x = 320; x > 0; x--) {
        draw_square(x, y, 2);
        tgi_clear();
        if (kbhit()) {
            goto clr;
        }
    }
}

More complex animations are harder on the CPU to draw, so they end up being much worse performing.

Here's an example of a neocat animation:

while (playing) {
	for (x = 0, y = 0; x < 320 && y < 200; x++, y++) {
		draw_neocat(x, y, scale_factor);
		tgi_clear();
		if (kbhit()) {
			goto clr;
		}
	}
	for (y = 200; y > 0; y--) {
		draw_neocat(x, y, scale_factor);
		tgi_clear();
		if (kbhit()) {
			goto clr;
		}
	}
	for (x = 320, y = 0; x > 0 && y < 200; x--, y++) {
		draw_neocat(x, y, scale_factor);
		tgi_clear();
		if (kbhit()) {
			goto clr;
		}
	}
	for (x = 0, y = 200; x < 320 && y > 0; x++, y--) {
		draw_neocat(x, y, scale_factor);
		tgi_clear();
		if (kbhit()) {
			goto clr;
		}
	}
	for (x = 320; x > 0; x--) {
		draw_neocat(x, y, scale_factor);
		tgi_clear();
		if (kbhit()) {
			goto clr;
		}
	}
}

Drawing using built in shape drawing functions

Up to this point I've been using TGI's line drawing functions to draw shapes and graphics, however TGI has built in shape drawing functions. This includes functions for drawing shapes like rectangles, elipses, and circles.

These functions are:

tgi_arc(int x, int y, unsigned char rx, unsigned char ry, unsigned sa, unsigned ea); // Draws an elliptical arc, the center is at the x and y coordinates, and the rx and ry coordinates are the radii, the sa and ea are start and end angles
tgi_bar(int x1, int y1, int x2, int y2); // Draws a bar (rectangle) at these set coordinate pairs
tgi_circle(int x, int y, unsigned char radius); // Draws a circle with the center at the x and y position of a given radius
tgi_ellipse(int x, int y, unsigned char rx, unsigned char ry); // Draws an ellipse, the center is at the x and y coordinates, and the rx and ry coordinates are the radii
tgi_pieslice(int x, int y, unsigned char rx, unsigned char ry, unsigned sa, unsigned ea); // Draws a pie slice, the center is at the x and y coordinates,the rx and ry coordinates are the radii, and the sa an ea are the start and end angles

The bar drawing function is the only one which fills in the shape, the others draw an outline but don't fill it in. Here's some screenshots below of each function in use.

Arc:

tgi_arc(160, 100, 80, 40, 0, 180);

Arc

Bar:

tgi_bar(50, 50, 250, 150);

Bar

Circle:

tgi_circle(160, 100, 75);

Circle

Ellipse:

tgi_ellipse(160, 100, 80, 40);

Ellipse

Pieslice:

tgi_pieslice(160, 100, 80, 80, 0, 240);

Pieslice

Conclusion

In conclusion, the TGI library makes graphics programming for 6502 based 8-bit computers very doable, but it has performance issues from drawing and redrawing graphics on the screen. However, if you're looking to get started I think it's a good option and I learned a lot from it.

Original post

Fediverse post