NESROGUE, part 3

It would seem that writing a program for the Nintendo is not like writing a program for a “normal” computer: It’s not enough to target the core system, you also have to consider the layout of the cartridge you are storing the program on. There are over 200 “mappers” for the Nintendo that define different memory maps created over the course of a decade as developers pushed various limits of what the hardware could do. Some have more tile memory, others can save data to the cartridge. No one cartridge mapper can do everything. I have not yet decided if this is a breaking point for me or not, but for now I would love to see my initial level generator go live on the emulator.

The initial version of my level generator is based around 64×60 tile levels, using all 4 nametables. The initial version sets the background attribute colors to an XOR-pattern, and places 5 rooms randomly around the level connected with passages. I had to do a bit of math to get the generator to merge the 4 nametables into a single map, then split it back out again for the 4 binary files. You’ll see some weird math in a couple of places, like where I’m getting the tableIndex in SetTile. Ultimately all of this is going to be implemented in 6502 assembly, so all base-10 math is going to need to be evicted, and anything that can be rendered down to simple logic operators should be.

If you run the LINQPad script that I’m about to share with you, it will generate a simple level (in 4 files) that you could scroll around if only you had a NES-compatible program to load it into. I have the start of an NES ROM that is suitable for this, but it’s not quite debugged yet.

The next edition of the level generator will probably line up the rooms with the attribute tiles, so that I can more easily assign a palette to each room.

const string PATH = @"C:\Users\Trey.Tomes\projects\personal\nes\";
const int NAMETABLE_SIZE = 1024;
const int NUM_TABLES = 4;
const int NT_ROWS = 30;
const int NT_COLUMNS = 32;
const int TILE_START = 0;
const int ATTR_START = NT_ROWS * NT_COLUMNS;
const int TOT_ROWS = NT_ROWS * 2;
const int TOT_COLUMNS = NT_COLUMNS * 2;
const int COLORS_PER_PALETTE = 4;

static Random rnd = new Random();

struct Room {
	public int x;
	public int y;
	public int w;
	public int h;
}

void Main() {
	const string FILENAME_OUTPUT = "sample";
	
	byte[][] data = CreateNametableSet();
	
	for (var y = 0; y < 60; y++) {
		for (var x = 0; x < 64; x++) {
			SetTile(data, x, y, (byte)'#');
		}
	}
	
	// Set the attribute table to an XOR-pattern.
	for (var y = 0; y < 30; y++) {
		for (var x = 0; x < 32; x++) {
			SetAttribute(data, x, y, (x ^ y) % COLORS_PER_PALETTE);
		}
	}
	
	var maxRooms = 5;
	var roomNumber = 0;
	var room = CreateRoom(data);
	while (roomNumber < maxRooms) {
		var lastRoom = room;
		room = CreateRoom(data);
		CarveTunnel(data, lastRoom, room);
		roomNumber++;
	}
	
	File.WriteAllBytes(Path.Combine(PATH, FILENAME_OUTPUT + $"{0}.nam"), data[0]);
	File.WriteAllBytes(Path.Combine(PATH, FILENAME_OUTPUT + $"{1}.nam"), data[1]);
	File.WriteAllBytes(Path.Combine(PATH, FILENAME_OUTPUT + $"{2}.nam"), data[2]);
	File.WriteAllBytes(Path.Combine(PATH, FILENAME_OUTPUT + $"{3}.nam"), data[3]);
}

void CarveTunnel(byte[][] data, Room room1, Room room2) {
	var x = room1.x + (room1.w >> 1);
	var y = room1.y + (room1.h >> 1);
	var x2 = room2.x + (room2.w >> 1);
	var y2 = room2.y + (room2.h >> 1);
	
	var offset = (x < x2) ? 1 : -1;
	while (x != x2) {
		SetTile(data, x, y, (byte)' ');
		x += offset;
	}
	offset = (y < y2) ? 1 : -1;
	while (y != y2) {
		SetTile(data, x, y, (byte)' ');
		y += offset;
	}
}

Room CreateRoom(byte[][] data) {
	const int ROOM_MIN_WIDTH = 5;
	const int ROOM_MAX_WIDTH = 8;
	const int ROOM_MIN_HEIGHT = 5;
	const int ROOM_MAX_HEIGHT = 8;
	
	var width = rnd.Next(ROOM_MIN_WIDTH, ROOM_MAX_WIDTH + 1);
	var height = rnd.Next(ROOM_MIN_HEIGHT, ROOM_MAX_HEIGHT + 1);
	
	var left = rnd.Next(1, TOT_COLUMNS - width - 1);
	var right = left + width - 1;
	var top = rnd.Next(1, TOT_ROWS - height - 1);
	var bottom = top + height - 1;
	
	for (var x = left; x <= right; x++) {
		for (var y = top; y <= bottom; y++) {
			SetTile(data, x, y, (byte)' ');
		}
	}
	
	return new Room() { x = left, y = top, w = width, h = height };
}

