Let's Make a Game
For Aprilcot 2025, I continued my exploration of the graphics and sound
capabilities of the Apricot. At the same time, CanDev and the Internet
Archive were doing the DiscMaster
Jam, a game jam where you have to
use assets from the DiscMaster, a
search engine for assets found on old CD-ROMs. I thought it would be fun
to combine both things and make a game for the Apricot.
Aprilcot?
Aprilcot is a celebration of the ACT Apricot that happens in April. It
is... pretty much just me, as the Apricot is rare, and especially rare
in the US. But I spend the month exploring this old and fascinating
machine and posting about it on
my fediverse account.
The Apricot is not, uh, graphically gifted (we'll get to that in a
moment), so I needed a type of game that played to its strengths.
Something with a lot of static art and infrequent scene changes.
Something puzzle-y with text. The Carmen Sandiego games have long been a
favorite of mine, and it's a style the Apricot has no problem doing.
More importantly, it's a thing I can throw together quickly. The jam
itself was only eight days (really a week, but with buffer to cater for
time zones around the world).
The DiscMaster Jam gave us a "starter pack" of random assets, just to
serve as inspiration. One of the things I got was Corel Professional
Photos 75:
Owls,
which was just a lot of photos of owls. I decided these owls would be
the villain (anti-hero?) of my game. An owl has stolen one of your prize
disks and ran off with it. You have to track it down using geographical
clues.
To prepare, I played a bit of Carmen Sandiego Deluxe, the CD-ROM version
I played as a kid. It's a hard game! I found it challenging to look up
clues even with Internet access. They did have CD-ROM encyclopedias at
the time (our CD-ROM kit had Grolier Multimedia Encyclopdia), but you
couldn't use it because you were playing the game! I guess in 1993 you
just would have gotten very familiar with a physical set of
encyclopedias.
On the technical front, I decided I needed to develop four things to
pull this off:
- Load and display static images
- Composite images on top of other images
- Draw text boxes
- Play some music
That all seemed doable, but the devil is of course in the details. So
let's meet our devils.
Graphics, such as they are
The Apricot has a very high resolution display for 1983 – it's
only monochrome, but it has a screen resolution of 800x400 pixels. On a
9-inch screen that's a horizontal pitch of ~110 dpi! But what it doesn't
have in any way is a real graphics mode. It has a pseudographics mode
made of 50 rows and 25 columns of 16x16 pixel "character" cells, which
are mapped into individual characters in RAM (because it's of course
just a small modification to the usual 80x25 character mode). That does
provide pixel addressable graphics – every pixel is individually
addressable in the 40K of RAM reserved for alternate character sets and
secondary disk cache.

But in order to address that pixel, you must first translate it to a
character cell and then modify the pixel inside that character cell. You
can speed that up a bit by arranging the characters so each column is a
contiguous 16 pixel wide strip of memory (which I realized much later,
actually), but fundamentally you have to work within this character cell
system.
There is another more practical concern that led me away from pixel
graphics - a full screen of graphics on the Apricot is 40,000 bytes!
That's not a lot nowadays, but I wanted this to fit on and run from a
720K floppy disk. Storing full fidelity images would drastically limit
my options. So I leaned into the limitations.
Tile-based graphics compression
"Well," I thought, "what if I just make an image format that works
directly with 16-bit words of pixels? Deduplicate those into a smaller
list and just store an index into that list to save space?" I
experimented with that for a bit until I realized that it only saves
space if the index is less than 16 bits. Otherwise you're storing a
16-bit index to a 16-bit graphics chunk plus the chunks. That's not
saving space at all. Even if you limit it to just 256 chunks indexed by
a single byte, you get some very jagged graphics and a savings of no
better than 2:1. But I got some neat glitch art out of it.

Clearly the better idea was to index into full 16x16 "tiles". The added
benefit there is you can copy the tiles directly into character RAM and
blast the indexes more or less directly into screen RAM. That certainly
made the comparison heuristic messy (see similarity() in
tools/pic_compress_8x8.py). But with some patience you can coax it
into something that looks decent. I also added an option to "merge"
similar tiles by interleaving the two of them with a checkerboard
pattern. In practice this actually decreased compression by creating
new, unique tiles, but it increased the apparent quality by adding
noise.

