In early 2020, Songbird picked up the rights to an old, unpublished Lynx game from the 1990s called Quadromania. This was a great find by an Atari fan in Europe, and the demo ROM was released on a small quantity of carts before finding its way to Songbird. As-is, Quadromania is a solid game, but I knew it could do more. This series of blog posts will catalog my efforts to both fix existing issues as well as improve on the original ROM.
The good news: everything in the ROM basically works. The gameplay is solid, scoring works, sound effects are present, and graphics/screens are colorful throughout. Nice; this definitely has the polish of a 1990s-era Lynx game. And there is source code available!
The bad news: The source code is incomplete. Even worse, the “final” version of source doesn’t match the published ROM file (but I wouldn’t figure that out until many, many hours into the debug). And all music was turned off in the ROM even though a music player capable of playing fully digitized samples existed in the ROM.
So, with my limited wits and unlimited curiosity, I set off to modify the ROM and see what could be done. First I created a list of goals. No worry if they are realistic; just need a target to shoot at.
- Expand the ROM from 128KB to 256KB, to allow more assets such as digital music to be added.
- Swap the music, and re-enable music in the ROM for the title screen and game over screen.
- Add music during gameplay.
- Add stereo panning.
- Fix user interface oddities, like Pause working on every screen.
- Fix any bugs found during gameplay.
- Redesign the levels from both a layout and speed perspective, as the game is quite difficult as I received it.
- Redo some graphics like the intro screen and parts of the animated title screen.
- Add EEPROM support so user could start on higher levels.
- Support some kind of encrypted score hash for web competitions.
Expanding the ROM was my first priority. I hoped this would be a relatively simple affair, and it turns out, it was. Before I acquired Quadromania, I wrote one of my first-ever Python tools to decode Atari-format ROM directories. Every Atari-era Lynx game has an encrypted header followed by a series of 8-byte directory entries. The encrypted header is usually (but not always) 410 bytes, followed immediately by the directory. Each directory entry follows this pattern:
byte 0: Block number byte 1-2: Block offset (endian swap) byte 3: Dummy byte 4-5: RAM address (endian swap) byte 6-7: Length (endian swap)
Lynx ROMs are generally 128KB, 256KB, or 512KB, with a corresponding block size of 512 bytes, 1024 bytes, or 2048 bytes. To find the location of the file in the ROM, simply multiply the block size by block number and add the block offset. The length is how many bytes will be copied to the given RAM address.
Many old Lynx games have quite a few directory entries, so they can swap code or graphics in and out of RAM. This makes it hard for the rookie detective to make sense of what’s happening. Fortunately, Quadromania had a very straightforward layout: boot screen followed by RAM load of entire game/graphics followed by several digital audio samples. Thus, to satisfy feature #1 (expand ROM to 256KB), I only had to relocate each file to a new ROM offset and update the corresponding directory entry.
Of course, nothing is ever quite that simple, and the problem I found after doing this was that the game would load and play, but the audio was corrupted. Turns out the digitized music player, which I correctly surmised would match digi.src provided in the old Epyx source libraries, has a ROM block size dependency in one spot in the buffer management. Ugh! Now I needed to find where that spot was in the ROM, which is where the real detective work began…
Looking at digi.src, the only lines of code which had a dependency on the ROM block size (or page size, as it is sometimes called) were these:
inc cartseg lda cartseg cmp #ROMSEGSPERPAGE bne .1 stz cartseg inc CartPage lda CartPage jsr SetCartPage .1 jsr RestoreReturn
Of course, I don’t know the location in the ROM where this check happens, and I certainly don’t know the address of any of the variables like cartseg or CartPage. But I was able to deduce the value for ROMSEGSPERPAGE, since this was a hard-coded value. It was 4 for a 128KB ROM, since 128KB ROMs have a 512 byte block and you divide it by 128 to get ROMSEGSPERPAGE.
I then turned to an online 6502 assembler for help. It doesn’t support all the 65c02 op codes, but that’s OK; it’s close enough. I assembled a tiny slice of code:
cmp #4 bne 10
C9 04 D0 06
The “10” for the branch was just a guess, and wasn’t correct. Ignoring that, however, I could search on C904D0 in the ROM. I found only two occurrences, and only one of those had a lda instruction immediately preceding it. Instruction located! All I had to do was change the “C904” to “C908”, and voila! Quadromania was now fully functional as a 256KB ROM.
Feature #1: SUCCESS
Looking at #2 (re-enable music), I needed to find in the ROM where each piece of music was triggered. By inspecting the partial source I received, I discovered there were only three invocations of a routine called launch_sound. I could also see there were unique values set in the A / X / Y regs prior to the jsr launch_sound instruction. Using a similar detective method to what I did on #1, I built small samples of code in the online assembler and found the unique spots in the ROM where each sequence occurred. Soon it was trivial to swap where each song was triggered, or even overwrite the jsr with nop instructions instead if I wanted to eliminate a spot.
What about adding new music? Well, that was a bit tougher. First I had to demonstrate I could extract existing music samples and play them in an audio tool such as Audacity. I found I could import the audio as a raw signed 8-bit PCM mono and it would play, but the frequency was messed up. So I did trial and error with different frequencies until what Audacity played sounded very similar to what the Lynx played.
I felt like the title screen needed some better music, so I moved the existing music to the game over screen, picked a new song from my library, and converted it into a similar raw audio format using Audacity.
I inserted this new file at the end of the ROM (since I had a lot of extra space after the conversion from 128KB to 256KB, then manually added a new ROM directory entry which pointed to the start of this file. Since it’s digitized audio, the player doesn’t really care how long the audio is; it just needs the start address, and then the code must instruct it how long to play (or more precisely, how many ROM segments to load to RAM in sequence). Quadromania had this set up as a unique variable for each audio clip. So while it was fairly easy at this point to change the music player to point at the new file instead of the old file, the looping was off so you didn’t get a steady beat when the clip would loop back to the beginning. Again, some trial and error was necessary to determine how many ROM segments to load for proper looping. But in the end, I was able to both turn music back on (it was missing in the prototype) and add new music to the title screen.
Feature #2: SUCCESS
I’m happy to share the ups and downs of other features I laid out in future posts, and believe me, I did not achieve success on them all. But the journey was fun! Let me know if you enjoy reading about this process and I will gladly continue.