/**
 * 4 nametables for the 4 screen quadrants.
 */
byte[][] CreateNametableSet() {
	return new byte[NUM_TABLES][] {
		new byte[NAMETABLE_SIZE],
		new byte[NAMETABLE_SIZE],
		new byte[NAMETABLE_SIZE],
		new byte[NAMETABLE_SIZE]
	};
}

void SetTile(byte[][] data, int x, int y, byte tileNumber) {
	var tableX = x >> 5; // x / 32
	var tableY = y / 30; // How to shift right to divide by 30?
	var tableIndex = ((tableY & 1) << 1) | (tableX & 1); // convert to range [0, 3]
	var tbl = data[tableIndex];
	x = x % 32;
	y = y % 30;
	
	var offset = y;
	offset = offset << 5;
	offset += x;
	offset += TILE_START;
	tbl[offset] = (byte)tileNumber;
}

/**
 * Set the background palette number for a 4x4 tile block.
 *
 * blockX: tile x / 2, [0, 15]
 * blockY: tile y / 2, [0, 14]
 * attr: background palette number, [0, 3]
 */
void SetAttribute(byte[][] data, int blockX, int blockY, int paletteNumber) {
	var tableX = blockX >> 4; // blockX / 16;
	var tableY = blockY / 15; // How to divide by 15?
	var tableIndex = ((tableY & 1) << 1) | (tableX & 1); // convert to range [0, 3]
	var tbl = data[tableIndex];
	blockX = blockX % 16;
	blockY = blockY % 15;

	if ((blockX < 0) || (blockX > 15)) {
		throw new ArgumentNullException("Must be [0, 15].", nameof(blockX));
	} else if ((blockY < 0) || (blockY > 14)) {
		throw new ArgumentNullException("Must be [0, 14].", nameof(blockY));
	} else if ((paletteNumber < 0) || (paletteNumber > 3)) {
		throw new ArgumentNullException("Must be [0, 3].", nameof(paletteNumber));
	}

	var bitOffset = (((blockY & 1) << 1) | (blockX & 1)) << 1;
	var byteOffset = ATTR_START + (int)((blockY >> 1) << 3) + (blockX >> 1);
	var oldAttr = tbl[byteOffset];
	var attr = (oldAttr & ~(0b11 << bitOffset)) + (paletteNumber << bitOffset);
	tbl[byteOffset] = (byte)(attr);
}

It’ll probably be a couple of weeks before I come back to this. I recently had a dream where I was playing a game on the Color Computer 3 that hasn’t been created yet, so I’m going to spend some time seeing if I can build it.

NESROGUE, part 2

My adventures in creating an NES roguelike continue. The first problem I’m out to solve is the tileset. I want to use the Extended ASCII character set for my game tiles. That character set has always brought back fond memories of the weeks of life I lost to playing ZZT in the ’90s.

The NES supports 2 banks of 256 tiles, with each tile being 8×8 pixels. It only supports 64 sprites at a single time (each sprite being represented by a single tile). The mario sprite from the original Super Mario is actually 4 8×8 sprites. Having larger sprites means you can’t have as many on the screen at one time. My characters will be 1 8×8 sprite each. Each tile can have 4 colors (a single palette set), with the first color in each palette being transparent.

Given all that, what makes the most sense to me is mirroring the ASCII set over both tile banks. Bank 0 will be used for sprites, with the background color being transparent. Bank 1 will be for background tiles, which will make up the structure of the level. The bank 1 tiles will have an opaque background that can be swapped out by pointing to a different palette.

Getting the character set stuffed into the CHR format required for the NES was a bit of a trick. I am currently using the YY-CHR program to view the CHR data. The biggest problem I had to solve is described on this page. Each tile is represented by 16 bytes, with each pixel split over 2 bit planes. The first 8 bytes are plane 0, second 8 bytes are plane 1. Took me a couple of false starts to figure out a straightforward way of transferring pixel values from a PNG file into a CHR file. I did this all in a short C# script running in LINQPad, like so:

/*
 * Convert the OEM437 Extended ASCII chart into a NES-compatible CHR file!
 *
 * References:
 * - http://wiki.nesdev.com/w/index.php/PPU_pattern_tables
 *
 */

const int NUM_TABLES = 2;
const int TILES_PER_TABLE = 256;
const int TILE_WIDTH = 8;
const int TILE_HEIGHT = 8;
const int BITS_PER_PIXEL = 2;
const int BITS_PER_BYTE = 8;
const int PIXELS_PER_BYTE = BITS_PER_BYTE / BITS_PER_PIXEL;

