Close

Some Byte-saving tricks

A project log for Dominant Amber - A 1KB Atari 2600 game

Identify the dominant color in random arrangements as quickly as possible

rick-skrbinaRick Skrbina 01/04/2017 at 03:290 Comments

The most enjoyable part of coding for the Atari 2600 is getting it to do what you wanted with the very limited resources available. Released in 1977, it is one of the earliest microprocessor based game consoles. The console features 128 bytes of RAM, 2x 8 bit I/O ports (usually used for input), and a very limited graphics chip that must be updated every scan line 'on the fly' as the screen is drawn in real time.

When trying to cram your program into a limited ROM, you will find that there are always bytes to be saved if you look hard enough. Here are some of the tricks I used to cram Dominant Amber into 1024 Bytes.

BIT opcode to skip an instruction

An old trick with the 6502 is that the BIT (absolute) instruction can be used to skip the next instruction, eliminating a branch. This is a savings of a byte each time it is used, since branch instructions take 2 bytes.

The BIT (absolute) instruction is 3 bytes. It's function is to load D7 and D6 of the byte at the address specified into two of the CPU's flags. It does not corrupt any registers other than the status register, so using it as a pseudo branch instruction has no ill effect. Here is an example in the code:

	lda ScreenSaverPosition
	cmp #65
	beq DontIncScreenSaverPosition
	inc ScreenSaverPosition
	.byte $2C                          ;opcode for BIT instruction
                                           ;used to skip dec instruction
                                           ;dec .. used as absolute address
                                           ;for BIT 
	
DontIncScreenSaverPosition
	
	dec ScreenSaverOffset


Branch instead of Jump

Another useful trick is to branch instead of jump when possible. Jump instructions take 3 bytes and branches are only 2. So when possible, you should always use a branch. The catch is that the 6502 does not have a 'branch always', and branches can only go to +/- 128 bytes from the instruction itself. When you have a situation where you don't need to jump far, and you know for sure what the status register will contain, use the appropriate branch.

Reuse your data tables when possible

An example of data reuse in Dominant Amber is the frequency/volume envelop data for the sounds.

'Correct' sound envelop table = 'Incorrect' sound frequency table

'Correct' sound frequency table = 'Incorrect sound envelop table

This was enough along with a different distortion setting on the TIA to make a positive and negative sounding effect using the same data. The savings here was 24 bytes.

It is also common to reuse graphics data that repeats. For example the 0, 8, 9, and 6 characters share byte patterns, so these characters share bytes with each other.

BRK instead of JSR

The BRK instruction pushes the processor status to the stack and effectively jumps to a subroutine pointed to by the break vector. The advantage of using BRK to call a subroutine is that it only costs 1 byte, as opposed to the 3 bytes that a standard JSR takes. These 2 byte savings add up if a subroutine is called many times.

Use illegal opcodes

The 6502 has 8 bit instructions, but not all possible 255 of them are defined and explained in the datasheet. Over the years, hackers have documented what the other instructions missing from the documentation do. Of course it is normally bad practice to use such an instruction, but for the Atari 2600, there are some useful ones that work on all the official hardware out there. For example, I use a couple different forms of LAX, or Load Accumulator and X to save some space when I need the same value in two registers. Normally this would require an LDA and TAX, or LDX and TXA, so just using LAX eliminates a byte for the transfer.

Add more than you want..

There was a couple places in the code that sometimes a value would be added, and some times subtracted from a variable. I found that I could avoid a branch instruction if I added the value I wanted + the value that would be subtracted in the other case, and ran both cases. For example:

RandomAdd7
	clc
	adc #14	 ;save branch instruction by +14 -7 for +7

RandomSub7
	sec
	sbc #7

Branching to RandomAdd7 will result in adding 7 to the accumulator when the block of code is executed (+14, -7). There was originally a branch instruction between the ADC and SEC in order to skip the subtraction (SBC).

Anyway, hope you enjoyed my write up of some of the tricks used in this project, and that I didn't ramble too much :)


Discussions