Apricot Bits

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:

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/.