const string PATH = @"C:\Users\Trey.Tomes\projects\personal\nes\";
const string FILENAME_ASCII = "OEM437.png"; // must be a 128x128 image
const string FILENAME_OUTPUT = "ascii.chr";

var OEM437 = new Bitmap(Path.Combine(PATH, FILENAME_ASCII));

// Not efficient, but easiest to setup both bit planes in advance.
var bitPlane0 = new List<int>();
var bitPlane1 = new List<int>();

// I'm mirroring the Extended ASCII chart onto both tables, but with different background colors.

// Table 0 will have a transparent background.
for (var tile = 0; tile < TILES_PER_TABLE; tile++) {
	var row = ((int)(tile / 16)) * TILE_HEIGHT;
	var column = (tile % 16) * TILE_WIDTH;
	for (var y = 0; y < TILE_HEIGHT; y++) {
		for (var x = 0; x < TILE_WIDTH; x++) {
			int colorIndex = 0;
			if (OEM437.GetPixel(column + x, row + y).A != 0) {
				colorIndex = 3;
			}
			bitPlane0.Add(colorIndex & 1); // low bit in plane 0
			bitPlane1.Add((colorIndex >> 1) & 1); // high bit in plane 1
		}
	}
}

// Table 1 will have an opaque background.
for (var tile = 0; tile < TILES_PER_TABLE; tile++) {
	var row = ((int)(tile / 16)) * TILE_HEIGHT;
	var column = (tile % 16) * TILE_WIDTH;
	for (var y = 0; y < TILE_HEIGHT; y++) {
		for (var x = 0; x < TILE_WIDTH; x++) {
			int colorIndex = 0;
			if (OEM437.GetPixel(column + x, row + y).A != 0) {
				colorIndex = 3;
			} else {
				colorIndex = 1;
			}
			bitPlane0.Add(colorIndex & 1); // low bit in plane 0
			bitPlane1.Add((colorIndex >> 1) & 1); // high bit in plane 1
		}
	}
}

// Compress the bits into bytes.
var bytes = new byte[8192];
var index = 0;
var n0 = 0;
var n1 = 0;
while (index < 8192) {
	byte nextByte = 0;
	
	// First byte of the tile is from plane 0.
	for (var y = 0; y < 8; y++) {
		nextByte = 0;
		for (var x = 0; x < 8; x++, n0++) {
			nextByte = (byte)(nextByte << 1);
			nextByte = (byte)(nextByte | bitPlane0[n0]);
		}
		bytes[index++] = nextByte;
	}
	
	// Second byte of the tile is from plane 1.
	for (var y = 0; y < 8; y++) {
		nextByte = 0;
		for (var x = 0; x < 8; x++, n1++) {
			nextByte = (byte)(nextByte << 1);
			nextByte = (byte)(nextByte | bitPlane1[n1]);
		}
		bytes[index++] = nextByte;
	}
}

var numExpectedBytes = NUM_TABLES * TILES_PER_TABLE * TILE_HEIGHT * TILE_WIDTH / PIXELS_PER_BYTE;
if (bytes.Length != numExpectedBytes) {
	throw new Exception($"The byte array isn't long enough.  Expected {numExpectedBytes}, found {bytes.Length}.");
}

File.WriteAllBytes(Path.Combine(PATH, FILENAME_OUTPUT), bytes.ToArray());

This produces an ascii.chr file that can be easily included into a NESASM-formatted assembly file.

Next up: How am I going to build a game level with this thing?

NESROGUE, part 1

I’ve decided to write a Nintendo game. An 8-bit Nintendo game, targeting the original NES console, written in 6502 assembly language. The question you might be asking yourself now is: “Why would he do this to himself??” So glad you asked!

The 6502 processor was first introduced in 1975, and is inconceivable still in production to this day! Some form of this processor powered most of the computers and game consoles of the late ’70s and ’80s, and is still used by hobbyists all over the world! As I’ve been learning about this processor, I’ve been getting the itch to acquire one of these retro computers to do some development work of my own. They’re hard to find. It occurred to me just this week that I already own one. The original Nintendo, which my parents bought somewhere around 1990, runs on a Ricoh 2A03. A 2A03 is basically a 6502 minus decimal mode + a bunch of sound generation hardware. The only real difference between the Nintendo and any other ’80s computer is the lack of a keyboard and some type of storage device, both of which I could probably cobble together given enough time. Why not use the hardware I already own, rather than pining after what I don’t have?

A roguelike is a game genre based on the game Rogue written in 1980. I love playing roguelikes, though I don’t think I’ve ever beaten one (if they even can be beaten…). Writing my own version of Rogue targeting the original Nintendo is almost as retro as it gets. It’ll include several interesting challenges, like figuring out random-number generation in assembly language. And since I’m targeting a really popular console, I’ll have plenty of people to share the final results with!