The other trick is there are two predefined tiles you can always
reference – all white and all black. This is a pretty common case
for illustrations. Even photographs can benefit from this when you tweak
the contrast.
And with that, you can even load two images in memory at the same time,
assuming they don't both take up more than 40K.
Then I figured out how to composite images on top of each other. This
image format is basically two images compressed with the same block
method – the image data, and a mask. The default white/black tiles
helped a lot for the mask, and the existence of default tiles provides a
short circuit for the masking. If the mask tile is black, you can omit
it. If it's white, it can be referenced directly. Otherwise, you
allocate a new tile that is the composition of both.

Obviously you can't do this indefinitely or you'll run out of memory.
But for one or two additional graphics it works fine. If you treat
character memory like a stack, you can save and load the top of the
stack to deallocate the extra tiles after your overlay is offscreen.
The character-based graphics also still works like characters, so the
text box routines were pretty simple. I found a 16x16 JIS pixel
font
(apparently originally from an X11R6 archive on ftp.cray.com!) with
latin and block drawing characters, which worked a treat. And since
they're still 50% taller than they are wide, it doesn't even look half
bad.

The other tool that helped out a lot is the
Ditherinator, which had many options
for fine tuning a dither (with non-square pixel ratios!). The photos
wouldn't look half as good as they do if I had just used
ImageMagick/GIMP.
So that's three devils down, one to go.
Music and Rhythm
Music was kind of a solved problem. I was already familiar with the
SN76489 as I had already written
APSG, which plays back music tracks
designed for the chip. But that's music written for other systems, and
I'm already committed to stealing from 30 year old asset CDs. And also
APSG is not a good citizen. It hijacks the 8253 timer interrupt and
programs its own timer interval. If I want to rely on DOS working
properly, that's probably a bad idea.
So how do you keep time, anyway? Well, in the IBM PC, the 8253 is set up
to divide a 1.19MHz clock down by 65536, giving you an 18.2Hz tick that
DOS uses as its "system tick". I was pretty sure there was something
like that going on in the Apricot, but the Apricot isn't an IBM PC and
its 8253 is set up differently.

