Liber VII · Capitulum I
The cartridge
Why there is an ARM inside the cartridge and what each processor does.
A 100 KB game on a 4 KB console
The initial evaluation estimated that the port would need between 100 and 150 KB of code and data and about 16 KB of RAM: the complete logic of the original (scripts, monk AI, pathfinder, doors and objects), the 94 rooms, the music and the texts in two languages. A 2600 has 128 bytes of RAM and sees 4 KB of cartridge. We needed a cartridge that supplied the brain.
The chosen format is ELF: the cartridge carries an ARM processor that runs the game written in C. The console's 6507 does not run code from the cartridge. It runs what the ARM puts on the bus in each cycle in which the 6507 reads an instruction. With the vcsLib library, a call such as vcsWrite3(COLUBK, 0x84) becomes an instruction that the 6507 executes at that very moment.
The ROM, abadia2600.bin, is an ELF file: a relocatable ARM object with the game's code and data, in which the calls to vcsLib are left unresolved. It is the loader of the cartridge, or of the emulator, that copies it into its memory and links those calls to its own vcsLib when loading it; then it calls the game's entry function with the TV system and the clock. On the Chameleon Cart that linking happens at build time: the game is joined to the objects of its firmware into a single executable, which is written to the cartridge.
What weighed in the decision, according to the evaluation: logic in C with plenty of room, bus stuffing writes in 3 cycles (next chapter), the format working on the PlusCart, the UnoCart, the Chameleon Cart, Stella 7 and Gopher2600, and both emulators allowing debugging and measuring. Emulating the original's Z80 was ruled out: it cost about 58,000 T-states per frame and 128 KB of RAM. The logic is a reimplementation in C, checked tick by tick against our own Spectrum emulator.
Who does what
| Who | What it does |
| ARM | All the game logic. It composes the 128 × 160 room into a 1-bit bitmap, with the tiles and the characters. It precalculates the data for each line and synthesises the music. |
| ARM, during the picture | Injects each 6507 instruction on its exact cycle: lda #, sta, nop and one jmp per line so that the program counter never leaves the cartridge's 4 KB. |
| 6507 | Runs what is injected. In the overblank (63 lines) it runs a program from the RIOT RAM while the ARM computes. |
| TIA | Draws and sounds, as always. |
The characters are not TIA sprites: the ARM paints them into the room bitmap, with the original's painter's algorithm, and the TIA only sees one image per line. That leaves the TIA sprites free to draw the room.
The frame: 199 dictated lines and 63 free ones
An NTSC frame has 262 lines. During the 199 lines of the kernel the ARM is tied to the 6507: between two injections it only has the time the 6507 takes to execute the previous one, one cycle after a write, about 40 ARM instructions at 48 MIPS. In the other 63 (overscan, VSYNC and VBLANK), the call vcsStartOverblank() leaves the 6507 running a fixed program in the RIOT RAM, which also writes one sound sample per line, and the ARM is free: about 191,000 instructions at 48 MIPS. All the heavy work goes there: part of the logic, building the room, drawing and preparing the lines of the next frame.
The Chameleon Cart, the PlusCart and the UnoCart
The target cartridge is Phantom Logic Studios' Chameleon Cart, created by Zackary Scolaro (ZackAttack): an STM32G070 with a Cortex-M0+ at 64 MHz at most, 128 KB of flash with the firmware inside, and 36 KB of RAM. It is the one that would allow a physical edition. The game fits: with the firmware template it takes 93.6 % of the flash and 97.6 % of the RAM, after saving everything possible (the saved game only in the SaveKey, the line data of every mode in the same memory, the cover image decompressed over that same RAM, the stack cut from 1.8 to 1.1 KB, and the old wide scoreboard, no longer used by any mode, removed).
On the Chameleon the code runs from flash, and its accelerator can stall in ways that would be fatal in the kernel: if the ARM is late, the 6507 reads garbage. So everything that feeds the 6507 (the game kernel, the cover kernel, the start-up code and the SaveKey, about 8.3 KB) goes in the .RamFunc section, which the firmware copies to RAM at start-up. A tool checks that none of it stays in flash. It already runs on the real cartridge, although its current firmware does not include the bus stuffing functions yet: meanwhile a version without them is built, which only offers DOUBLE and STRIP and starts in DOUBLE. On the console the game hung when pausing: the SaveKey is driven over I²C from the controller port, and between one write and the next the ARM ran out of time. Now each I²C step makes no function calls between writes and ends with a WSYNC, and pause and SaveKey also work on the Chameleon.
The PlusCart by Wolfgang Stubig (Al_Nafuur) (and the UnoCart, with the same UCA firmware) is another story. The loader in version 2.3.28 of its firmware read the whole ELF into a 96 KB RAM buffer and ran it there, and vcsLib lives in flash, too far away for a short ARM jump. The vcsLib calls are long (#pragma long_calls and some trampolines for memcpy and memset) and the constant data without pointers goes in a separate .rodata section, so that Wolfgang's (Al_Nafuur) new loader leaves it in flash. The game is compiled with one section per datum, but after the partial link they are merged into four: .bss, .data and the two .rodata. With hundreds of sections, Gopher2600, which finds the one for each ARM memory access by going through them one by one, ran at 48 frames per second in COMPLETO; with four, at 121. From version 2.3.32 on, the PlusCart firmware has that loader, and with it the game runs on a real console; with 2.3.28 it did not fit. On these two cartridges the title menu has one more row, EXIT, which hands control back to the firmware: the 6507 is left executing NOP from $1000 and the cartridge starts its menu. To write PLUSCART or UNOCART below it, the game injects two harmless LDA # at startup and checks which output register of the STM32F4 each byte ends up in: the data bus goes through port C on the PlusCart and port E on the UnoCart.
How it all fits
To fit the game in the cartridge, each kind of data is stored in the way that suits it best. The rooms are not even stored as images: as in the original, each one is a list of blocks that a small builder assembles on entry. The rest is compressed only if it can be read at no cost at the moment it is needed.
| Data | How it is stored | Size |
| The 94 rooms | The original's block lists and its builder, rewritten in C. As 128 × 160 images they would take 235 KB. | 11.7 KB |
| Intro and ending music | The AY register changes, frame by frame, with a custom LZ77 whose references point into the compressed stream itself: it is decompressed on the fly, with no window in RAM and a limit on reads per frame. | 1.4 + 3 KB (2.9 + 11.6 uncompressed) |
| In-game tunes and effects | The original's scores, with its player. | 0.5 KB + 1.4 KB of code |
| Title screen | The program of writes for each line, with a custom LZSS with optimal parsing. It is decompressed once, on entering the title screen, over the RAM of the game's line data, which is not in use meanwhile. | 2.8 KB (6.5 uncompressed) |
| Parchment texts | 6 bits per character. | 2.9 KB (3.8 in ASCII) |
| Parchment lettering | Each glyph as a chain code in nibbles that keeps the order of the pen. The English one only stores the six glyphs that change. | 1.5 + 0.3 KB |
| Dialogue | As in the original, sentences made of word numbers; the dictionary at 5 bits per letter. | 0.9 KB of dictionary |
| Sprites | Uncompressed: the engine reads them in any order, and compressed they would only go down to 7.4 KB. | 13.3 KB |