You can follow my development process on this blog, and at this git repository.

HOWTO: Windows Terminal for XAMPP

I have found myself using XAMPP a lot lately. The Shell button in the XAMPP Control Panel is useful for running php and whatnot, but don’t like using the old cmd.exe. All of the cool kids are using Windows Terminal these days. These are the 2 steps I had to execute to make it easy to start the XAMPP Shell from Windows Terminal.

1. Create a batch file.

When you press the Shell button in the XAMPP Control Panel, c:\xampp\xampp_shell.bat will be executed. You need to clone this file, then gut it so that it only creates the environment variables you need. Something like this:

SET "MIBDIRS=%~dp0php\extras\mibs"
SET "MIBDIRS=%MIBDIRS:=/%"
SET "MYSQL_HOME=%~dp0mysql\bin"
SET "OPENSSL_CONF=%~dp0apache\conf\openssl.cnf"
SET "OPENSSL_CONF=%OPENSSL_CONF:=/%"
SET "PHP_PEAR_SYSCONF_DIR=%~dp0php"
SET "PHP_PEAR_BIN_DIR=%~dp0php"
SET "PHP_PEAR_TEST_DIR=%~dp0php\tests"
SET "PHP_PEAR_WWW_DIR=%~dp0php\www"
SET "PHP_PEAR_CFG_DIR=%~dp0php\cfg"
SET "PHP_PEAR_DATA_DIR=%~dp0php\data"
SET "PHP_PEAR_DOC_DIR=%~dp0php\docs"
SET "PHP_PEAR_PHP_BIN=%~dp0php\php.exe"
SET "PHP_PEAR_INSTALL_DIR=%~dp0php\pear"
SET "PHPRC=%~dp0php"
SET "TMP=%~dp0tmp"
SET "PERL5LIB="
SET "Path=;%~dp0;%~dp0php;%~dp0perl\site\bin;%~dp0perl\bin;%~dp0apache\bin;%~dp0mysql\bin;%~dp0FileZillaFTP;%~dp0MercuryMail;%~dp0sendmail;%~dp0webalizer;%~dp0tomcat\bin;%Path%"

Save all of that to c:\xampp\setenv.bat. Now open Windows Terminal. Click on the button next to the “+” that you use to open a new tab (looks like a “V”). Click on “Settings”. It should open up a JSON file in a text editor. If you scroll down a bit you’ll see a list of profiles. Add this block to the profiles list:

{
"guid": "{b453ae62-4e3d-5e58-b989-0a998ec441b9}",
"hidden": false,
"name": "XAMPP",
"commandline": "cmd.exe /K c:\\xampp\\setenv.bat",
"icon": "c:/xampp/install/xampp.ico",
"startingDirectory": "c:/xampp",
"experimental.retroTerminalEffect": true,
"foreground": "#00FF00",
"cursorShape": "vintage",
"cursorColor": "#00FF00"
//"useAcrylic": true,
//"acrylicOpacity": 0.7,
},

I created the “guid” value by copying a value from another profile and changing one character. It doesn’t matter what you put here as long as it’s unique. Once you save this the XAMPP option will be available from the new tab menu in Windows Terminal.

If you remove the comment markers from the “useAcrylic” and “acrylicOpacity” settings then the terminal will be slightly transparent. It’s a neat effect, but I couldn’t decide if I liked it or not. The retro terminal effect is pretty cool though.

The Ray Tracer Challenge, pt. 1

I received this book from my parents for Christmas, and am super-excited to start playing with it! The topic of ray tracing combines math and computer programming in one of my favorite ways. This book claims to tackle it’s subject using test-driven development, which from my experience is incredibly difficult to do in any project that involves generating images, so I’m eager to learn what he has to teach.

The book is designed for it’s reader to translate pseudo-code and unit tests into any language and framework of choice.  I prefer .NET, and will probably pick on C# / .NET Core as that’s the direction the Microsoft world is headed.  The unit tests are described using Cucumber syntax.  While I have tended to prefer xUnit for unit testing, I will probably take this opportunity to learn the SpecFlow library; the .NET implementation of Cucumber.

You are welcome to follow my Github repository if you are curious to see how this develops over time.

Converting palettes between RGB and Composite

As a reminder, here are the 2 palette’s we have to work with on a Color Computer 3:

Composite
RGB

Fun fact: The first 8 colors of the Color Computer 3 RGB palette match the first 8 colors of the CGA 16-color text-mode palette!

For the game I’m putting together, I’m wanting the user to be able to choose their monitor type, then construct the palette to look the same on either monitor. As it turns out, the conversion table has already been assembled. From the book Coco III Secrets Revealed, page 6:

RGBCMPCOLORRGBCMPCOLOR
0000BLACK3223MEDIUM RED
0112DARK BLUE3308MEDIUM RED / MAGENTA
0202DARK GREEN3421YELLOW / ORANGE
0314DARK CYAN3506LIGHT RED
0407DARK RED3639BRIGHT RED
0509DARK MAGENTA3724LIGHT RED / MAGENTA
0605BROWN3838ORANGE
0716DARK GREY3954PALE RED / MAGENTA
0828MEDIUM BLUE4025MEDIUM BLUE / MAGENTA
0944BRIGHT BLUE4142BLUE / PURPLE
1013LIGHT BLUE / CYAN4226LIGHT MAGENTA
1129LIGHT BLUE4358PURPLE
1211INDIGO4424LIGHT PURPLE
1327MED BLUE / PURPLE4541BRIGHT MAGENTA
1410MEDIUM SKY BLUE4640PALE BLUE / MAGENTA
1543MEDIUM PEACOCK4756PALE PURPLE
1634MEDIUM GREEN4820MEDIUM YELLOW
1717MEDIUM GREEN / CYAN4904LIGHT YELLOW
1818BRIGHT GREEN5035LIGHT YELLOW / GREEN
1933BRIGHT GREEN / CYAN5151PALE YELLOW / GREEN
2003MEDIUM YELLOW / GREEN5237LIGHT YELLOW / ORANGE
2101LIGHT GREEN / CYAN5353MEDIUM YELLOW
2219BRIGHT YELLOW / GREEN5436BRIGHT YELLOW
2350LIGHT GREEN5552PALE YELLOW
2430MEDIUM CYAN5632LIGHT GREY
2545PEACOCK5759PALE BLUE
2631LIGHT GREEN / CYAN5849PALE CYAN
2746BRIGHT CYAN5962PALE BLUE / CYAN
2815LIGHT PEACOCK6055PALE RED
2960PALE PEACOCK6157PALE MAGENTA
3047PALE GREEN / CYAN6263VERY PALE YELLOW
3161LIGHT CYAN6348WHITE

This mapping will not generate precisely the same colors between RGB and composite, but it may be considered “close enough”.

I pulled the following BASIC program from the same article where I found the color table. There are 3 parameters: RG, PL, and CL. Set RG=0 for Composite monitors or RG=1 for RGB monitors, then set PL to the palette index and CL to the RGB color value. As written, it will set the background to a medium green color regardless of your monitor type.

0 ' SET PALETTE USING SAME COLORS ON RGB OR COMPOSITE.
10 WIDTH 40
20 ' SET TO RG=0 IF COMPOSITE MONITOR USED.
30 RG=1
40 ' PALETTE TO CHANGE (PL) RGB VALUE (CL).
50 PL=0
60 CL=16
70 DIM CP(63) ' SETUP ARRAY FOR COMPOSITE VALUES.
80 FOR X=0 TO 63:READ CP(X):NEXT X ' FILL ARRAY.
90 GOSUB 900 ' CALL CONVERSION ROUTINE.
100 END
900 IF RG=1 THEN PALETTE PL,CL:RETURN
910 PALETTE PL,CP(CL) ' USE ARRAY VALUE IF COMPOSITE.
920 RETURN
1000 DATA 0,12,2,14,7,9,5,16,28,44,13,29,11,27,10,43
1010 DATA 34,17,18,33,3,1,19,50,30,45,31,46,15,60,47,61
1020 DATA 23,8,21,6,39,24,38,54,25,42,26,58,24,41,40,56
1030 DATA 20,4,35,51,37,53,36,52,32,59,49,62,55,57,63,48

EXEC from VS.Code to MESS.

I finally got MESS working as a Color Computer 3 emulator, but in doing so I lost the ability to quickload .BIN files that I had in Vcc. The way around this is through the use of 2 command-line parameters provided by MESS: -debug and -debugscript. -debug will cause the debugger to start as soon as the machine starts, and -debugscript will provide what amounts to a batch file of debugger commands that will be immediately executed.

Loading a machine-code program from the debugger requires a set of 4 commands:

LOAD ADVGAME1.ROM,4000
GT 500
PC=4000
GO

First I have to load my game, ADVGAME.ROM, into address 4000 (the same as the org $4000 in the assembly source code). There is an additional trick here, as .BIN files cannot be loaded through this method. When you assemble the program with lwasm, add the -r parameter to create a RAW file. GT tells the debugger to wait 500 milliseconds and then pause, giving the emulator a chance to finish loading. Then we move the program counter to the start of our program in memory, and use GO to resume execution. All of this can be loaded into a text file and given to MESS on the command-line.

I’m wanting to further parameterize this though, so I don’t have to manually create a new debug script every time I create an assembly file. That led me to the creation of exec.bat:

@echo off