Like the PC, channel 0 is connected to the 8259 PIC, but unlike the PC,
the input clock is divided in hardware to 250KHz. The Technical
Reference doesn't explain how the 8253's divider was set up, so I didn't
know the actual tick rate. Was it emulating the PC's 18.2Hz?
I tried to read the counter value, but that didn't give me anything
useful. Then I went looking through the BIOS to see where it might be
updating it. The Apricot puts the 8253 on IO ports 58-5Eh (on the
Apricot all 8-bit I/O addresses are even because it has a little-endian
16-bit bus). So I fired up DEBUG and searched for instances of OUT
58, AL, which encodes as E6 58.
-s e00:0 fffe e6 58
0E00:0222
0E00:0226
0E00:1AFA
0E00:1AFE
0E00:1BAD
0E00:1BB1
0E00:B336
You need to write it twice to set all 16 bits of the value, so we're
looking at three pairs here. The last one at B336 is probably a red
herring.
I checked 0E00:1BAD:
-u e00:1ba0
0E00:1BA0 73F9 JNB 1B9B
0E00:1BA2 880E7608 MOV [0876],CL
0E00:1BA6 2BC1 SUB AX,CX
0E00:1BA8 2BC1 SUB AX,CX
0E00:1BAA 2D0700 SUB AX,0007
0E00:1BAD E658 OUT 58,AL
0E00:1BAF 8AC4 MOV AL,AH
0E00:1BB1 E658 OUT 58,AL
0E00:1BB3 CDFF INT FF
0E00:1BB5 FB STI
0E00:1BB6 C3 RET
That looks a lot like some value being calculated and stored into
counter 0. But what's this at the end? INT FF? What's going on there?
Well, every interrupt vector has four bytes in the vector table at
address 0. So interrupt FF is at 0:03FC. What's there?
-d 0:3fc
0000:03F0 3A 03 00 0E
That's an address in the BIOS. And what's there is a single IRET used
for all non-functional interrupts. Is that actually being used? Well,
there's one way to check - corrupt the vector table and see what
happens.
I wrote 00 to 0:03F0 and the machine immediately rebooted. 😃
I did some more tests and it turns out the timer ticks at 50Hz. A
predictable result for a machine designed in a PAL country! No doubt
designed that way for compatibility with vertical retrace interrupt
timing on earlier machines. But that was perfect for running a music
loop. I would just have to hook my music routine into INT FF.
Then with the assistance of Nina
Kalinina, a veritable Apricot mage and
general retromancer, I learned that this was in fact documented on a BIOS documentation
disk the whole time. 😅
INT 0FFH - Clock Interrupt - every 20ms
SOURCE: ROM BIOS
INPUT: none
OUTPUT: none
SS, DS, ES must be preserved,
Stack must not be changed if interrupts
are enabled, or a call to the ROM BIOS is to
be made.
Okay but music though...
Finding music is a whole other problem. I was going for the 100%
category, so all my assets had to come from DiscMaster. They could be
remixed and transformed, but they had to start there. And there aren't
just discs full of PSG tunes I can use directly (well... maybe there
were. I didn't check). But there are tons of discs full of MIDI files.
MIDI files?
Long ago, in the before times, it was somewhat difficult to transport
music as full-fidelity PCM audio due to limited bandwidth. So it was
popular to share music in MIDI files, which encapsulated abstract note
data. MIDI was originally a digital transport format for musical data
largely used by musical hardware like keyboards and synthesizers, and
as such depended on the hardware to reproduce the sounds in the MIDI
data. So it sounded a little different on different hardware. Your sound
card might have a cheap FM version of a steel guitar instead of the
rich, wavetable version the author originally used. But because it only
stored the note data, a MIDI file was incredibly compact and well suited
to the low-speed modem Internet of the 90s.
They were so small, there used to be a browser feature that would
automatically download and play a MIDI file when you loaded the page. And
you couldn't turn it off. I had Green Day's "Brain Stew" on mine.
So I wrote some terrible Python code (tools/midi_convert.py) that uses
miditoolkit to parse the
note data into three channel square wave tracks. It's pretty primitive,
as it only considers note on, note off, and initial velocity. But that
was enough to get something working.
But in order for that tool to work you need a three-channel MIDI, so I
used MIDI Editor to pare down and remix
the MIDIs I found on DiscMaster.
The actual audio data format is a very simple byte-oriented format with
three commands - delay by a number of frames (MSB set; can delay from
1-127 frames), send bytes to the SN76489 (MSB clear; can send up to 127
bytes), or stop playing (FFh). So the interrupt handler only really
needs to keep track of a buffer, a position in that buffer, and a
counter. Once it reaches the end, it loops.
The airplane noise is also a "song", but it uses the noise channel to
simulate a doppler faded jet wash. I wrote it by hand, in a hex editor!
It turns out when you stare at this all day for a week, you can just
visualize the sound as bytes. This must be how so many developers made
tunes on 8-bit machines despite not having any real music tools. Brains
are wild, man.
But anyway, this airplane noise triggered a bug on real hardware because
it was the only audio not designed to loop. In MAME, disk access was
fast, so it proceeded to the next scene and loaded that scene's song
before the clip ended. On real hardware, disk access took longer, so the
clip ended and it ran the stop routine, which waits for the next
interrupt. Which never came because it was currently in the interrupt
handler. So it just hung. Easy fix, but that's why you test on real
hardware!
Gamedev like it's 1992
The game itself was written in Pascal and compiled with Turbo Pascal
7.0, which is actually relatively modern compared to the Apricot. It's
from 1992. Pascal is a really good language for its time. Real booleans!
Enumerations! Set types! Unions discriminated by enumerations! I think I
wrote the entire thing in Pascal, graphics and all. It could certainly
be faster, but I also didn't spend a lot of time chasing down pointer
issues.
A ranty aside
Look, Pascal isn't perfect, but if Niklaus Wirth could come up with this
in 1970 and its compiler can run on a Z80 with 64K of RAM, Golang has no
excuse for not having at least this much. And like... you thought
iota was an elegant alternative? Fuck's
sake, Rob. Do better.
Wait, who can actually play this?
The number of people I know who actually have an Apricot I can count on
one hand. Shirley targeting the Apricot for a game jam is a bad idea?
It is, and don't call me Shirley.
But it turns out MAME has nearly perfect support
for the Apricot PC and Xi. So I just packaged up the game as a "windows
game" that ran MAME.
Epilogue
I called the game "Where In The World Is That #@*% Owl!?", and you can
find it over on itch.io. The
game was one of seven category winners in the 100% "Spelunker"
category.
:D
I really recommend checking out all of the games in that
jam, though, as there were
many excellent submissions. My favorites in no particular order were
Super Cats &
Boxes,
Trophy Phishing, Break
In, GET THAT CD
BACK,
copy_on_write, Anglesoft's C2
Prototype,
Business Days, The
Compunaut & the Meaning of
Life,
20XX, and
FileBurning.
You can find the source code for the game over at my git site:
https://git.bytex64.net/where-is-owl/.