You can have as much code and data in a Commodore 64 program as you want – you just can’t have it all at the same time.
The CC65 compiler supports an idea called “overlays,” which has kind of fallen out of popular use over the years. The idea is that, if your code is too big to fit in memory at once, you can break it up into pieces. You can call a function in once piece, then load another piece and call a function in it. The result is a little bit like loading a new level in a game, and a little bit like virtual memory, but it’s all done in a very restricted way under the explicit control of the application.
Since I was going to have to deal with it sooner or later, I decided to give overlays a shot before I was in a complete memory crunch. I divided my code into three pieces. The main section contains the loaders, the main() routine, and the map and graphics code that gets used in a lot of places. The first overlay contains the code that’s specific to the map editor, and the second overlay is code for the “gameplay” mode.
The two overlay sections expect to be loaded into the same chunk of memory – all the branches and labels and entry points are set up that way, and the main routine knows where to call into the overlay sections. When the application starts up, it loads the main routine, and then the main routine loads the gameplay section and calls the main gameplay loop. When the user presses the ‘q’ key, the gameplay mode exits and control returns to the main program. It then loads the editor section and starts it. Pressing ‘q’ again switches back to gameplay.
Here’s the code for the main application loop, which is responsible for loading the right code into the overlay area:
// load the gameplay overlay. Shame on me for hardcoding
// the drive number.
cbm_load ("play", 8, (void*)0x7000);
// PlayLoop is a function inside the "play" overlay.
gGameMode = PlayLoop();
// load the map editor overlay
cbm_load ("edit", 8, (void*)0x7000);
// MapEditorLoop is a function in the "edit" overlay.
gGameMode = MapEditorLoop ();
The overlay support in CC65 is mostly handled by the linker. I was already using a custom linker configuration so that I could reserve a large block of memory for map buffers and other data structures. I added two new sections to the memory map, and told the linker to write those sections to new files, “edit.prg” and “play.prg”:
ZP: start = $0002, size = $001A, type = rw, define = yes;
RAM: start = $07FF, size = $6801, file = %O, define = yes;
PLAY_OVERLAYADDR: start = $6ffe, size = $2, file = "play.prg", define = yes;
PLAY_OVERLAY: start = $7000, size = $2000, file = "play.prg", define = yes;
EDIT_OVERLAYADDR: start = $6ffe, size = $2, file = "edit.prg", define = yes;
EDIT_OVERLAY: start = $7000, size = $2000, file = "edit.prg", define = yes;
RESERVE: start = $9000, size = $4000, type = rw, define = yes;
Once you’ve defined the output files like that, you can create new data and code segments for each overlay, and then stick some CC65-specific #pragmas into the code so the compiler knows which segments the code it generates should be placed into. Then you just compile the whole thing at once and it creates all the output files.
You know, I’ve written a tremendous amount of C and C++ over the years, and I never had to muck around with my own linker configuration until I started working with the 6502 CPU. I guess that’s just one of the things you take for granted when you’re working on modern hardware.
Getting all this to work was a lot harder than it should have been because I was trying to be clever (and didn’t pull it off). I foolishly used a loading routine that expected the first two bytes of the file to indicate the memory address where it should be loaded (this is a common convention on the C64). My overlay files didn’t have that feature, and as a result all my code was in the wrong place in memory, offset by two bytes. You’d expect that a bug like that would cause the program to crash immediately and spectacularly, but the actual results were weirder. My character would jump around on the map, collide with nonexistent obstacles, and occasionally the system would load a new map section for no good reason at all. With all the weird symptoms, it took me a while to figure out exactly what was going on.
For some reason, I kept rejecting the simple solution to the problem (load the file myself instead of letting a KERNAL routine do it) until I created a fix that was at least technically correct (the best kind of correct). That’s why I added the PLAY_OVERLAYADDR and EDIT_OVERLAYADDR in the memory map above. Each of those sections are two bytes long, and I’m using them to coax the compiler and linker into putting the correct start addresses at the beginning of the overlay files. I’m sure there’s a more straightforward way to do this, but it worked, and at 2 in the morning that’s good enough.
So enough about esoteric compile features. Next time I promise I’ll talk about something graphical with, like, pictures and stuff!