REM %~nf1 : The fully-qualified path.
REM %~n1 : The filename without the path or extension.
REM %~dp0 : The full path without the filename.

set lwasm=..\..\bin\lwasm.exe
set messuipath=..\..\messui
set messuiexe=messui64.exe
set debugpath=%~dp1%~n1.debug

echo Assembling %~nf1
%lwasm% -r --list=%~n1.lst %~nf1 -o%~n1.rom"

echo Building debug script.

del %~n1.debug
echo LOAD %~dp1%~n1.rom,4000 > %~n1.debug
echo GT 500 >> %~n1.debug
echo PC=4000 >> %~n1.debug
REM echo GO >> %~n1.debug

echo Running ROM %~n1.rom
cd %messuipath%
%messuiexe% coco3 -skip_gameinfo -window -ui_active -debug -debugscript %debugpath%
cd  %~dp0

echo Done!

You will need to change lwasm, messuipath, and messuiexe to point to your own set of tools, but running this at the command line with exec advgame1.asm allows me to assembly and run in a single step!

Since I use Visual Studio Code as my editor, I need an additional file to automatically run this final batch file. This is what I added to .vscode\tasks.json in my project folder:

{
	"version": "2.0.0",
	"tasks": [
		{
			"label": "EXEC 6809 ASM",
			"type": "process",
			"windows": {
				"command": "../../bin/exec.bat",
				"args": ["${fileBasename}"],
			},
			"group": {
				"kind": "build",
				"isDefault": true
			},
			"problemMatcher": []
		}
	]
}

Since I’m using batch files, this task will only work in a Windows OS. I may create a bash script to go along with this next time I’m on a Linux machine.

Once all of this is setup you can use Ctrl+P task exec to assemble and run any assembly language file you have open in the editor.

That should do for now.

Bringing DawnBringer to the Color Computer, part 2.

Continued from part 1.

About MAME.

Following some advice I received on the Facebook group, I spent some time learning to use MAME and investigating the difference between the composite and RGB palettes. I found this video really helpful for getting starting with MAME; the setup truly is not intuitive.

In summary, this is what I learned about using MAME:

  • Use mame.exe -cc from the command-line to generate the mame.ini file that you will need to use to configure the display.
  • In mame.ini, set window=1 and maximize=0.
  • Inside of MAME, find each of the Color Computer ROMs that you downloaded and dumped into the ROM directory and click the star to favorite them, then open the Machine Configuration options for each machine and turn mouse emulation off. This should keep the program from capturing the mouse.
  • Start a machine, then use the Scroll Lock key to turn on Partial Emulation. Once this is enabled you can use the Tab key to open a system menu.
  • In Input (General), change Pause key configuration to the Pause key.
  • Create a batch file to launch your Color Computer 3 ROM (if you are not using a UI):
@echo off
pushd "%~dp0"
cd mame
mame.exe coco3 -ext multi -skip_gameinfo -window -ui_active -flop1 ..\src\trey.dsk
popd
  • Don’t use MAME. Use MESS UI. It’s tons easier to work with.

Using MESS UI, you can paste BASIC programs directly into the emulator, which is a lot easier than my pre-existing method of creating the .BAS in Visual Studio Code, then using file2dsk to put it on a disk image, then mounting the disk and loading the file. You will want to increase the emulation speed when you do this though. The pasting doesn’t always seem to capture every keystroke. Not really sure why yet, but you’ll want to check your work to make sure all the lines got pasted in properly.

RGB/Composite

The main problem I am needing to solve here is that most people these days use RGB, not composite. Makes sense to me; composite connections are getting harder to find these days. Here is the palette I generated yesterday using composite emulation:

…and here is that same palette using RGB emulation!

My beautiful palette is ruined!

It looks nothing like the composite color selection. I went back to the drawing board and selected a new set of palette colors to use in RGB mode. It doesn’t look exactly the same as the composite palette, but hopefully it’s close enough to keep the game I’m writing from looking like it was attacked by an angry unicorn:

I have added a little menu to select which palette you want to use. I am going to have to add a monitor type selection to the next edition of ADVGAME1.BIN as well.

