Now that Cab Hustle has been released, I have taken a look at porting it to Windows. The idea is to create a native executable instead of packaging the C64 with a Windows-based emulator. Most of Cab Hustle is written in C with some assembly for time-critical parts and for the interrupt service routine, using the cc65 cross-compiler. So the challenge is to get that existing C64-specific C code to run on Windows. (I’ve done the reverse when porting a Tcl shell to the C64, and my blog post on it discusses some of the limitations of the cc65 compiler.)

Programming C64 Hardware

Cab Hustle interfaces with the hardware of its target 8-bit machine in two ways. First and most importantly, it interacts with the C64 by writing to memory. Those can be registers of ICs, e.g. the register of the VIC-II chip that controls the background color of the screen or one of the registers of the first CIA that allows reading the joystick state. Or it can be regular memory that is used by other hardware than the CPU. For example, the VIC-II reads from a region in memory called Screen RAM to determine what characters to display in which location.

Second, cc65 comes with a library of functions to make working with 8-bit systems such as the C64 easier. For example, cputsxy() writes a string to the screen at the specified coordinates. The cc65 toolchain also handles mapping the ASCII characters in the source code to the correct representation on the C64, specifically to so-called Screen Codes, which is the character encoding to be used when writing to Screen RAM. There are a large number of such functions, but Cab Hustle only uses a handful.

Tools

For the Windows build, I’m using the MinGW-w64 compiler, a version of gcc targeting Windows systems. Specifically, I’m using the Linux version of MinGW, which cross-compiles to Windows. Why Linux? Most of Cab Hustle has been developed on Linux, using the Windows Subsystem for Linux (WSL). The build is already set up for Linux, and overall I prefer a more Unix-like environment.

To get graphics onto the screen (or rather into a window), I am using the SDL library. I’ve used it for some smaller tasks before, e.g. for the simulator I wrote to test the logic for my breadboard VGA generator project.

The Cab Hustle build relies on a handful of Python scripts that transform graphics assets into assembly files that can be assembled and linked into the C64 binary (e.g. _foo: .byte $A0,$A0,$A0,$F9,...). Cc65 comes with an assembler, ca65, that handles these files. To also support MinGW, I changed the Python scripts to instead output C code for inclusion (e.g. const char foo[] = { 0xa0, 0xa0, 0xa0, 0xf9,...). The C version of the transformed assets can easily be read by both compilers.

With these preliminaries out of the way, it’s time to get something onto the screen.

Approach

The Windows version uses shim code (which I call SSF, for “shim sixty-four”) that replicates the aforementioned interfaces and abstracts them so that they can be implemented using SDL. SSF uses a 64 kB memory array that represents the memory of the C64. Whenever Cab Hustle accesses memory directly to interface with hardware, it does so through this array. To facilitate that, I am using a C macro that translates addresses to pointers in the memory array (or when building for the cc65, the macro does nothing).

The main thread of the Windows version reads from the C64 memory array, interprets the bytes similar to the VIC-II graphics chip, and then renders an image to a framebuffer array. An SDL texture wraps that array, and the texture is then rendered into the application’s window. Like most C64 games, Cab Hustle is synchronized to the screen refresh rate. To ensure the timings are right, the texture is currently rendered at a constant 50 fps.

In addition, the main thread monitors for keyboard events and translates those into memory changes that the Cab Hustle code can understand. Specifically, the cursor keys and the left control key are mapped to a C64 joystick by updating the memory array at 0xdc00. The cc65 libraries expose that address using a convenient struct, so it can be accessed as CIA1.pra (Complex Interface Adapter 1, Data Port A). SSF provides these structs as part of its shim as well, pointing into the memory array, so the code can stay the same.

Raster Interrupts

The original interrupt service routine for the raster interrupt is implemented in 6502 assembly. For SSF, a callback is provided to the SDL render code that gets invoked before rendering a line of pixels into the framebuffer. Cab Hustle’s ISR does four different things: calling the playroutine for music in the title screen (not implemented on Windows), updating the game counters once per frame, turning the player sprite off before drawing the status screen at the bottom, and turning the sprite back on.

To keep it simple, the Windows callback version of the ISR just returns a status to indicate that all sprites should be turned off for a raster line. That is quick and easy to implement and less cumbersome than what the C64 version has to do: before drawing the status display, saving the sprite pointer for the player sprite and replacing it with a pointer to an empty sprite; after drawing the display, restoring the saved pointer.

Updating the counters is a bit more cumbersome. Just doing that on the render thread at the time of the interrupt has the risk of creating race conditions. Instead, since the game calls waitvsync() periodically to wait for the current frame to be completely displayed, the counters get now updated from there (the function invokes the ISR callback with a special flag). The waitvsync() function waits for a signal using a condition variable that unlocks the thread after the render thread produced a new frame. Races are less of a concern on the C64 side because most things happen on specific raster lines with no risk of conflicting.

It turns out that speed is no issue at all, so there are possible optimizations and simplifications. For example, for a program that always calls waitvsync() once per frame such as Cab Hustle, the render logic could all be handled within that function on the same thread, eliminating risks for any races.

Changes Needed

Some small changes to the main Cab Hustle source code are needed to make things work. Developing for the C64 is by modern standards more similar to writing code for an embedded system than a computer. While the code is in C, it is not written as modern portable C but with some architecture-specific optimizations in mind. For example, 16-bit operations on an 6502 CPU are expensive, so most integers are typed as char where possible.

For cc64, char is unsigned char while for MinGW it is signed char. That caused subtle overflow errors related to detecting when landing on a platform or when trying to find which platform the ship just landed on. Just defining those respective variables explicitly as unsigned char solves it for both compilers.

Any access to memory that is used by C64 hardware needs to be prefixed using a new macro, SSFPTR, which generates a pointer into the memory array for MinGW but just expands to its argument for cc65. Whenever memory for hardware-related access is used, the macro ensures that the right memory is accessed.

In some cases, Cab Hustle uses the PEEK and POKE macros that cc65 provides (and that SSF also implements) to read or write a specific memory location. For MinGW, if a pointer that got the SSFPTR treatment is to be used with those macros, that step needs to be undone to only get an offset into the C64 memory array. Another macro handles getting that offset, SSFADDR, and it also just expands to its argument for cc65.

Status

With all of the above changes and additions in place, the game can be compiled and played on Windows without an emulator. SDL is a multi-platform library, so with some tweaks to the build it could also run on other platforms. What is still missing is sound—there’s no sound or music in the port yet.

If you want to play Cab Hustle in the interim, you can get the C64 version on itch.io and run it in an emulator or on an actual C64.