1 ON BRK GOTO 100
5 ' CMP PALETTE
10 DATA 0,9,13,16,5,3,23,32,44,21,29,19,54,45,36,63
15 ' RGB PALETTE
20 DATA 0,4,14,7,34,2,39,6,29,53,56,20,60,24,62,63
25 DIM CP(16)
30 DIM RP(16)
35 FOR N=0 TO 15
40 READ CP(N)
45 NEXT
50 FOR N=0 TO 15
55 READ RP(N)
60 NEXT
100 HSCREEN 2
105 PALETTE 0,0
110 PALETTE 1,63
115 HCOLOR 1
120 HPRINT (4,4),"CHOOSE YOUR MONITOR TYPE:"
130 HPRINT (5,6),"1. CMP"
131 HPRINT (5,7),"2. RGB"
132 HPRINT (5,8),"3. EXIT"
150 A$=INKEY$:IF A$="3" THEN GOTO 500 ELSE IF A$<>"1" AND A$<>"2" GOTO 150
155 S=ASC(A$)-ASC("0")
200 HSCREEN 2
210 L=24
220 N=INT(L/8)
230 FOR Y=0 TO 3
240 FOR X=0 TO 3
250 C=Y*4+X
260 IF S=1 THEN PALETTE C,CP(C) ELSE PALETTE C,RP(C)
270 HCOLOR C
280 HLINE (8+X*L,8+Y*L)-(8+(X+1)*L,8+(Y+1)*L),PSET,BF
290 IF C>13 THEN PC=0 ELSE PC=15
300 HCOLOR PC
310 IF S=1 THEN HPRINT (X*N,(Y+1)*N),CP(C) ELSE HPRINT (X*N,(Y+1)*N),RP(C)
320 HPRINT (X*N,(Y+0.5)*N),C
330 NEXT
340 NEXT
350 HCOLOR 15
360 HPRINT (1,5*N),"BEHOLD THE DAWNBRINGER-ISH PALETTE!"
370 IF S=1 THEN HPRINT (1,6*N),"USING THE CMP PALETTE." ELSE HPRINT (1,6*N),"USING THE RGB PALETTE."
380 HPRINT (1,7*N),"PRESS <BREAK> TO RETURN TO THE MENU."
400 GOTO 400
500 HSCREEN 0
510 WIDTH 80
520 PALETTE 0,0
530 PALETTE 8,63

Bringing DawnBringer to the Color Computer.

I’m on a quest to implement a rogue-like game on the Color Computer 3 (in the Vcc emulator) using 6809 assembly. The game will be rendered on the 80-column text screen, which gives us 8 background colors and 8 foreground colors. I can choose from a palette of 64 colors made available on this machine, but which colors should I pick?

I have found the DawnBringer 16-color palette to be fairly useful for pixel-art games on sites like OpenGameArt.org, so I’m going to see if I can use that here. As a reminder, this is the complete palette as defined by the author.

And here is the complete palette available to the Color Computer 3:

I overlaid these images on top of each other in Paint.NET, and picked the best color matches through trial-and error. My final palette doesn’t look quite like the DawnBringer palette, but perhaps you will be able to see a family resemblance:

The top number is the index into the active 16-color palette; the bottom number is the index into the full 64-color palette. The BASIC code to generate this palette is as follows:

10 DATA 0,9,13,16,5,3,23,32,44,21,29,19,54,45,36,63
20 DIM A(16)
30 HSCREEN 2
40 L=16
50 N=INT(L/8)
60 FOR Y=0 TO 3
70 FOR X=0 TO 3
80 C=Y*4+X
90 READ A(C)
100 PALETTE C,A(C)
110 HCOLOR C
120 HLINE (8+X*L,8+Y*L)-(8+(X+1)*L,8+(Y+1)*L),PSET,BF
130 IF C>13 THEN PC=0 ELSE PC=15
140 HCOLOR PC
150 HPRINT (X*N,(Y+1)*N),A(C)
160 HPRINT (X*N,(Y+0.5)*N),C
170 NEXT
180 NEXT
190 HPRINT (1,5*N),"BEHOLD THE DAWNBRINGER-ISH PALETTE!"
200 GOTO 200

Word on the street is that, to properly implement this in assembly, the VSYNC and HSYNC signals must be respected. I’ll be writing more on this subject soon.

Gameboy Development, Part 2: Sound Channel 2

Continued from part 1.

A Gameboy has 4 sound channels:

  1. Tone & Sweep: Quadrangular wave patterns with sweep and envelope functions.
  2. Single Tone: Quadrangular wave patterns with envelope functions.
  3. Arbitrary 4-bit Wave
  4. Noise: White noise with an envelope function.

These are all controlled by toggling registers.  I found this video useful for getting started.  The video doesn’t provide a source code attachment, so you’ll need to copy the text as you see the presenter writing it.  I removed the UI-related code, so tune.c only has sound code (and a couple of printfs…). The video doesn’t explain what the registers mean though.

Setup

Before you can make any sound on a Gameboy, you have to turn the sound on. This is controlled by 3 registers:

  • NR50: Channel control / ON-OFF / Volume (R/W)
  • NR51: Selection of sound output terminal (R/W)
  • NR52: Sound on/off

When reading about sound registers, “terminal” refers to either the left or right speaker. If you were writing in assembly you would need to write to addresses $FF24-$FF26. Using the gbdk, you have access to variables that can read/write the registers for you. This function will initialize the system to allow sound to play:

void init_sfx_registers()
{
	// 1. Turn on the sound.
	NR52_REG = 0x80u;

	// 2. Mute all sound channels.
	NR51_REG = 0x00u;
	// High nibble is S02 (left), low nibble is S01 (right), 1 bit per sound channel.

	// 3. Turn the volume all the way up for both ears.
	NR50_REG = 0x77u;
	// High bit of each nibble will enable/disable SO2/SO1.
	// Lower 3 bits of each nibble set the volume for that output channel.
	// This value appears to mute both ears, but maxes out the volume.
	// The high bit of each nibble appears to do nothing in bgb.
}

When using an emulator, often you will find edge cases where bits of the hardware were not completely emulated. I tested several values in these registers to find out which bits actually did anything.

There are 3 functions that work together to allowing toggling individual channels on NR51. Working in C, you would expect to have access to a diverse set of data type (int, float, double, bool, etc.). Gbdk provides a thin layer of C code over 6502 assembly language; don’t count on anything aside from BYTE, UBYTE, WORD, and UWORD. Not hard to work around, you just have to keep this restriction in mind.

// Convert the channel number into the channel bit-value.
UBYTE get_channel_value(UBYTE channel)
{
	if (channel > 4)
	{
		channel = 4;
	}
	else if (channel < 1)
	{
		channel = 1;
	}

	// Convert the channel number to it's bit-value.
	channel = channel - 1; // 0=channel 1, 1=channel 2, 2=channel 3, 3=channel 4
	channel = 1 << channel; // 1, 2, 4, 8
	channel = (channel << 4) + channel; // 0x22 = channel 2 on both terminals, etc.
	return channel;
}

void deactivate_channel(UBYTE channel)
{
	channel = get_channel_value(channel);
	NR51_REG = NR51_REG - (NR51_REG & channel);
}

void activate_channel(UBYTE channel)
{
	channel = get_channel_value(channel);
	NR51_REG |= channel;
}

Once you have the N5# registers all set properly and channel 2 is active, you can start generating square waves. You can configure 3 components of the square wave generator: the length pattern, volume envelope and frequency.

Length Pattern

NR21 sets the sound length/wave pattern duty. The most significant 2 bits set the wave patter duty to one of 12.5%, 25%, 50%, or 75%, and the least significant 6 bits sound the sound length to 0-63. To calculate the actual sound length in seconds, given t1 as the 6 bit length value: length=(64-t1)*(1/256). This function will set the pattern and length:

/*
 * pattern = [0=12.5%, 1=25%, 2=50%, 3=75%]
 * length_ms = [0-252]ms
 */
void set_length_pattern(UBYTE pattern, UBYTE length_ms)
{
	length_ms = (length_ms / 4) & 63; // [0..252] --> [0..63]
	length_ms = 64 - length_ms;
	pattern = (pattern & 3) * 0x40;
	NR21_REG = length_ms + pattern;
}

Volume Envelope

This has probably been the most mysterious part of configuring channel 2. The envelope basically determines what “instrument” you are going to be playing. An envelope is composed of 4 sections:

  1. Attack
  2. Decay
  3. Sustain
  4. Release

The Gameboy has 8 volume envelopes, assigned on NR22. Bits 0-2 select the envelope sweep number (0 will mute the sound), bit 3 controls the direction (0=decrease, 1=increase), and bits 4-7 control the initial volume.

void set_volume_envelope(UBYTE initial_volume, UBYTE envelope_direction, UBYTE envelope_number)
{
	if (initial_volume > 0x0F)
	{
		initial_volume = 0x0F;
	}
	if (envelope_direction > 1)
	{
		envelope_direction = 1;
	}
	if (envelope_number > 7)
	{
		envelope_number = 7;
	}

	NR22_REG = envelope_number + (envelope_direction << 3) + (initial_volume << 4);
}

Frequency

The frequency is an 11-bit value in a world of 8-bit registers. The complete frequency consists of all 8 bits of NR23, plus the least significant 3 bits of NR24. Bit 6 of NR24 can be set for continual output, sort of like holding down the pedal on a piano while you play. Setting bit 7 to 1 will restart the sound. You will generally want to toggle this to 1 when you are using it.

void set_frequency(UBYTE restart_sound, UBYTE counter, UWORD frequency)
{
	NR23_REG = frequency & 0xFF;
	if (restart_sound != 0)
	{
		restart_sound = 0x80;
	}
	if (counter != 0)
	{
		counter = 0x40;
	}
	NR24_REG = restart_sound + counter + ((frequency >> 8) & 0x7);
}

Download the GB file and run it through any Gameboy emulator to see the final results. The file will play 6 different sound effects on channel 2, the final 2 of which are short musical tunes.

Downloads

References