Dan's MEGA65 Digest : Dan Sanderson

Dan Sanderson

Details

What's new with the MEGA65 personal computer

Recent Episodes

AUG 29, 2026
Calypsi C, part 3: Making large programs
Calypsi C, part 3: Making large programs. Dan’s MEGA65 Digest for August 2026. (Audio) Calypsi C, part 3: Making large programs. A leisurely stroll through a dungeon. Previously, on The Digest… we were looking at ways to fit the original Unix Rogue dungeon crawler game into the MEGA65 memory system using the Calypsi C compiler and a boot loader. The memory map we came up with keeps the KERNAL in memory, so the program can use a KERNAL-based implementation of the C standard library without fuss, in case we needed it. That meant storing both code and data in banks 0 and 1, and using the 45GS02 MAP register to point 24 KB of the 16-bit address space at one of five regions of the 28-bit address space. We got as far as teaching Calypsi how to place code and data into these regions upon request, compiled as if visible at the 16-bit addresses, as well as how to output each region as a separate file that could be loaded into memory by a boot loader program. The 24 KB window works like a giant spotlight pointed at one region at a time. We need a mechanism to move that spotlight when code in one region wants to access code or data in another region. Calypsi doesn’t know how to do this when generating code for function calls, so we also need to revise the Rogue code to move the spotlight at appropriate times. Finally, we need to decide how to organize all of the pieces of Rogue into the regions. I’ve been working with Calypsi’s author hth313 while writing this series, and he has a new version with improvements based on those discussions. Go get Calypsi 5.18 for your platform. A bit of code in this Digest depends on a fix in the most recent version. Let’s see how far we get! MEGA65 News Paused shipping to EU Trenz Electronic has temporarily paused shipments of the MEGA65 to the EU except for Austria and France. A new EU regulation on packaging waste has gone into effect, and many small retailers are pausing shipments while they figure out how to comply. The European Union Packaging and Packaging Waste Regulation (PPWR) seeks to reduce packaging waste, a serious problem in the EU that needs some kind of solution. It covers all packaging for products made for sale in EU member states, including imports. See the European Commission website, as well as this article for more information. Summer of Cores Several alternate core projects have been inspiring each other to make progress this summer. I try to only introduce projects in this space and not report on every incremental improvement, but it’s worth doing a round-up, especially considering the growing interest in the MEGA65 as a multi-core platform. As always, see Boris’s MEGA65 alternate cores website (cores.mega65.org) for up-to-date information. The Amiga 500 OCS core by sy2002 now has a stable version 1 release, with support for a single virtual disk drive, analog CRT displays, and the ability to disable slow RAM. sy2002 is already working on a version 2, with v2 alpha 8 (Discord link) being the latest test release as of this writing. This test release includes read-only physical floppy disk support with the built-in disk drive, up to three virtual drives, and improved support for some disk copy protection. Testers are reporting good results, including the ability to use X-Copy to copy original physical disks to ADF disk images. Note that the core does not yet have the ability to create ADF images on the SD card, so you’ll have to use a PC tool to make some empty images to play with. Be sure to follow along in the Discord when testing alpha releases, and provide feedback. MJoergen and sy2002 are making improvements to the C64 core with two exciting projects: physical internal 1581 floppy drive support, and a built-in RR-Net-compatible network interface based on the MEGA65’s ethernet port. You can test out version alpha 19-X2 (Discord link) to play along. See Sy’s RR-Net article (Discord link to a PDF attachment) for more information. sy2002 has also updated the Game Boy Color core to v1.0 (Filehost link) to support the latest R6 mainboards and use the latest MiSTer2MEGA framework. The Game Boy Color core was a very early alternate core, and it’s exciting to see this update bring it forward for the latest hardware. muse5150 is back with an exciting new vintage desktop computer core. Mega //e is an Apple IIe core, with support for .nib disk images, Commodore-style joysticks and Amiga mice, and various video display options. Caution: do not connect an Apple II mouse to your MEGA65, they are not electronically compatible. See also the Mega //e Github repo. Oh, and muse is also working on an Atari 800 core (Discord, Github). Fenix has launched a Sinclair QL core (Github link). The QL was based on the Motorola 68008 CPU at 7.5 MHz, with a multitasking operating system called QDOS. This version 1.0 release supports .mdv disk images, selectable memory configurations and CPU speed. Last but not least, Paolo Pisati (piso) has started an MSX1 core (Discord link), based on the MiSTer core. See the Github repo. The current test version supports PAL video, cartridge ROMs, and Commodore joysticks. Keep up the good work, everyone! VCF Midwest A quick reminder that I’ll be at the Vintage Computer Festival Midwest in Schaumburg, Illinois, USA, September 12-13. It’s one of the biggest volunteer-run vintage computer shows in the United States. Do stop by if you have a chance. VCF West was a blast. I shared a table area with Garry Kitchen and David Crane, and I got to chat with them about GameMaker, Activision, and 6502 assembly language. Steve Wozniak was on stage telling Apple I stories. I showed off Roguecraft and the Amiga core to tons of people. And I brought home too many books, as always. Good times! Common memory maps I know the last few Digest feature articles have felt like a lot of material that seems specific to Rogue and Calypsi, so I want to take a moment to review why this is important. These are generally useful design patterns for large MEGA65 programs. The 32 KB PRG. The DLOAD command can load a PRG up to 53 KB in size, into bank 0 starting at address $2001. This is sufficient for BASIC programs up to 53 KB. For PRGs that contain a BASIC launcher and machine code (assembly language, compiled C code), the default memory map invoked by the SYS command only allows the CPU to see 40 KB of this as machine code. The default Calypsi linker config limits this to 32 KB, which can be amended to use the remaining 8 KB for the heap and soft-stack if needed. The KERNAL remains fully available to the program in this MAP. Short programs and far data. Calypsi uses 16-bit addressing for code, heap, stack, and variables not flagged as “far” data (the __far qualifier). A program that keeps all of its code in the first 32 KB could use upper memory for data, in several ways. One way is to amend the linker config with “far” sections, and tell Calypsi to put specific variables there with the __far qualifier. Another way is for the program to manage upper memory directly, using __far pointers and addresses. A third way—which we haven’t properly introduced yet in the Digest and I’ve been meaning to get to it—is to use the DMA feature of the CPU to copy upper memory into lower memory as needed. The program might not even need to manipulate this memory much if it’s just loading in graphics or sound data, to be pointed at by hardware registers. Such a program can still be a single PRG file, and can still use the KERNAL at any time. Multi-region programs and boot loaders. When machine code outgrows the 32 KB, the next step up is the strategy we discussed previously. KERNAL code stays visible in the upper half of the 16-bit address space (MAPHI), KERNAL variables stay visible in the lower half (MAPLO), and DOS variables remain unmolested in bank 1. There’s room for the heap and soft-stack in the upper half of bank 0. That leaves a 24 KB region of the lower half, $2000-$7FFF, that can be pointed to any region in memory using the MAP register. This requires manually organizing the program into 24 KB regions, and using a technique for transitioning code execution between regions that we’ll discuss in this Digest. It also requires a separate bootstrap program that the user DLOADs and RUNs to pull all of the regions into memory from separate files, and launch the program. KERNAL-less full control. If a program doesn’t need the KERNAL, or only needs it under specific circumstances, the program can take over the entire 16-bit address space and do whatever it wants. It still has to comply with a few requirements of the CPU, specifically the placement of the base page (“zero page”) and CPU stack, and the management of interrupt handlers, which the KERNAL does for us in the other schemes. The program could even switch back to a KERNAL-compatible MAP temporarily to use its features, such as accessing disks, as long as it’s careful to stash and restore KERNAL/DOS variable space and meet other pre-conditions. More control over 16-bit address space gives the program more options for efficiency and speed, with less time spent on MAP switching and far data access. Rogue could be a KERNAL-optional program. It only needs disk access when saving and restoring the game, or updating the high score table. But I don’t want to make the first version too complicated—unless I have to. Dispatching function calls between regions We’ve seen how to convince Calypsi to put code and data into different regions, using the #pragma statement. But all this does is change how the output of the compiler is organized. The code that Calypsi generates for calling a function simply uses the 16-bit address assigned to the fragment in the region. If a function in one region calls a function in another region, we need to add code to the program that changes the MAP before the call, and changes it back afterward. Consider a simple C function, and another function that calls it: int times(int a, int b) { return a * b; } void main() { int result = times(7, 3); } If we compiled this program in Calypsi… well, it’ll outsmart us and generate machine code for a single function that stores the number 21, pre-calculated by the optimizer. For this thought experiment, imagine that optimization is turned off, or that this otherwise doesn’t happen. When the compiler sees times(7, 3) in the definition for main(), it produces code that performs these steps: Prepare the arguments 7 and 3. jsr to the address of the times subroutine. The subroutine executes, prepares the return value, then executes an rts instruction. Manage the value returned by times. One of the biggest tasks of the compiler is to figure out the optimal code to perform these steps, in a way that works for all callers of the times function. In most cases, the compiler puts arguments in the zero page, then adds instructions that move them around to meet the expectations of the function being called. If necessary, it’ll put arguments on the C soft stack. The code that it generates for the times subroutine knows where to find its arguments, and all callers comply with the decisions that the compiler has made. The address of the times subroutine is determined by the linker. The compiler generates object code that leaves the address of the jsr instruction blank, to be filled in later. In this example, the compiler assigns both functions to the same section, and the linker puts them next to each other in memory at distinct 16-bit addresses. Now imagine that we’re using our regional memory system, and the functions are in two separate regions. #pragma clang section text = "r2Text" data = "r2Data" rodata = \ "r2Rodata" bss = "r2Bss" int times(int a, int b) { return a * b; } #pragma clang section text = "r0Text" data = "r0Data" rodata = \ "r0Rodata" bss = "r0Bss" void main() { int result = times(7, 3); } The #pragma clang section directives tell the compiler to generate the object code for each function earmarked for different sections. When the linker goes to place the fragment the compiler generated for the times function, it consults our mega65-crogue.scm file and sees that the r2Text section starts at address $2000, and so wires up the times subroutine to be at that address. When the linker places the main function, it sees the compiler has requested the r0Text section, which is also configured to start at address $2000. So the linker wires up main as if it lives at $2000. Obviously, we wanted this to happen. These subroutines will actually reside in different regions of memory, and we want to add a step to move the MAPLO window from region 0 to region 2 when main calls times. Perhaps the calling code looks something like this: Prepare the arguments 7 and 3. Update MAP to switch to region 2. (Uh oh…) (?) jsr to the address of the times subroutine. The subroutine executes, prepares the return value, then executes an rts instruction. (?) Update MAP to switch to region 0. (?) Manage the value returned by times. Without further changes, the final machine code for main includes the instruction jsr $2000, which is the 16-bit address of the times subroutine in region 2. We want this to occur after we change the MAP, so that times is visible at address $2000. There’s just one problem: the jsr $2000 instruction is in region 0. As soon as we change the MAP, region 0 will no longer be visible, and the CPU won’t see the jsr instruction. For all intents and purposes, you can’t perform a MAP change with instructions in the 16-bit addresses whose MAP is changing. When the CPU performs the map instruction, the program counter advances to the 16-bit address for the next instruction, but the view of memory has changed out from underneath it. The solution is to use a dedicated subroutine for managing the transition, and to put that subroutine outside the regional memory window. In my mega65-crogue.scm file, I reserve a “common” section at addresses $1600-$1FFF for this purpose. The revised function call should look something like this: Prepare the arguments 7 and 3. Call the dispatch routine in the “common” section, giving it the new region (2) and the subroutine address for times ($2000). The dispatch routine performs these steps: Change the MAP to region 2. jsr to the address of the times subroutine. The subroutine executes, prepares the return value, then executes an rts instruction to return to the dispatch routine. Restore MAP to region 0. Perform an rts instruction to return to the caller. Manage the value returned by times. The jsr to the times routine functions correctly because it occurs in the “common” section, unaffected by the MAP change. When times returns, control returns to the dispatch routine, so it can change back to region 0 before returning to the main subroutine. Mixing assembly language and C Recall that to update the MAP register, you perform the following 45GS02 machine code instructions: ; MAPLO = $(E)0 $A0 ; X Accumulator ; Map $E = %1110 = $2000-$7FFF ; Offset $080(00) lda #$80 ldx #$e0 ; MAPHI = $(8)3 $00 ; Z Y ; Map $8 = %1000 = $E000-$FFFF ; Offset $300(00) ldy #$00 ldz #$83 map ; MAP change occurs here. Disable interrupts. eom ; End of MAP change. Re-enable interrupts. This example sets MAPLO so our region window $2000-$7FFF is pointing at region 1, which starts at 28-bit address $0.A000. This uses an offset of $0.8000, so 16-bit address $2000 is 28-bit address $2000 + $0.8000 = $0.A000. Similarly, this example sets MAPHI so 16-bit addresses $E000-$FFFF see KERNAL code at $3.E000-$3.FFFF, using an offset of $3.0000. You must always set both MAPLO and MAPHI when invoking the map instruction, because it’ll use all four A, X, Y, and Z CPU registers. In Rogue’s case, I’ll just always set Y and Z to $00 and $83 when changing MAP. So how do we get this assembly language code into our C program? There are two ways to do this: assembly language files, and inline assembly language inside C functions. Calypsi has its own assembler, which can be invoked with the command as6502. You can build entire assembly language programs with Calypsi, but it is more appropriate to use it to mix assembly language and C in the same project. Given an assembly language source file (typically with the .s filename suffix), as6502 can produce an object file that can be linked with other object files into the final program. As with the C compiler, the object file the assembler produces is not a complete program, and needs the linker to decide where the machine code lives in memory. The Calypsi MEGA65 starter project has a Makefile rule that assembles all .s files in the project into object files. I’m using that Makefile, so I merely need to create .s files alongside .c files, and they will be built into my project. Assembly language source code for a C project looks a bit different from a pure assembly language project in other assemblers. The code needs to tell the linker the intended section for the code, and how to make a subroutine available to C code as if it were a function. With some effort, an assembly language routine can be a full C function that other C code can call with arguments, and can return a value. The routine can even access other C functions and global variables. Here’s the beginning of a file named dispatch.s in my code, which sets up region_dispatch as an external symbol that the linker can use as an address for the dispatch code: .public region_dispatch .section commonText, text, root region_dispatch: ; ... rts I also have a header file dispatch.h that describes how the C code should treat this subroutine as a function. I’ll explain this more later, but for now it can just contain this declaration: void region_dispatch(); This is enough for the C program to invoke the region_dispatch assembly language subroutine by calling the region_dispatch() function, without arguments or a return value. The .section directive assigns it to the commonText section, type text (code), which my linker configuration puts in $1600-$1FFF. This code will always be resident, so any code can call it at any time, regardless of the current MAPLO window setting. Assembly language can refer to symbols in other modules, such as global variables or C functions, by declaring them as “externs,” like so: .extern monster_table ; Load the 16th byte from the monster_table memory. ldx #$10 lda monster_table,x If you know that the symbol refers to a zero page address, prefix it with zp: to force zero page addressing: .extern foo lda zp:foo If the symbol refers to a 16-bit value, prefix it with .byte0 or .byte1 to access its individual bytes: .extern some_addr, ptr lda #.byte0 some_addr sta zp:ptr lda #.byte1 some_addr sta zp:ptr+1 Assembly language knows nothing about C structures or types. Take care that the assembly code is treating the memory the way it wants to be treated. For more on Calypsi assembler syntax, see the Calypsi guide, chapter 21: Assembler. For information on passing arguments to or returning values from assembly language functions, see chapter 20: Assembly language interface. Inline assembly language If you just need a few raw CPU instructions inside a C function and don’t need them to be separate functions, you can use inline assembly. You invoke the __asm() directive directly within a C function, and give it a C string containing Calypsi assembly language. This machine code is inserted into the definition of the function. void foo() { // ... __asm( " inc 0xd020\n" " inc 0xd021\n" ); // ... } The C string must resemble an assembly language source file, including a leading space and a newline for each line, as shown. The __asm() directive takes three optional “constraints,” separated by colons. The second and third of these can be a list, delimited by commas. These constraints serve the following purposes: Output variable. The assembly code prepares a result to return to the C code. It does so via a given register, which populates a given C variable. Input expressions. The C code prepares one or more values into registers prior to executing the assembly code. Clobbered registers. The assembly code modifies the given registers. Calypsi will take this into account when optimizing the code that surrounds the assembly code. Here’s the example from the Calypsi guide: char foo(char xx) { char out; __asm( " inx\n" " inx\n" " inx\n" " inx\n" " txa\n" : "=Ka" (out) : "Kx" (xx) : "a", "x" ); return out; } This says: "=Ka" (out) : After executing this inline assembly code, take the accumulator (a) and store it in the local byte variable out. "Kx" (xx) : Prior to executing this inline assembly code, prepare the X CPU register (x) with the value of the local byte variable xx. "a", "x" : This inline assembly code modifies the accumulator and X CPU registers. (The “=” and “K” symbols are required as shown.) If you don’t need a constraint, omit it, but keep its colon. For example, to indicate clobbered CPU registers without output or input constraints: __asm( " inx\n" " txa\n" ::: "a", "x"); Calypsi supports register classes for a, x, y, z, and q for the 45GS02 CPU. You can also request zero page soft registers for the output and inputs, using the desired size as the register class: zp8, zp16, or zp32. The assembly code refers to a zero page output variable address using %0, and refers to zero page input variables using %1, %2, etc. in the order they appear in the input expression list. You can also give these inputs names instead of numbers; see the Calypsi guide for the syntax. For more on inline assembly, see the Calypsi guide, chapter 20: Assembly language interface, section 20.4: Inline assembler. Implementing the dispatch routine With assembly language at my disposal, I can now implement the dispatch mechanism. This needs to be a little bit clever. I mentioned that Calypsi generates code for a function call to prepare arguments, perform the jsr, then process the return value. Ideally, for a dispatched function call, the MAP changes happen just before and after the jsr, so Calypsi can still own the argument and return value prep. If the C code says int result = times(7, 3);, there isn’t really a way for me to inject the MAP changes into Calypsi’s usual sequence. Whatever alternative I provide needs to work for any function type signature. Also, the dispatch routine needs input parameters of its own, including the destination region. Here’s what I came up with: Use global variables to store the destination MAPLO setting and function address. Cast the function pointer to (void (*)(void)) to support all possible function signatures. Cast the dispatch function to ((__typeof__(&(func))), such that Calypsi treats the dispatch function as if it had the signature of the called function. Call the dispatch function with the called function’s arguments. Within the dispatch function, detect the current MAP setting, and push it to the stack so it can be restored later. These type casting gymnastics took me a while to figure out. In particular, __typeof__ is a non-standard but popular extension of the C99 standard. It was added to GNU C a while ago, and only adopted into the C standard starting with C23. (The standard numbers refer to the year they were ratified: C99 in the year 1999, C23 in 2023.) Calypsi supports __typeof__. In dispatch.h: void region_dispatch(); extern volatile uint16_t region_dispatch_dest_maplo; extern void (*region_dispatch_dest_addr)(void); A dispatched call to times(7, 3) might look like this: region_dispatch_dest_maplo = 0xe100; region_dispatch_dest_addr = (void (*)(void))times; ((__typeof__(&(times)))region_dispatch)(7, 3); There’s no way I’m typing all of that out for every dispatched call. That’s what preprocessor macros are for. Back in dispatch.h: #define REGION_0 0xe000 #define REGION_1 0xe080 #define REGION_2 0xe100 #define REGION_3 0xe160 #define REGION_4 0xe1c0 #define REGION_DISPATCH(dest_maplo, func, ...) \ ({ \ region_dispatch_dest_maplo = dest_maplo; \ region_dispatch_dest_addr = (void (*)(void))func; \ ((__typeof__(&(func)))region_dispatch)(__VA_ARGS__); \ }) In the earlier example, we put a function called times() in region 2, and want to call it from the main() function in region 0. Instead of: void main() { int result = times(7, 3); } I can now dispatch between regions like so: void main() { int result = REGION_DISPATCH(REGION_2, times, 7, 3); } The C preprocessor performs transformations on a C source file before handing it over to the compiler. For example, when it sees a #include directive, it replaces it with the contents of the file being included. #define can be used to define names that get replaced with strings. In this example, REGION_2 gets replaced with 0xe100. You can also define macros that take arguments. These look kind of like functions, but they aren’t, it’s just more text substitution. In this example, after REGION_2 is replaced with 0xe100, the REGION_DISPATCH macro expands to its definition with dest_maplo replaced with 0xe000. As of C99, macros support variadic argument lists. The ellipsis (...) at the end of the argument list says that the macro can accept any number of comma-delimited arguments at that point. The special symbol __VA_ARGS__ in the macro definition expands to all of these arguments, preserving the commas. According to the C standard, C99 requires at least one variadic argument. C23 added support for empty variadic argument lists. Calpysi has not back-ported this feature exactly, but it seems to support empty variadic argument lists anyway with the C99 syntax, so I’m not complaining. The slashes at the ends of the macro definition lines are important. Preprocessor macros are required to be on a single effective line of the source file. Each slash says the macro continues on the next line. It’s important to remember that this isn’t C source code, this is a C preprocessor directive that becomes C source code later. The macro expands to multiple lines of code, inside parentheses and brackets: ({ ... }) This is a statement expression, and is critical for this technique to work. A statement expression contains multiple C statements, then ends with an expression. The entire expression evaluates to that last expression. This causes the entire macro expansion to evaluate to the return value of the dispatch function, so the macro can be used just as a function with a return value might be used. This also works for void-type expressions, without changes to this example. Statement expressions are another GNU C extension not strictly supported by the C standard, but they are supported in Calypsi. Here is a complete implementation, in dispatch.s: .public region_dispatch_dest_maplo .public region_dispatch_dest_addr .public region_dispatch .section commonData, data, root region_dispatch_dest_maplo: .word 0x0000 region_dispatch_dest_addr: .word 0x0000 region_dispatch_last_maplo: .word 0x0000 register_stash: .long 0 region_dispatch_stack_i: .byte 0 region_dispatch_stack: .space 32,0 .section commonText, text, root region_dispatch: ; Stash A, X, Y, Z stq register_stash ; Stash the current MAPLO setting on a stack. ldy region_dispatch_stack_i lda region_dispatch_last_maplo + 1 sta region_dispatch_stack,y iny lda region_dispatch_last_maplo sta region_dispatch_stack,y iny sty region_dispatch_stack_i ; Remember the new MAPLO setting, and set MAP. ldx region_dispatch_dest_maplo + 1 stx region_dispatch_last_maplo + 1 lda region_dispatch_dest_maplo sta region_dispatch_last_maplo ldy #0x00 ldz #0x00 map ; The function's region becomes visible here. eom ; Restore A, X, Y, Z. ldq register_stash ; Call the function. jsr (region_dispatch_dest_addr) ; Stash A, X, Y, Z, as returned by the function. stq register_stash ; Pull the caller's MAPLO off the stack, and restore it. ldy region_dispatch_stack_i dey lda region_dispatch_stack,y sta region_dispatch_last_maplo dey lda region_dispatch_stack,y sta region_dispatch_last_maplo + 1 sty region_dispatch_stack_i tax lda region_dispatch_last_maplo ldy #0x00 ldz #0x00 map ; The caller's region becomes visible here. eom ; Restore A, X, Y, Z, as returned by the function. ldq register_stash rts The region_dispatch assembly language routine is standing in for the dispatched function. We are tricking Calypsi into thinking it is the dispatched function so that it prepares arguments and manages return values in the same way as simply calling the function. The first thing region_dispatch does is stash the CPU registers to be restored just before calling the dispatched function, in case they’re important. They may not be, but we don’t want to confuse Calypsi. The dispatch subroutine keeps track of the current state of MAPLO by storing it in a variable every time it is set (region_dispatch_last_maplo). This works fine as long as dispatch is in full control of the MAP register, which it is in this case. There is a way to read the current state of the MAP register by triggering the MEGA65’s Hypervisor, but this is not needed in this case. Before it calls the function, the dispatch subroutine remembers the caller’s MAPLO using a stack data structure. When the function returns, it pulls the caller’s MAPLO off of the stack and restores it, so it can return control to the caller. The stack allows for the function being called to make its own dispatch calls: if the function being called tries to dispatch somewhere else, the callers pile up in the stack, then un-pile as they return. I allocated 32 bytes for this stack, so this mechanism supports 16 “nested” dispatch calls. Disabling Calypsi code reuse There’s one more subtle thing we need to get this technique to work. On its strongest setting, Calypsi’s optimizer will attempt to share pockets of generated assembly code between functions. This is clever and useful, but we have to tell Calypsi to not share code between two functions that live in different regions. Add these two arguments to the compiler (cc6502) command line, in your Makefile: --no-cross-call and --no-interprocedural-cross-jump cc6502 --target=mega65 -O2 ... --no-cross-call --no-interprocedural-cross-jump ... I didn’t know about this one at first and spent days trying to figure out why my program was crashing. I stepped through the generated assembly language, noticed the cross-call, and thought I was sunk. Then kibo told me about these command line arguments, and the day was saved. Thank you kibo! Limitations of the dispatch technique The original proposed memory map for Rogue. (Click to enlarge.) Last month I proposed that this region switching mechanism (the MAPLO window) could accommodate keeping the KERNAL active in the 16-bit address space, and could still use the RAM underneath the KERNAL’s addresses in bank 0 as a mappable region. I described a candidate memory map that did so, with regions all over bank 0 and 1 that avoid colliding with KERNAL and DOS variable space. There’s no reason I couldn’t allocate more regions in banks 4 and 5 if needed, I just wanted to see if this program would fit entirely into banks 0 and 1. The most important thing to remember about this region switching technique is that the compiler will treat everything as if it is accessible at 16-bit addresses. At any given point in the code, the 16-bit address space must meet this expectation with whatever region is active. This works fine if the code in the active region only needs to access static memory (constants, local variables) within its own region, or on the heap, C stack, or “common” areas, which are always visible. When code in the active region needs to access code in another region, it uses the dispatch macro to invoke the function. There is no equivalent mechanism to reach directly into another region’s data space. One way around this is to provide accessor functions that could be called via dispatch. Arguments and return values are managed through the dispatch process via the C stack. Another way around this is to move shared data outside of the region space, such as into the “common” area. There’s not a lot of room in there, but if having just a few shared variables untangles the program, it’s worth doing. You can relocate a module’s data simply by changing the #pragma: #pragma clang section text = "r5Text" data = "commonData" rodata = \ "r5Rodata" bss = "commonBss" More care is needed when passing, storing, and dereferencing pointers. Whatever the pointer points to must be visible when the pointer is dereferenced. The pointer itself can pass through functions in other regions, but the thing that acts on the pointer has to be able to see that data. This can be counterintuitive when it comes to passing string literals as arguments. C makes it look like the string literal is a value that is being passed to the function like any other. What actually happens is the string literal is generated into the current rodata section, and a pointer to that value is passed to the function. If the string literal is an argument to a dispatched function, the literal will not be visible after the map has changed. // This won't work. REGION_DISPATCH(REGION_2, foo, "hello"); One solution for string literals is to tell Calypsi to place all rodata into the “common” area. In my case, I don’t have nearly enough room. Rogue is very string heavy, and makes extensive use of passing string literals as function arguments. Instead, I just barely managed to reorganize functions that take strings into the “common” area, so calls to these functions don’t need the dispatch and the string literals stay visible in the region window. I eventually decided to implement another dispatch mechanism for functions with a single string argument, which happens frequently in Rogue for printing messages. This new mechanism copies the string argument to a buffer in the “common” area, then calls the function with a pointer to the buffer substituted for the string argument. I won’t paste the whole thing here, but this is what it looks like to use it: REGION_DISPATCH_STR(REGION_2, "hello", foo, STRARG); I originally put the default code, cdata, and switch sections in region 0. Calypsi also uses these regions to place the C standard library functions that get pulled in when I use them. I was able to get away with dispatching to standard library functions for a while, but in the end I moved these sections into a common area so I wouldn’t have to worry about it. I also narrowed my dependency on the standard library to just a few features. I didn’t want to make any assumptions about whether the standard library was implemented in a way that was compatible with the dispatch mechanism. While functional, use of this dispatch mechanism is error prone and difficult to debug. If I use the wrong region ID for either the caller or destination, the program compiles but crashes. If I forget to use dispatch when it is needed, the program compiles but crashes. Debugger breakpoints are fraught, because they’re associated with the program counter and not the 28-bit address of the instruction, so a breakpoint intended for code in one region might trip at the same address in another region. You might ask, with all of these limitations, is this dispatch technique even a good idea? I have two answers. For one, mostly yes. Most of these issues come from the fact that my project is a port of existing code that was written for the Unix process memory model. If I were writing a program from scratch for the MEGA65, I’d design the program to the constraints of the memory map. And second, you don’t really have much of a choice. If you have more than 32 KB of code, you have to do something about it, eventually needing a mechanism to bring code from upper memory into the 16-bit address space. A dirty, dirty port The Rogue function call graph. It sure would be nice to end this article series with a complete port of Unix Rogue for the MEGA65 that you could just download and play, even if you’re not interested in the techniques. Unfortunately I don’t have that for you just yet. Just to be clear: My goal was to port the actual original Unix source code to the MEGA65, not just come up with an arbitrary Rogue-like game that vaguely resembles the original. This is a challenging goal. Rogue relies heavily on the Unix process model to simplify its code base. Any function can call any other function, and any function can access any global data, with all of the overhead of calling functions and accessing data handled by the C compiler. The easiest way to write a game in this environment is to put all of the game state into a single public memory blob, and not think too hard about organizing functions into modules. The developers of Unix Rogue didn’t have to think about 24 KB memory banks or cross-region visibility. I wanted to get something working as quickly as possible, so I started making some rash decisions. I tried revising my memory map so that the 23,674 bytes of global data were always visible at 16-bit addresses. To do this, I deleted all the disk-based features, disabled interrupts, ejected the KERNAL, and replaced “region 1” with global memory. This was not a complete solution to memory visibility, because many functions still used local variables—and passed the addresses of those local variables to functions in other regions. I plowed through the code, juggled modules between regions, added dispatch calls, and fixed memory visibility bugs as I went. Dispatch calls generate much more machine code than regular function calls, and simply adding dispatch calls caused modules to overflow their regions, requiring more revision. I wrote Python scripts to help manage this process. Call graph for main.c. I originally tried to use the call graph to help organize code into regions, thinking that “adjacent” code would benefit from being in the same region and limit the number of dispatch calls. I didn’t get very far with this technique. It was much more important to pack regions tightly. This is a classic partitioning problem, with simple approximate solutions. “Longest processing time-first scheduling” is the algorithm I understand, so I wrote a simple Python script that analyzed module sizes and proposed region placements. The compiler makes .lst files for each C module that summarizes the section contributions in bytes at the end. Executable (Text): 4213 bytes Zero initialized (BSS): 4880 bytes Data : 16 bytes The linker’s own crogue-mega65.lst file provides a complete summary of where it puts things and how the sections are filling up, invaluable if I were building up from scratch. In this case, until the regions were packed properly, the linker wouldn’t even finish, printing the relevant information in a giant report of error data. Because the original code wasn’t written to accommodate regions, I ended up with a grotesque amount of dispatch calls. Nearly every cross-module call became a cross-region call. I allowed this just to get to a point where I could see something running, promising myself I’d clean it up later. Milestone: a walkable dungeon, with bugs. I got far enough to prove the point. The game starts, and you can walk around one floor of the dungeon and pick up objects. I’m confident that the rest of the work is just more of the same, cleaning up memory and code visibility bugs. It successfully runs a 90 KB program spread across two banks of MEGA65 memory, all from compiled C code. All of this manual effort of placing code into regions and tracking visibility resembles tasks that you would normally delegate to the compiler. If the compiler were aware of the region-based memory map, it could place fragments into regions automatically, optimize their placement to minimize dispatch calls, and generate the dispatch code itself as part of function calls. This could provide a C abstract machine similar to the Unix linear memory space, such that I wouldn’t need to do any of this by hand. And yes, I tried using genAI coding tools to do this optimization. I won’t go into detail about what I tried and didn’t try, but suffice it to say that inexpensive context windows can’t hold enough information for an LLM to act as a meta-compiler on its own. For my goals of understanding the original Unix Rogue code base and developing the region dispatch system, I didn’t want genAI to completely rewrite the program, so I didn’t make a deeper investment in token costs or genAI workflow. A better approach would be to use genAI to build the meta-compiler—but I’m not inclined to outsource the fun part of my own hobby. 😛 In the end, I’ve decided not to finish the dirty port strategy. I have a way to write large programs in C, and I have bigger Rogue-like fish I want to fry. What I’m doing instead I don’t like promising future results for hobby projects—it takes the fun out of it—so don’t take this as a pledge. But I have made some exciting progress on a new path. Taking what I learned from this dispatch technique, I started a fresh project with a new goal: to implement the maximal memory management strategy. I now have a new C-based game shell with these features: Potential to use almost all of 384 KB of MEGA65 fast memory for C code, data, CPU stack, and compiler-owned variable, heap, and stack memory. Full control over hardware interrupts. Handlers are C routines, and can power animations, sound, and music. Switchable “major” modes. Each major mode has its own memory map, and mode transitions load new code and data regions from disk. Each mode can use all 384 KB of memory, such as a title mode with full-screen murals and menus, and a game mode that repurposes that memory for monsters, potions, treasures, and logic. A switchable KERNAL-active mode specifically for disk access. KERNAL memory is stashed in Attic RAM when the KERNAL is not active, so game modes can use all available space. Game state memory that can be saved to and restored from disk. RRB character graphics baked in. Unused “ROM” space in banks 2 and 3 repurposed for graphics data. This shell could be used for all kinds of games and applications. And really, it’s the right way to do a port of Unix Rogue in the first place. Start with a shell like this, then gradually populate regions with Rogue functionality, one piece at a time. I’m not ready to support this shell as a product. But it’s working, and if I make a full game out of it, I’ll share source code. It’s been a super busy August, with a combination of retro computer work and family vacation. September will be no different! I have stuff planned for VCF Midwest and not nearly enough time to prepare. I have a more laid-back Digest planned for the end of next month, so hopefully that’ll be a nice change of pace. Your support for the Digest is more important than ever these days. Thanks as always for the feedback, encouragement, and contributions. If you’d like to support the Digest, please visit: ko-fi.com/dddaaannn Keep circulating the tapes! — Dan
43 MIN
JUL 15, 2026
Calypsi C, part 2: Using memory
Calypsi C, part 2: Using memory. Dan’s MEGA65 Digest for July 2026. (Audio) Calypsi C, part 2. A mysterious message. The story so far: Back in May, we introduced the Calypsi C cross-compiler, a highly capable optimizing compiler toolchain for multiple vintage and retro computers, including the MEGA65. Calypsi’s default linker configuration works well for pure C programs that compile to a single MEGA65 PRG file, a fine starting point for learning the language and writing small- to medium-sized programs. Then last month we discovered a vintage Unix text game, written in C and based on the curses terminal display library: Rogue: Exploring the Dungeons of Doom. I got the idea in my head that given how portable C can be on a good day, it might be possible to use the original source code for Rogue to make a faithful version for the MEGA65. I would just need to replace the curses display library with a MEGA65 equivalent, and everything would just work, right? Without further changes, I ended up with a program that Calypsi tried—and failed—to build into a single PRG file 112 KB in size. While the MEGA65 has enough memory for that much code and data, its memory architecture and KERNAL don’t support a single PRG file that large. I will need to use advanced memory management, coding, and boot loader techniques, along with some powerful features of Calypsi, to produce a MEGA65 program that will load and run this much code and data. I owe most of this article to kibo and his work on the MEGASPUTM graphic adventure game player, which is built in Calypsi, uses many of these techniques, and serves as a useful code example for large MEGA65 projects in the C language. The final solution uses a combination of multiple techniques, each of which are worth knowing about so you can apply them selectively to your own projects. MEGA65 News Amiga 500 core, in progress Amiga 500 core for the MEGA65, by sy2002. The MEGA65 gets its first Amiga core! sy2002, of the C64 core project, has launched an FPGA core that recreates an Amiga 500. This early version is feature complete, and supports the OCS chipset and PAL video output, with 512 KB of Chip RAM and 512 KB Slow RAM. Connect an Amiga mouse (or mouSTer or Tank Mouse) to port 1, and a joystick to port 2. Disk support is currently limited to 880 KB ADF disk images on the SD card, in a single df0: virtual drive. It loads Kickstart and Workbench from disk images, and can run demos and games. Yes, including Lemmings. You will need to acquire files for Kickstart 1.3 revision 34.5, Workbench 1.3 ADFs, and any other ADF disks you might want to run. Put the Kickstart ROM on the SD card as: /amiga/kick.rom The author has clarified that this was largely an AI-assisted port of MiniMig that he won’t have time to maintain. This version will likely inform an active sibling project that will carry it forward. Head to the announcement post to download it, and don’t miss the project’s documentation website. I know lots of people who have been waiting for this milestone for the MEGA65 project, and I can’t wait to see what develops. Commodore 128 core, in progress Commodore 128 core for the MEGA65, by Stefan Eilers. Meanwhile, Stefan Eilers has a Commodore 128 core in progress, with an early release for people to try. So far only 40-column mode and IEC disk drives are supported. It does not yet support cartridges, 80-column mode, or virtual disk images off the SD card. But there’s meaningful progress, and you can play with it! You will need C128 ROMs for this one as well. The 72 KB system ROM bundle is to be installed as: /c128/boot0.rom The 512 KB drive ROM is: /c128/boot1.rom Check out the Github repo for the project for more information. More MiSTer2MEGA documentation sy2002 has also extended The Ultimate MiSTer2MEGA Porting Guide with a supplement on the QNICE CPU and the shell ROM that provides the pop-up menus. If you’re interested in porting MiSTer cores, be sure to keep track of these useful updates. Goals If I were making a game for the Commodore 64 that required 112 KB of code and data, I’d have no choice but to build it in a way that loaded pieces from disk at moments during play, keeping only a small portion in the C64’s 64 KB of memory at a time. The MEGA65 has 384 KB of main memory, so I should be able to get Rogue to run entirely from memory after it is loaded the first time, using the disk only to save and restore the game state at the request of the player, and perhaps to save a high score table to disk at the end of each game. The DLOAD command can only load a PRG file into a single contiguous region of memory in bank 0, and will refuse to load a PRG file larger than 53 KB. I will need to write my own custom program, known as a boot loader, that loads discontiguous chunks of the game’s code and data into banks 0 and 1. The user will load and run the boot loader program to start the game. This requires splitting the game code and data into chunks. The default Calypsi linker configuration only knows how to build a single PRG file of 32 KB, but it is possible to give the linker more specific instructions to fit the target architecture. I will need to configure the Calypsi linker to organize the program according to my desired memory map. I will also need to tell the Calypsi compiler how to access code and data across banks 0 and 1, once the linker has placed it there. The compiler doesn’t do this automatically: it doesn’t know anything about what the linker will do. I’ll need to be explicit about how I want the machine code that Calypsi generates to use the MEGA65 memory system, according to the needs of the program. Lastly, I want to fit the entire program into banks 0 and 1. Technically that’s 128 KB of space, but that has to include zero page registers, the CPU stack, and dynamic memory allocation space. While the program is loading from disk, the KERNAL will be using disk system memory at the beginning of bank 1, so I can’t just load data directly into that space. In theory, I can hide the KERNAL while I’m not using it, but it would be easiest if I could just keep it in memory. Overall, there’s something between 24 KB and 32 KB of the first two banks that I cannot use for program code and data. So I will have to reduce the size of the game. Some of this will just be a bit of belt tightening. I may be able to reduce the game’s reliance on the C standard library, so I’m pulling in less of its code. Worst case scenario, I’ll just need to use more banks. My target, for now, is to use only banks 0 and 1. Why only 32 KB? Available memory in the initial program memory map. 32 KB doesn’t seem like much. Why is Calypsi’s default maximum PRG size so small? When you type the command DLOAD "SOMEFILE", the MEGA65 loads the complete contents of the file into memory starting at address $2001. It is universally expected that the beginning of this data is a BASIC program that can be RUN. You can combine the DLOAD and RUN commands by simply giving RUN the filename: RUN "SOMEFILE" The file can contain other information beyond the BASIC program. DLOAD just paints all of the data from the file into RAM, and doesn’t care what it is. We take advantage of this to make runnable programs in machine code, such as assembled from assembly language or compiled from a language like C. The start of the PRG file is a short BASIC program that invokes the machine code that appears after the BASIC program in the file. This is what Calypsi produces when you ask it to build a PRG file with the --output-format=prg linker argument. DLOAD loads the program into MEGA65 RAM in bank 0. The next reserved region in RAM starts at address $F700, where BASIC keeps some of its variable storage. DLOAD will refuse to load a PRG larger than $F700 - $2001 = $D6FF bytes, or 55,039 bytes, about 53 KB. This is the largest possible size of a program written in pure BASIC: the tokenized BASIC code cannot go beyond that address. When machine code is involved, we have to take the rest of the MEGA65’s memory system into account, especially how the CPU locates machine code instructions. We’ve covered a few of these concepts in previous Digests, but it’s important enough to review. The MEGA65 memory system has 2^28 = 268,435,456 possible addresses, a 28-bit address space. Some of these addresses are connected to memory, others are connected to peripherals such as the VIC video chip, and most aren’t connected to anything (reserved for future expansion). The 45GS02 CPU has ways it can access any of these addresses, using four bytes to store the address and leaving the top four bits set to zero. The 16-bit address space memory map just after SYS is called, with ROM and I/O registers $C000-$FFFF, shadowing bank 0 RAM. For backwards compatibility with the MOS 6502 lineage of CPUs, most 45GS02 CPU features expect the addresses of things to only be two bytes = 16 bits. In particular, the program counter, which keeps track of the address of the next instruction to execute, is only 16 bits wide. It’d be a shame if these features could only access the first 2^16 = 65,536 addresses of memory. So, instead, the memory system maintains a 16-bit address space that assigns regions of 16-bit addresses to regions of 28-bit addresses. These assignments can be changed by the program as needed. When the KERNAL is active, it uses a 16-bit memory map that puts KERNAL code at 16-bit addresses $C000-$CFFF, hardware registers at $D000-$DFFF, and more KERNAL code at $E000-$FFFF. KERNAL code actually lives in upper regions of the 28-bit address space, within 28-bit addresses $002.0000 to $003.FFFF (banks 2 and 3). There is still RAM at 28-bit addresses $000.C000 to $000.FFFF, but the KERNAL sets the memory system so 16-bit addresses $C000 to $FFFF refer to the KERNAL code and I/O registers. The RAM is shadowed in the 16-bit address space by the KERNAL’s memory settings. This is how the DLOAD command can load a 53 KB PRG file into the RAM, underneath the KERNAL code visible at those 16-bit addresses. This establishes a practical size limit on machine code in a PRG file loaded by the DLOAD command. Without changing the memory settings, the CPU can only see machine code from the PRG file up to address $BFFF. $BFFF - $2001 = $9FFE, or 40,958 bytes, or about 40 KB. It’s entirely reasonable for a program to execute machine code that changes memory settings before accessing code at higher 16-bit addresses. If we were writing an assembly language program, this would be straightforward, because we have complete control over where the entry point of the program is in memory. When writing a C program, we delegate that decision to the linker. We’d need a way to tell the linker to put certain things in certain places if we want more control. But wait, why did I say 32 KB earlier if we have 40 KB? That’s one last detail. Calypsi’s default linker configuration keeps everything in a single region of 32 KB, from 16-bit addresses $2001 to $9FFF. This leaves $A000-$BFFF (8 KB) unassigned. Setting custom linker configuration When we first started with Calypsi, I said the mega65-plain.scm configuration file is included with Calypsi, and it’s for simple programs. Here are the complete contents of this file: (define memories '((memory program (address (#x2001 . #x9fff)) (type any) (section (programStart #x2001) (startup #x200e))) (memory zeroPage (address (#x2 . #x7f)) (type ram) (qualifier zpage) (section (registers #x2))) (memory stackPage (address (#x100 . #x1ff)) (type ram)) (memory freeSpace (address (#x1600 . #x1eff)) (section zpsave)) )) Without understanding the syntax of this file, you can see where it sets the 32 KB limit. We could change #x9fff to #xbfff to extend the maximum PRG size to 40 KB. This file uses the Scheme programming language to define a data structure named memories. You don’t need to know much about Scheme except that all the parentheses need to match, and the little single-quote mark in the 2nd line is important. All of the important settings are within the second set of parentheses. In this case, the configuration file defines a set of memory entries. Each one has a name, such as program or zeroPage, and some properties in their own sets of parentheses. From top to bottom, mega65-plain.scm defines four regions of MEGA65 memory and declares their purpose, like so: program: In the address range $2001 to $9FFF, the linker can use this memory for any purpose. It must use the beginning of the region (address $2001) for the start of the program. Calypsi generates a BASIC PRG preamble that invokes the SYS command with address $200E, which is just enough space for the preamble. zeroPage: In the address range $0002 to $007F, there exists RAM that Calypsi will recognize as the “zero page” (the 45GS02 base page) and use as register space. It stops at $007F to preserve the KERNAL variables in the upper half of the zero page. A program that completely shuts off the KERNAL could change this configuration to dedicate more of the zero page to program registers. stackPage: The 45GS02 CPU stack is in the address range $0100 to $01FF, by default. freeSpace: Memory in the range $1600 to $1EFF is available for programs to use. In this case, it is configured as a place to stash the initial zero page contents when using the #pragma require __preserve_zp directive we saw in a previous Digest. This tells Calypsi that it has 32 KB of space in which to place code, data, and variables, all in bank 0. If the linker runs out of space before it has placed everything, it aborts the process and reports an error. To provide Calypsi with a custom configuration file, create a file similar to mega65-plain.scm in your project, and give it a name that starts with mega65- and ends with .scm. Provide the file path to the linker command line instead of mega65-plain.scm, such as in your Makefile. ln6502 --target=mega65 mega65-crogue.scm -o crogue.prg --output-format=prg ... You can set up the Makefile rule to re-build the project after making changes to the linker configuration file like so: $(PROGNAME).prg: $(OBJS) mega65-crogue.scm $(LN6502) --target=mega65 mega65-crogue.scm --output-format=prg -o $@ $(filter-out mega65-crogue.scm,$^) Calypsi memory concepts Let’s take a moment to clarify Calypsi’s memory configuration terminology. Memory. A region of contiguous memory declared for a particular purpose in the linker configuration, with certain attributes. Example: program, as defined in mega65-plain.scm. Section. A way that memory is used, with an internal name used by the compiler and linker. Example: code represents memory that contains machine code. Fragment. A piece of memory usage that belongs to a section, and is placed into a memory by the linker. Fragments are generated by the compiler. For example, each function in C source code becomes a fragment in a code section. Type. The fundamental usage type of a memory. A section can be bound directly to a specific memory, or can be associated with any memory of a corresponding type. A memory whose type is any allows the linker to decide how to best use the memory. Example: the text type refers to executable code. Placement group. A subdivision of a memory, to allow for assigning multiple types in a single address range. Recall that the compiler takes a C source file and produces an object file. The object file defines fragments that belong to sections. The linker takes all of the object files for the project and tries to place fragments into memory locations based on the linker configuration. For example, code sections would all be placed in a memory configured to accept code. A few useful section types to know about: code : The executable machine code. data : Initialized data, such as global variables assigned a definition with their declaration. zdata : Uninitialized data, which can be set to zero at the beginning of the program. cdata : Constant data, such as string constants. Calypsi can generate a report of all of the linker’s decisions. This is a really handy way to make sure your linker configuration is doing the right thing. Back in May, we saw that the MEGA65 starter project is already set up to generate this report, as the file hello-mega65.lst. The --list-file argument to the linker tells it to produce this file. Here’s a brief excerpt of the report from a short sample program I wrote. The mega65-plain.scm configuration defined these memories: Name Range Size Used Checksum Largest unallocated ------------------------------------------------------------------------------------ zeroPage 00000002-0000007f 0000007e 44.4% none 00000046 stackPage 00000100-000001ff 00000100 0.0% none 00000100 freeSpace 00001600-00001eff 00000900 0.0% none 00000900 program 00002001-00009fff 00007fff 27.7% none none > program-nobits 000032ff-00004380 00001082 100.0% none 00005c7f Calypsi assigned the sections of my program to memories like so: Name Range Size Memory Fragments -------------------------------------------------------------------- registers 00000002-00000039 00000038 zeroPage 1 programStart 00002001-0000200d 0000000d program 1 startup 0000200e-0000203e 00000031 program 5 cdata 0000203f-0000204d 0000000f program 2 data_init_table 0000204e-00002057 0000000a program 1 cdata 00002058-00002067 00000010 program 1 switch 00002068-0000207f 00000018 program 1 code 00002080-000032fe 0000127f program 71 zdata 000032ff-00003380 00000082 program-nobits 11 cstack 00003381-00004380 00001000 program-nobits 1 For a complete list of sections and their types, see the Calypsi manual chapter 6.1: “Sections.” Configuring heap memory A C program can allocate an arbitrary amount of memory determined while the program is running using a built-in memory management system known as the heap. Here’s a trivial example: #include #include #include #define BUF_SIZE 256 void main () { char *buf = malloc(BUF_SIZE); if (buf == NULL) { puts("out of memory\n"); return; } // Trivial example of using the buffer. strncpy(buf, "hello\n", 7); printf("%s", buf); free(buf); } If you build this program with the default mega65-plain.scm, compilation and linking will succeed, and the program will run, but it will report “OUT OF MEMORY” then quit. This is because the default linker configuration does not assign any memory to the heap. To fix this, add the following definitions to the project’s custom linker configuration file: (block heap (size #x2000)) (memory bss_main_lo (address (#xa000 . #xbfff)) (section (heap #xa000) )) The block rule defines a chunk of heap memory for the linker to assign. Most blocks come from the object files of the program, but blocks for the heap and stack need to be defined explicitly. The memory named bss_main_lo assigns the heap to an explicit 8 KB region of memory at addresses $A000 to $BFFF. Try setting up the MEGA65 starter project with this configuration, and see how it changes the report in hello-mega65.lst. Important: Calypsi only knows how to use 16-bit addresses when allocating dynamic memory. While you can manipulate the memory system settings to map these addresses to upper banks, it’s easiest to assume that heap will use RAM in bank 0, and keep its 28-bit addresses un-shadowed when messing with the memory system. Far pointers We have seen how to access specific addresses in bank 0 using pointers. This allowed us to access registers in the $d000-$dfff range by casting the numerical address to a pointer type, then dereferencing the pointer, like so: *((volatile uint8_t *)0xd020) = 7; A pointer such as this one refers to the 16-bit address space. The pointer data is 16 bits, and Calypsi uses the CPU’s 16-bit access mode in the code that it generates. For example, if screen memory is in bank 0, you can use a 16-bit pointer to access it, reading the start address out of the SCRNPTR register. The following code reads the location of screen memory from the lowest two bytes of SCRNPTR as a 16-bit unsigned integer, then treats it as a pointer to a byte: uint8_t *scrnptr = (uint8_t *) (*((uint16_t *)0xd060)); for (int i = 0; i We’ve seen that there are several ways to access color memory, including a way to use registers starting at $D800 as a window into the first 1 KB of color memory, or 2 KB while manipulating the CRAM2K register. (For a full explanation, see “Colour Memory” in the MEGA65 Compendium.) It’s often easiest to access color memory at its physical location at address $FF8.0000. In machine code, you could use 32-bit indirect addressing to get at these addresses. Calypsi can also do this, but it needs to be told explicitly to use a 32-bit pointer. The solution is to use what’s known as a far pointer. It looks like this: uint8_t __far *colormem = 0xff80000; for (int i = 0; i The __far qualifier is part of the pointer’s type, and makes the type distinct from a regular non-far pointer. A far pointer’s data is 32 bits, and Calypsi uses the CPU’s 32-bit access mode in the code that it generates. This typically requires more machine code instructions and some space in zero page, so it takes more CPU cycles and is more difficult for Calypsi to optimize. Calypsi far pointers can refer to any address, but for efficiency purposes, pointer arithmetic (such as adding an index i, as above) is not allowed to leave the bank. If you need to cross banks, Calypsi also has a huge pointer (__huge) that doesn’t have this restriction, at the expense of needing more machine code. There’s currently a bug in Calypsi’s parser where __far and __huge aren’t recognized everywhere they’re supposed to be recognized. The workaround is to use the longer phrase, __attribute__((far)), which works everywhere. The __far keyword is specific to Calypsi. This can be confusing to code editors with support for C language error detection. I’m using VSCode and its C language mode to verify my code as I type, and it doesn’t recognize __far like this. You can tell VSCode to ignore __far by putting the following in the project’s settings.json file, nestled appropriately with other values that might already be in there: { "C_Cpp.default.defines": [ "__far=" ] } Far variables You can tell Calypsi to place a global variable into the MEGA65’s upper memory. To do this, use the __far or __huge qualifier with the variable type. uint8_t __far player_score; This has two effects: The compiler requires the __far or __huge qualifier on pointers that refer to the variable. The compiler adds the variable to a section of type far or huge if it is initialized, or zfar or zhuge if it is not initialized. The linker attempts to place these sections in appropriate memory based on the linker configuration. If no memory is defined to hold far data (as is the case in mega65-plain.scm), the linker aborts with an error. The following configuration declares addressees $1.2000 to $1.3FFF as reserved for far memory: (memory bank1_far (address (#x12000 . #x13fff)) (type data) (qualifier far)) Setting up far memory and using it for variables gives Calypsi permission to spread out your data storage. But remember that it comes at the expense of slower access and more code generated. Every use of a far variable, or dereferencing a far pointer, requires 32-bit indirect addressing, which involves storing an address in zero page and invoking the addressing mode. Reducing printf() In the previous Digest, I mentioned that using C standard library functions can drag a lot of code into your project. While printf() is powerful for formatting output in complex ways with very little calling code, it comes at the cost of a big chunk of library code to support all of its formatting features. This short program uses printf() with one of its most expensive formatting features, rendering floating point numbers: #include void main () { printf("example: %f\n", 22.0/7.0); return; } This produces a PRG of 10,983 bytes. That’s quite a lot for a single function call. Calypsi has a cool trick to help with this. Calypsi actually has several versions of printf() it can choose to include, each with an increasing selection of supported formatting features. The compiler reviews all uses of printf() (and related functions) in your program, and selects the leanest version of the string formatting engine that supports all of the features the program needs. In the previous example, I forced Calypsi to use the beefiest version of printf() by formatting a floating point number (%f). If I change the printf() call to only use integer formatting: printf("example: %d\n", 22 + 7); I get a PRG of 4,860 bytes. That’s a big improvement, and I still have many useful formatting features I can use with this version. While looking over how Rogue uses printf(), I thought maybe I could save a few more bytes by writing my own version, with even fewer features. Perhaps it was foolhardy to try to write my own version of the most famous standard library function of all time, but I got a positive result. By limiting it to just %d, %s, %c, and %%, I managed to write a smaller printf() that brought this test program down to 3,708 bytes. If absolutely no string formatting is needed, the program is better off using puts() instead of printf(): puts("example\n"); This compiles to 2,761 bytes. Calypsi chooses the printf() implementation automatically based on how your code uses it. If for some reason you want to force it to use a specific version, you can pass a command line argument to the linker, such as --rtattr printf=nofloat. For a complete description, see the Calypsi manual, chapter 18.6: “Library on a diet.” Reducing exit() While on this topic, Calpysi can also save a few bytes if your program never exits. Many Commodore programs never bother to exit to the READY prompt because users don’t need it. When the user is finished with the program, they simply reset the computer. Calypsi normally attaches a chunk of clean-up code in case the program returns from the main() function, or calls the exit() function. If your program doesn’t need this exit code, it can request that it be omitted, with a linker argument: --rtattr exit=simplified ln6502 --target=mega65 mega65-crogue.scm ... --rtattr exit=simplified I saw a savings of 87 bytes when I did this with this test program. Not much, but I’ll take all the help I can get. Reducing Rogue C source for the tombstone ASCII art, 482 bytes. If I were writing this game from scratch, I would try to get something working within the default 32 KB, and only start mixing in advanced techniques with a working application. The responsible thing to do with this port would be to cut out entire features—potions, scrolls, even monsters—down to this size, then start making decisions so I can add stuff back in. I’m not going to go quite this far, but I do want to see how much space I’ll actually need to get the game going. I started by stripping out all of the Unix-specific stuff I mentioned last month. I simplified main.c extensively, having the program enter an infinite loop instead of exiting. I kept wizard mode disabled in the build, and deleted code related to encrypting the wizard password. Nearly all screen manipulation was happening in calls to my curses alternative, so I was able to clean up or replace calls to writing to the Unix standard output stream (printf, fflush, etc.). My custom printf implementation is now just a limited sprintf(), which performs string formatting into string memory. This gets written to the screen with my curses library. A big space hog turned out to be rip.c, which handles all of the game ending logic and exiting the program. When you die, this code prints a large ASCII art tombstone with your name on it, and implements this as a collection of large strings stored in program memory (cdata). The tombstone is charming and I’ll want to replace it, but removing the inline strings for the art saved 482 bytes alone. There’s a similar ASCII art mural for the unlikely event when you win the game, which I also removed temporarily. I might replace these with separate files loaded from disk as needed, or load them separately into bank 4, instead of keeping them in program memory during the game. I also disabled the high score file for now. According to the linker, my changes brought the program down to $c9ef of code and $185d of data, with $1000 reserved for cstack and $69a4 of zdata, for a total size of $15bf0, or about 86 KB. Designing a memory map A possible memory map for Rogue. (Click to enlarge.) My goal for now is to get a version of Rogue working that keeps the MEGA65 KERNAL in memory. This gives me the following restrictions, around which I can plan a memory map: Zero page. $0002-$008F are available for Calypsi to use for fast variables, similar to CPU registers. $0090-$00FC are used by the KERNAL for various purposes. CPU stack. $0100-$01FF is the default location of the CPU stack. Calypsi uses machine code instructions for calling subroutines, and may use the CPU stack for data. Calypsi also maintains a separate “C stack” for function arguments and such, located elsewhere. MAP register. The CPU MAP register determines how the 16-bit address space maps to the 28-bit address space, in 8 KB chunks. The KERNAL needs $0000-$1FFF (8 KB) to stay “un-mapped,” referring to physical RAM in bank 0. It also needs $E000-$FFFF (8 KB) to map to the KERNAL’s own machine code in bank 3. ROMC and I/O. The KERNAL expects $C000-$CFFF to refer to additional KERNAL code, selected with the ROMC register bit $D030.5. It also expects I/O registers (VIC-IV, SID, etc.) at $D000-$DFFF, selected with the C64-style banking register bits $0001.0-1. The MAP register leaves the 8 KB region $C000-$DFFF “un-mapped” so that these other banking mechanisms maintain control of those pages. Disk variables. When the KERNAL is accessing a D81 disk image or the internal floppy drive, it needs additional variable space in bank 1, at 1.0000-1.1FFF. I won’t be accessing the disk often, but it’s easiest to just assume that memory is reserved for the entire duration of the program. Recall that the MAP register specifies which 8 KB regions are “mapped” or “un-mapped,” and applies an offset to the mapped regions to translate its 16-bit address to a 28-bit address. Also recall that the first four regions must share a single offset setting (MAPLO), as must the last four regions (MAPHI). To keep the KERNAL code mapped at $E000-$FFFF, the MAPHI offset must be $3.0000, which effectively rules out any custom mapping of $8000-$BFFF. If we set any of those regions to “mapped,” they’d use a +$3.0000 offset, which isn’t useful. So $8000-$BFFF stay un-mapped, and refer to memory in bank 0. This is a good place to put the heap. $0000-$1FFF needs to refer to bank 0 memory, so we can’t set that region to “mapped,” at least not with a non-zero offset. But I can leave it un-mapped, and use the rest of $2000-$7FFF (24 KB) as a MAP window to access upper memory. To do so, I would set MAPLO with an offset that gets added to the 16-bit address to form the 28-bit address. This doesn’t have to be memory in upper banks! It can also refer to other addresses in bank 0, to access the RAM being shadowed by the KERNAL ROM and banking mechanisms at $C000-$FFFF. My first guess at a memory strategy is to reserve parts of physical RAM in banks 0 and 1 as described, assign the heap at $0.8000, then divvy up the rest into 24 KB regions that I can access using MAPLO and 16-bit addresses $2000-$7FFF. I get four full 24 KB regions, plus another 8 KB at the end of bank 1 that I can either use the same way, or come up with a special purpose for later. The 28-bit addresses for these regions are 0.2000, 0.A000, 1.2000, 1.8000, and the little one at 1.E000. If I really want to use that little one at the end, I’ll need to account for the color memory window at 1.F800-1.FFFF, such as by pushing COLPTR ahead by 2 KB. In theory, that’s 104 KB I can use for both code and data. In practice, I can only use 24 KB at a time. I will need to do the following: Organize the Rogue code and its corresponding data into self-contained 24 KB regions. Tell the linker which code goes in which region. Tell the linker to treat each region as if it starts at 16-bit address $2000. Tell the linker to produce a separate file for each region that can be loaded into memory. Write a boot loader program that loads each region into its final location. Add code to move the MAPLO window when code in one region wants to call code in another region. We’ll cover the first four steps in the rest of this article. We’ll save step 5 for next month, so we can take our time to enjoy it. Configuring sections Recall that our linker configuration (mega65-crogue.scm) defines a memory named program at addresses $2001-$9FFF, of type any, with some additional parameters to set it up like a runnable program. (memory program (address (#x2001 . #x9fff)) (type any) (section (programStart #x2001) (startup #x200e))) With this configuration, the linker will try to fill this memory with code and data fragments, and also set the startup routine to call main(), wherever it ends up. For Rogue, I want to declare memory for each of my regions, then assign code and data to specific regions. Here is a complete linker configuration for Region 1: (memory crogue-r1-region (address (#x2000 . #x7fff)) (scatter-to r1) (placement-group r1-bits (section r1Text r1Data r1Rodata)) (placement-group r1-nobits (section r1Bss))) (memory crogue-r1 (address (#x0a000 . #x0ffff)) (section r1)) The first memory, crogue-r1-region, declares sections where the linker will place fragments. It sets the 16-bit address range that the linker will use when sewing together branch instructions and data references within the memory. Each type of memory needs its own section, so the linker can group stuff together by type. When inventing section names, use camelCase or underscore_case. Some parts of Calypsi do not support section names containing hyphens. These sections are organized into two placement groups: one for sections that will contain information (the “bits” placement group) and one for the section that will contain uninitialized memory (the “nobits” placement group). Uninitialized memory is filled with zeroes by the start-up code that Calypsi adds to the program. As such, it doesn’t need to be saved to disk the way code and initialized memory is. “BSS” stands for “Block Started by Symbol,” which is the term of art for this type in object files. The “nobits” placement group must come after the “bits” placement group, so the file on disk can simply end just before the “nobits” begins. The second memory, crogue-r1, declares how the sections of the first memory are actually stored in the 28-bit address space. The scatter-to property in the first memory tells Calypsi that it should pretend the fragments are being placed at the 16-bit addresses, but they will actually be stored at the 28-bit addresses of the second memory (section r1). Calypsi needs both sets of addresses because that start-up code uses 28-bit addressing to zero out the uninitialized memory. Regions 2, 3, and 4 are similar, with the same 16-bit address in the first memory, and their unique 28-bits addresses in the second memory. Region 4 has a smaller address range in the first memory because it is only 8 KB. Region 0 gets a bit of extra love, in several ways. For one, it actually starts at $2200 / $0.2200, to leave a little bit of room for the boot loader that I’ll add later. Also, it gets attributes to tell Calypsi to put the startup code at $2200, so the boot loader knows how to invoke the program once it is fully loaded. The startup code provided by Calypsi expects specific section names, types, and placements, which this configuration accommodates. (memory crogue-r0-region (address (#x2200 . #x7fff)) (scatter-to r0) (section (programStart #x2200) (startup #x220d) data_init_table) (placement-group r0-bits (section r0Text r0Data r0Rodata code cdata switch)) (placement-group r0-nobits (section r0bss))) (memory crogue-r0 (address (#x02200 . #x07fff)) (section r0)) There’s a minor oddity here due to how I have chosen to provide my own boot loader. programStart is actually a BASIC preamble that expects to be at address $2200, and the program’s machine code start address is startup at $220D. I have no use for programStart and it’s wasting a few bytes, but leaving it in is the easiest way to account for the startup code. You can see the assembly language source for the startup code here: /usr/local/lib/calypsi-6502/contrib/MEGA65-SDK/src/commodore-startup.s I bet I could customize this further, but this is fine for now. With a custom configuration like this, Calypsi needs me to decide where to put three special types of memory. We already saw the heap earlier, for dynamic memory. We also saw cstack briefly, which had an implicit location in the default configuration, but we now need to declare it explicitly. Finally, zdata, the default section for uninitialized variables, needs an explicit location. I decided to go with 4 KB of heap at $0.8000, 3 KB for cstack at $0.9000, and 1 KB for zdata at $0.9C00. These were arbitrary decisions just to get the program to build, and I may have to adjust them later if my program runs out of one type of memory. (block heap (size #x1000)) (memory bss_main_lo (address (#x8000 . #x8fff)) (section (heap #x8000))) (block cstack (size #x0c00)) (memory bss_main_hi (address (#x9000 . #x9fff)) (section zdata cstack)) One more and we’re done. As we will see next month, CPU instructions that change MAP have to be careful not to change the mapping of the region containing those instructions. Otherwise, the CPU will pull the rug out from under itself, and won’t be able to see the rest of the instructions that complete the adjustment. I declared a new memory outside of the five swappable regions to contain the code that does the swapping, at $0.1600-$0.1EFF. Other than the name and address ranges, its configuration looks like any of the other regions. (memory crogue-common-region (address (#x1600 . #x1eff)) (scatter-to common) (placement-group common-bits (section commonText commonData commonRodata)) (placement-group common-nobits (section commonBss))) (memory crogue-common (address (#x1600 . #x1eff)) (section common)) Assigning code to sections I can tell Calypsi to put a C module into a section by issuing a pragma at the top of the C source file. The pragma tells Calypsi that all of the fragments produced for the subsequent code in the file should be assigned to the given sections, for each type of fragment. You can get fancy with this, but for my initial attempt I just want everything in a given C module to live in the assigned region, so it is visible when the region is active. The following pragma tells Calypsi to put all code that follows into Region 1. It will do so for all code that it processes until it sees another pragma. #pragma clang section text="r1Text" data="r1Data" rodata="r1Rodata" bss="r1Bss" // Functions and global variables... If I were writing this program from scratch, I would give each region a purpose, and try to organize my code appropriately. For this port of Rogue, I will start by assigning C modules to regions based on their compiled sizes, so that I’m making efficient use of the memory in each region. This can be a delicate balance, and as you’ll see next month, I had to make some sacrifices to cram Rogue code, which was not intended for this, into these regions. Exporting sections as raw files Calypsi cannot build this program as a single runnable PRG file. Instead, I need it to produce one file per region. Each region will be loaded separately by the boot loader. For this technique, edit the Makefile (or wherever you have your linker command), and remove the --output-format=prg and -o hello.prg argument. Replace it with the following arguments: ld6502 ... -o crogue-common.raw --raw-multiple-memories --no-merge-raw-memories --output-format=raw Calypsi will produce a file for each memory in the configuration in which code or initialized data are placed. Empty sections don’t get files. Notice that there is still a -o argument that specifies an output filename, which we were previously using for the PRG name. This name is used for the first non-empty memory that appears in the configuration file. I organized mega65-crogue.scm in address order, so the first non-empty memory is the one named crogue-common, which I named crogue-common.raw in this command line. All subsequent non-empty memories get files named after the 2nd memory, with .raw at the end. I named my memories such that each region’s file has a name such as: crogue-r0.raw Writing the boot loader The linker is compiling the program into five files, but none of them are files that the user can DLOAD and RUN. We need a short program that loads all of the regions into their designated memory locations, then starts the game. My boot loader is a BASIC program. It uses the BLOAD command to load each of the five files, then uses the SYS command to start the program, with the entry point in Region 0. It’ll look something like this: 10 PRINT "LOADING ROGUE..." 20 BLOAD "CROGUE-COMMON",P($01600),R 30 BLOAD "CROGUE-R0",P($02200),R 40 BLOAD "CROGUE-R1",P($0A000),R 50 BLOAD "CROGUE-R2",P($12000),R 60 BLOAD "CROGUE-R3",P($18000),R 70 BLOAD "CROGUE-R4",P($1E000),R 80 SYS $220D All of these files will go on a D81 disk image. See previous Digest articles for examples of command line tools that build disk images, which you can use in your Makefile. For example, MEGASPUTM uses the cc1541 tool that comes with the VICE emulator. I build my disk image such that each memory file is a PRG-type file, but Calypsi doesn’t insert a PRG starting address, so I use the “raw mode” of the BLOAD command (the ,R flag). It’s a best practice to make the boot loader the first program in the disk’s directory listing, so RUN "*" (or the shortcut Shift + Run/Stop) finds it. The D81 disk image building tools often provide some degree of control over file ordering. My own tool that I wrote in Python does this. It’s also a best practice to name the boot loader AUTOBOOT.C65. This is a special filename recognized by the MEGA65 KERNAL. If this file exists on a disk inserted in the drive, the MEGA65 loads and runs it automatically. (You may have seen the Intro Disks do this.) This file also loads and runs when the user enters the BOOT command. We’ve made some good progress! We learned some useful features for accessing upper memory in Calypsi, and how to set up and use the heap for dynamic memory allocation. We figured out a strategy to spread our 86 KB program across several regions of 24 KB each, and use the MAP register to access one region at a time using 16-bit addresses. We also figured out how to tell Calypsi to put specific parts of the program into specific regions, and to write each region as a separate raw binary file instead of a single PRG file. With just a bit of BASIC, we have a way for the user to load and run the program just like any other, using a boot loader that installs the regions according to our plan. There’s one last thing we need for this strategy, and it’s a doozy. Calypsi does not know how to change the MAP register when it needs to switch regions to access code and data. I will have to do this manually in the code itself. For example, if the game loop is in region 0 and the code that handles potions is in region 2, when the player drinks a potion, the code will have to call the drinking function in a special way that switches to region 2, performs the drinking logic, then switches back to region 0 to resume play. I will need to make some decisions about how to organize the regions to keep the region switching code easy to manage. In the next Digest, we will build these last pieces, and (hopefully) finish this project. Thank you and welcome to all of the new Digest patrons that joined last month! If you’d like to support the Digest, visit: ko-fi.com/dddaaannn — Dan
48 MIN
JUN 16, 2026
Classic Rogue
Classic Rogue. Dan&rsquo;s MEGA65 Digest for June 2026. (Audio) Classic Rogue. Title screen for Rogue, Amiga version. (Photo courtesy Eric Hill.) It&rsquo;s the game that defined a genre. Rogue: Exploring the Dungeons of Doom by Michael Toy, Glenn Wichman, and Ken Arnold took college campuses by storm in the early 1980s, as a freely distributed game for Unix-based mainframes. Toy and Wichman formed a company to develop versions of Rogue for microcomputers, distributed by Epyx, with notable releases for the Amiga, classic Mac, and Atari ST. It inspired similar dungeon crawlers The Dungeons of Moria (1983), Hack (1984), NetHack (1987), and the Tolkien-themed Angband (1990). NetHack and Angband are both in active development today. Rogue&rsquo;s distinctive combination of gameplay properties, especially procedural level generation, turn-based movement, and &ldquo;permadeath,&rdquo; have persisted in video games over the decades, including recent commercial successes such as Diablo, Spelunky, and Hades. Hobbyist developers love to make &ldquo;Roguelikes,&rdquo; and the annual Roguelike Celebration joyfully encourages creative computing through personal game development. The Roguelike Celebration YouTube channel is packed full of informative and inspiring talks from the last ten years. And of course, Roguecraft DX for the MEGA65 (and Amiga, Game Boy Color, Evercade, and modern PC) by Badger Punch Games is a clear descendant of the seminal Unix game. The authors of the original released the C source code for the Unix version of Rogue under the BSD open source software license in 1986, currently maintained by David Silva. That got me wondering: how could I use the original code to make a faithful version of classic Rogue for the MEGA65? In this Digest, we&rsquo;ll cover how to play the original Rogue on a modern PC by building its vintage source code in just a few easy steps. We&rsquo;ll look around the code to see how it works, and chart a path to porting the game to the MEGA65. Next month, we&rsquo;ll return to the Calypsi C compiler, see how to build large projects with the tools, and get a real MEGA65 version of Rogue up and running. Spoiler alert: Rogue is much larger than 39 kilobytes. MEGA65 News BitBinders MEGA1581 external drive The BitBinders MEGA1581 external IEC floppy drive. BitBinders is now taking orders for the MEGA1581, a MEGA65-themed external 1581-compatible IEC 3-1/2" floppy disk drive. BitBinders has been making 1581-compatible external drives in interesting form factors for several years. This new drive has a case that matches the shape and color of the MEGA65, taking its place literally by its side. The drive includes JiffyDOS and a 12-month warranty. Craig has shown prototypes of this model at recent computer shows. I have a BitBinders dual drive, and it&rsquo;s good quality and fun to use. Congrats to Craig on the launch! The Ultimate MiSTer2MEGA65 Porting Guide As we know, the MEGA65 is just one kind of computer a MEGA65 can be. You can load alternate cores into the MEGA65&rsquo;s FPGA to run cycle-accurate recreations of other famous microcomputers and arcade game machines, including the Commodore PET, the VIC-20, the Commodore 16 / plus/4, the Commodore 64, the ZX Spectrum, and the TI-99/4A. And the list keeps growing. Hobbyists have been recreating vintage microcomputers using FPGAs for over a decade, with a great deal of work going into producing high quality cores for the MiSTer FPGA platform. MJoergen and sy2002 created the MiSTer2MEGA65 framework to make porting MiSTer projects to the MEGA65 hardware easier, and this framework is the basis for many (but not all) of the MEGA65 alternate cores currently available. You can also use the framework for all-new recreations, as well as original computer designs, taking advantage of the framework&rsquo;s well-documented connectivity to the MEGA65 input/output hardware. MJoergen and sy2002 just launched The Ultimate MiSTer2MEGA65 Porting Guide, a comprehensive step-by-step technical manual for using the framework to bring MiSTer projects to the MEGA65. Many hard-won lessons porting cores are fully documented to support future projects. The guide is highly technical, but also walks through every part of the process of porting a MiSTer core to the MEGA65. I&rsquo;m looking forward to trying this myself. Huge thanks to MJoergen and sy2002 for this new book, and for their ongoing contributions to the MEGA65! Upcoming shows in the USA Vintage Computer Festival Pacific Northwest 2026. Vintage Computer Festival Pacific Northwest 2026 took place in May, and the MEGA65 was there! Roguecraft DX was a huge hit, and people also came up to the booth to write BASIC programs and ask lots of questions. The show spanned many decades of computing, with some rare pieces in miraculously working condition available to try. Check out the shared photo album from the event. I will be bringing the MEGA65 booth to these upcoming events around the United States: Pacific Commodore Expo Northwest, Seattle, WA, June 20-21. Vintage Computer Festival West, Mountain View, CA, August 1-2. Vintage Computer Festival Midwest, Shaumburg, IL (outside of Chicago), September 12-13. My exhibit has not yet been confirmed but they haven&rsquo;t turned me down in past years. Last year they just kept growing the space to accommodate all exhibitors, it was wild. How to play Rogue The original Rogue game, running on a modern PC in a terminal window. For the sake of running on modern computers, Rogue is distributed as C source code. You can compile it for your PC with the most common C compiler toolchains, with the included GNU Autotools configure script and GNU Make. On Linux, you probably have the tools installed already. On macOS, xcode-select --install gets you everything you need. Windows users have plenty of options (MinGW, Windows Subsystem for Linux, Cygwin), but I don&rsquo;t have regular access to a Windows computer so you&rsquo;ll have to find them yourself. With the appropriate tools installed, and the Rogue source code downloaded and unpacked in a folder, open a command prompt (terminal) window. Change the current working directory to the Rogue source folder, run the configure script, then run make. cd ./configure make This produces the rogue program. To start a game, run the command. ./rogue The game runs inside the command prompt (terminal) window. If the program exits immediately back to the command prompt, your window may be too small. Rogue needs the window to be at least 80 columns wide and 24 rows tall. It can be larger, though Rogue will keep itself to the upper-left 80 x 24 area. Already this is good news for a MEGA65 port: we have an 80 x 25 character display we can use. Each game of Rogue is different. The layout for the entire dungeon, including the locations of treasures and enemies, is generated based on a pseudo-random number generator and a seed value. You begin knowing only about the room where you first appear. The rest of the dungeon is for you to explore and discover, making a map as you go. You begin in the first room of the first floor of the dungeon, shown in a top-down view. You are the @ symbol. The rectangular room may or may not contain other items, creatures, or a staircase to a lower level. You should see at least one door to a hallway, the + symbol. Hallways (#) connect rooms. (On rare occasions, hallways may have dead ends.) A close-up of a room in Rogue. Rogue is a turn-based action game. To take an action, press a key. For every action you take, other creatures in the dungeon may also take an action. If you&rsquo;re not pressing a key, the game is effectively paused. With so many keys and so many characters, it can be difficult to keep track of what everything does or is. Thankfully, Rogue includes a built-in help feature: To see a list of available actions, press ?, then press *. To be reminded of what a specific key does, press ?, then press the key. Notice that letter keys may be indicated as lowercase or uppercase; uppercase means hold Shift while pressing the key. To identify any on-screen character in the game, press /, then type the character. For example, typing / then + will remind you that + is a door. The game will not display a master list of all possible things. That&rsquo;s for you to discover as you play. The game describes the results of actions at the top of the screen. If you see --More--, press the spacebar to dismiss the message. Other messages are dismissed by pressing Enter; it&rsquo;ll say so. When the game asks a question, it typically expects a single keypress as a response. Spacebar will cancel the action that prompted the question. Escape can also cancel some prompts or exit some screens. On occasion, the game will prompt for a word or phrase. There is a way to complete the game. Locate the Amulet of Yendor below the 20th floor of the dungeon, then escape with your life. According to the Rogue user manual, &ldquo;Nobody has achieved this yet and if somebody does, they will probably go down in history as a hero among heroes.&rdquo; Of course, some people have indeed achieved this in the last 46 years. But take it as a warning. This feat is quite difficult, and most players just gather as much gold as they can before they are viciously murdered by a bat or a hobgoblin. Part of the fun of Rogue is figuring it out as you go. I recommend playing it without knowing more about it. For the sake of the porting project, I will describe the game further, but consider the rest of this article to be a mild spoiler. Of course, the source code itself is a complete spoiler for the game&rsquo;s surprises. Rogue gameplay in more detail To move, use either the cursor keys (up, down, left, right), or the following keys for all eight directions: y k u \ | / h - - l / | \ b j n Every item you find can be picked up, and every creature you find can be fought. To pick up an object or fight a creature, simply walk into it. Keep an eye on your health points (Hp) at the bottom of the screen. If this reaches zero, you die, and the game is over. This will happen often. Health points regenerate gradually over turns not spent fighting. To spend a turn without taking any action (thus regenerating health points), press . (period). Letter characters represent creatures. Some creatures will chase you, others may leave you alone unless you torment them. When an upset creature is next to you, it may attack you for its action, even if you don&rsquo;t attack it. Creatures will chase you down hallways to other rooms. They&rsquo;re relentless. Other things you might find: Gold (*) : Collect gold to increase your score. Food (:) : You keep a supply of food in your inventory. Occasionally, you will feel &ldquo;weak.&rdquo; Press e to eat food to restore your constitution. If you don&rsquo;t have food, weakness impedes your performance. Potion (!) : Each potion has a vague but consistent description. A distinctive feature of Rogue is that you don&rsquo;t know what each potion does until you&rsquo;ve either tried it or learned to identify it, and the effects are randomized per game. (If you&rsquo;ve played Roguecraft DX, this should be familiar.) To quaff a potion in your inventory, press q, then follow instructions. Scroll (?) : Similar to potions, each magic scroll has a randomized effect, unknown until you read it. To read a scroll in your inventory, press r. Armor (]) : You can wear one kind of armor at a time to benefit from its effects. Press Shift + w to wear armor, and Shift + t to take off the armor you&rsquo;re wearing. Staircase (%): To descend to the next level, stand on top of the staircase, then press: > If you&rsquo;re skilled enough to make it to the bottom level of the dungeon, you will need to make your escape by ascending staircases, by pressing: < You may also find armor, weapons, magic rings, and other unidentified objects. Wearable objects must be worn to take effect. See the actions list for the appropriate action keys to don and doff items. To see a list of everything in your inventory, press i. Identifying items without using them is a skill you can pick up later in the game, typically through scrolls. Sometimes, you&rsquo;ll experience a mysterious effect, and the game asks you to give the effect a name, which the game will use to describe it when it occurs later. To review what you have discovered about objects, press Shift + d. Some rooms are too dark to see. You can walk through these rooms like any other, but you will only be able to see the area around you, and won&rsquo;t be able to see walls, creatures, or objects unless you are standing next to them. Be careful! Defeating creatures earns you experience points (Exp), experience points cause you to gain levels, and each level grants you greater strength and maximum health. These mechanics should be familiar from Dungeons & Dragons, and countless tabletop and video games that followed. And that&rsquo;s not even everything in the game! I have yet to actually finish the game or see everything during gameplay. I&rsquo;ll spoil the last few surprises for myself while digging through the code, but it&rsquo;s very deep for such a compact vintage terminal game. The game loop How most games of Rogue end: a tombstone with your name on it. David Silva&rsquo;s code repository for Rogue includes a nice README.md file that describes the structure of the project&rsquo;s C source files. I won&rsquo;t repeat it all here, but I will start traversing it in the order that I found useful. Rogue has 33 C source files (.c), but only three C header files (.h). Nearly all of the declarations that allow functions in one C source file to call functions in other C source files are in a single giant header file, named rogue.h. It&rsquo;s not that big a project and it probably worked fine for the original developers. Personally, I prefer to keep closer track of how modules depend on each other, and one header per module forces each source file to list all of its dependencies at the top of the file. When I was trying to understand Rogue&rsquo;s display code to replace it with a MEGA65 alternative, I had to reorganize some of this. The first order of business is to find the main() function, where the program begins. This entry point is in main.c. Without fully understanding what&rsquo;s going on, we can get a sense that this routine sets up the game: it initializes global data structures, setting them up with the values they are expected to have at the beginning of the game. The main() function then calls the playit() function, also in this file, to play through all of the actual game. When the game is over, main() exits the program entirely, back to the command prompt. For the MEGA65 version, it may be easier to return to a title screen and allow the player to start a new game right away, so the game doesn&rsquo;t have to exit cleanly to the READY. prompt. It is quite charming that Unix Rogue forces the player to re-run the program to play again, one of many ways Rogue blurs the line between the player&rsquo;s reality and the game&rsquo;s fantasy. The playit() function does some more initialization, then drives the turn-based game loop by calling the command() function repeatedly. When the effects of a turn end the game, the code sets the global variable playing to FALSE, which exits the loop. while (playing) command(); Why does the playit() function perform more initialization after main()? Why is it a separate function at all, instead of putting the command() loop directly into main()? playit() is actually called from two places in the code: right away from main(), or later after restoring a saved game from save.c in the restore() function. The player can restore a saved game during a turn, so calling playit() again resets a few things to continue the restored game. Notably, this starts a new game loop inside the first one, as if the entire restored game is happening during a single turn of the previous game. Restoring a saved game results in a call stack where the main() function is waiting on the first playit() call to return, which is in turn waiting on the inner playit() call from restoring the game. As long as the player doesn&rsquo;t restore too many times in a single play session, potentially overflowing the limited space for the call stack, this works itself out. Once the playing global variable is set to FALSE, every playit() game loop in the call stack exits, finally exiting the program from main(). It&rsquo;s a cheap design, but it works well enough, especially considering that in Rogue, when you die, the program and its call stack die with you. How Rogue uses memory There are three ways that a C program uses memory: Global variables. Memory is reserved for the entire duration of the program at a fixed location, and can be accessed from any function. Local variables. When a function is called, the function&rsquo;s local variables, including function arguments, are reserved for the duration of the function call. They typically live on the call stack, or may borrow registers for faster access if the compiler can determine it is safe to do so without changing the meaning of the program. Dynamic memory. At any point, the program can reserve (or allocate) an arbitrary amount of contiguous memory, whose size is determined during the run of the program. The program can also free (or deallocate) any previously reserved memory, making it available for future use. The C language runtime environment manages this process in an area of memory called the heap, and provides the malloc() and free() functions (and a few others) via the standard library. Rogue uses dynamic memory directly in a few places. For example, memory is allocated whenever the player assigns a name to a discovered thing. Most of the program&rsquo;s use of the heap happens in its display library, which we&rsquo;ll discuss in a moment. All other game state is managed in global variables. For example, the player&rsquo;s hunger level is managed by the food_left global variable. This is initialized to the constant HUNGERTIME in the init_player() function. Most of the global state is declared in extern.h and defined in extern.c. The definitions provide some initialization, though all properties need to be initialized in functions anyway because they might be restored from a saved game during play. Global variables are a challenging software pattern in general because they can cause confusion between modules that have to coordinate how and when the variables change. But they&rsquo;re common in games, especially vintage ones running on small computers. Modern software patterns mitigate the complexities that global variables cause by spending large amounts of memory and CPU time on coordination, luxuries that small computers can&rsquo;t afford, and many games don&rsquo;t need. There&rsquo;s a fourth way that C programs use memory, referencing constant (unchanging, non-variable) data built into the program. Some of this, like messages in strings, is managed by C itself. For some constant data structures, Rogue just uses global variables, such as the monsters table that describes various aspects of how monsters behave. If we were writing it today, we&rsquo;d use the const type specifier to tell both the programmer and the compiler that constant data shouldn&rsquo;t change during the run of the program. But this feature wasn&rsquo;t added to the C standard until 1989, years after Rogue was written, so naturally Rogue doesn&rsquo;t use it, except in a couple of places that were added much later in Rogue&rsquo;s lifetime. #define ___ 1 #define XX 10 struct monster monsters[26] = { /* Name CARRY FLAG str, exp, lvl, amr, hpt, dmg */ { "aquator", 0, ISMEAN, { XX, 20, 5, 2, ___, "0x0/0x0" } }, { "bat", 0, ISFLY, { XX, 1, 1, 3, ___, "1x2" } }, { "centaur", 15, 0, { XX, 17, 4, 4, ___, "1x2/1x5/1x5" } }, // ... }; Daemons and fuses The game loop takes a player action, attempts to perform the action within the game state, then makes other updates to the game state. The loop returns to the beginning, awaiting the next player input. Rogue also manages game effects that happen over time. Within the code, these are known as daemons and fuses. A daemon is an effect that takes place periodically. The game loop gives each daemon an opportunity to do something before the players turn or after the player&rsquo;s turn. Daemon effects include moving monsters (runners), healing the player over time (doctor), making the player more hungry (stomach), and spawning new monsters at random moments (rollwand). There&rsquo;s also a daemon that manages a special effect that changes the game&rsquo;s appearance under special circumstances (visuals). A fuse is simply a daemon that runs once, then quits. Fuses manage time-limited effects of potions and spells, and trigger the spawning of monsters under certain circumstances. Rogue can create daemons and fuses dynamically as needed, but only needs a maximum number of them in a regular game. Instead of using dynamic memory, Rogue manages a pool of reserved data space in a table (d_list), then creates and deletes daemons (start_daemon) and fuses (fuse) in the table. The game loop iterates over the table, and performs the before and after actions for each active daemon and fuse as needed (do_daemons, do_fuses). This is built as an abstraction to make daemons and fuses easy to implement. The program calls start_daemon with a pointer to a function that performs the duties of the daemon, along with information about the daemon&rsquo;s frequency or duration. The daemon mechanism doesn&rsquo;t know anything about what the function does, it just knows to call the function during certain turns through the loop. Similarly, the function doesn&rsquo;t know anything about when it is being called, it just knows how to fulfill its purpose. For example, the doctor() function updates the player state in global variables (pstats) according to the game&rsquo;s healing rules, and it just does this whenever it is called by the daemon system. Here&rsquo;s an excerpt of daemon.c showing how fuses are implemented, edited for space: #define _X_ { EMPTY } struct delayed_action d_list[MAXDAEMONS] = { _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, _X_, }; struct delayed_action * d_slot() { register struct delayed_action *dev; for (dev = d_list; dev <= &d_list[MAXDAEMONS-1]; dev++) if (dev->d_type == EMPTY) return dev; return NULL; } void fuse(void (*func)(int), int arg, int time, int type) { register struct delayed_action *wire; wire = d_slot(); wire->d_type = type; wire->d_func = func; wire->d_arg = arg; wire->d_time = time; } void do_fuses(int flag) { register struct delayed_action *wire; for (wire = d_list; wire <= &d_list[MAXDAEMONS-1]; wire++) if (flag == wire->d_type && wire->d_time > 0 && --wire->d_time == 0) { wire->d_type = EMPTY; (*wire->d_func)(wire->d_arg); } } The game lights a fuse by calling fuse() with the function to call when the fuse runs out: fuse(swander, 0, WANDERTIME, BEFORE); The game loop calls do_daemons() and do_fuses() for the &ldquo;before&rdquo; phase and the &ldquo;after&rdquo; phase, shortening all fuses and calling fuse functions when their time is up: do_daemons(BEFORE); do_fuses(BEFORE); // ... do_daemons(AFTER); do_fuses(AFTER); This causes the game loop to call swander(0); after WANDERTIME turns, before the last turn. (In this case, WANDERTIME is a macro that generates a random number between 56 and 84.) Curses There is much that is magical about Rogue. One major source of that magic is how the game manipulates the text display to draw the dungeon, player, monsters, objects, messages, and status line. This is all thanks to curses, an early system for building textual user interfaces. The variant ncurses is better known today and widely used. curses is the only software library used by Rogue that is not part of the C standard library, though curses is so common that some version of it is included with most Unix-compatible development environments. Rogue uses curses for pretty much all of its display needs. One strategy for a MEGA65 version would be to make a MEGA65 version of the curses library. Add a MEGA65 version of the keyboard reading routine, and, in theory, everything else would just work. I spent some time teasing out which features of curses Rogue actually uses. Notably, Rogue does not use the ability for curses to treat windows as scrolling displays. The entire game is designed to fit in 80 x 24 frames. A simple layout of curses windows. curses defines the display in terms of windows, overlapping rectangles of characters. Each window has its own cursor, and a program can use the cursor to print characters or strings. When multiple windows overlap, only the frontmost window is visible for a given screen location. curses manages the contents of the windows, and tries to optimize drawing the composite of all of the windows with the fewest terminal operations as possible. Optimization was especially important back when each operation was transmitted to the terminal over slow connections, such as 1200 baud modems over telephone wires. curses windows aren&rsquo;t like a windowed operating system user interface: they aren&rsquo;t draggable, and don&rsquo;t have borders or close buttons. The program can add such features itself by drawing characters into the windows, but it has to manage them itself. The set of functions provided by curses (the application programming interface or API) is fairly intuitive from this definition: initialize the main window (initscr), create and destroy windows (newwin, subwin, endwin), move windows (mvwin), draw at the cursor (waddch, waddstr), move the cursor (wmove), clear the screen and redraw windows (clear, wrefresh), and variants for combined moving and drawing, and for manipulating the main window. Several functions exist for accommodating terminal-specific features (raw, noecho, keypad, leaveok, getmaxx), which a Rogue-specific MEGA65 replacement can leave out. I was impressed to see that Rogue uses curses to read characters from the screen as well (inch and variants, getyx). When the player wants to move in a direction, Rogue checks the screen directly to see if a wall character or a creature character are in the way. This is mostly surprising from the perspective of modern day, where a modern program might figure this out from a data structure that represents the entire dungeon, and update the display based on that data structure. In character-based games on computers with limited memory and CPU speed, it&rsquo;s just easier for the screen itself to be the source of truth for that information. Any wall character on the screen, no matter how it got there, acts like a wall. Plenty of Commodore games do the same thing, especially in short games you find in magazines. Rogue also reads characters from windows to save the game. With no other data structure representing the dungeon, or the portion of the current level known to the player, the best way to save the game is to take a screenshot. When the player restores the game, it loads the display back into the window. Save files contain much more information than this (see rs_restore_file in state.c), but this is the best way to remember the dungeon itself. Rogue uses overlapping windows for a few purposes. The message area at the top might contain multiple lines of text, and this text might overlap the dungeon map. So the message area uses an overlapping window in these cases. curses remembers the main window underneath, so when the upper window closes, the display can be restored. Needless to say, curses is a big deal for Rogue. Back in the day, Rogue was a big deal for curses, perhaps the most popular app to use the library. Unix stuff we don&rsquo;t need There&rsquo;s a lot of stuff in the original Rogue code specific to the Unix-like environment in which it expects to run. I won&rsquo;t cover all of this in detail, but we can list a few things to watch out for that would simply be removed from a MEGA65 version: Signal handling. Unix programs have to know how to respond to incoming signals from the rest of the operating system, such as a request to shut down. The MEGA65 is a single-process operating system and has no such mechanism. Environment variables. All Unix commands run in an environment, and this environment can have variables set by the user or by other programs. Rogue has configurable settings—type o during a game to see what they are—that a user can pre-set using the ROGUEOPTS environment variable. The MEGA65 game will need another way to save and restore settings, or just always start with defaults. Process ID. A Unix program is assigned a process ID when it starts. By default, Rogue uses this, plus the current system clock, to seed the pseudo-random number generator. The MEGA65 has a Real Time Clock, but no process ID. Command line arguments. Unix commands can take arguments that control or parameterize their behavior. Rogue supports two optional arguments, one to display the high score table then quit, and another to generate a random death screen for the player then quit (populating an empty high score table). That&rsquo;s cute, but the MEGA65 can&rsquo;t send arguments to programs, and we don&rsquo;t need these features. The Rogue command can also take the name of a save file to restore. The MEGA65 version will need to accommodate save files another way. Multi-user high score table. The high score table is stored in a shared location on a multi-user system, set up so only the Rogue program can modify it. Multiple users on the same system can see other top scores, and compete for the most gold. The MEGA65 version doesn&rsquo;t need the Unix-specific logic to handle contention between multiple processes accessing the file at the same time. Multi-user resource management. Having every academic in the department playing Rogue at the same time may have overloaded the mainframe on occasion, so Rogue has features to check system load and deny access. This can even check system load while the game is running, and kick the player after a certain amount of time, or if the game process is left idling. if (too_much()) { printf("Sorry, %s, but the system is too loaded now.\n", whoami); printf("Try again later. Meanwhile, why not enjoy a%s %s?\n", vowelstr(fruit), fruit); if (author()) printf("However, since you're a good guy, it's up to you\n"); else exit(1); } We also don&rsquo;t need any of the build automation and conditional compilation that supports building the game for multiple target platforms from the same code base. In theory, we could extend the Rogue code with similar logic for MEGA65-specific extensions, and then someone could use the ./configure script to decide whether to build the MEGA65 version or the Unix version or the macOS version or whatever, all from the same source code. In practice, the MEGA65 version will be drastically different, and, well, it&rsquo;s not worth my time to make it a multi-platform source repository. I&rsquo;ll be starting with the Calypsi C MEGA65 starter code and Makefile, and ditching everything out of the original code base that I don&rsquo;t explicitly need for the MEGA65 version. Wizard mode There are a bunch of places throughout the Rogue code that use conditional compilation to include or exclude features based on preprocessor variables set in config.h or provided to the compiler by the build automation. One of the more interesting ones is MASTER. If this is set, then the compiler produces a version of Rogue with a special &ldquo;wizard&rdquo; mode. When built with this feature, Rogue begins by prompting you for a password. If you know the password, it enables &ldquo;wizard&rdquo; mode for the play session. #ifdef MASTER /* * Check to see if he is a wizard */ if (argc >= 2 && argv[1][0] == '\0') if (strcmp(PASSWD, md_crypt(md_getpass("wizard's password: "), "mT")) == 0) { wizard = TRUE; player.t_flags |= SEEMONST; argv++; argc--; } #endif Wizard mode is a debugging mode for Rogue. It allows the operator to set the random number generator seed (a &ldquo;dungeon number&rdquo;), so that random events become repeatable across runs for testing purposes. A wizard can inspect internal game state during play, and is shown more explicit messages about how actions affect the game. A wizard can also step out of wizard mode temporarily during the game, to see what a normal player would see. It&rsquo;s especially amusing to me that wizard mode is password protected. Rogue was originally a competitive sport on a multi-user system, and perhaps there was too much of a chance that a MASTER version might leak to the public. In its early days, the authors kept the C source code to themselves, and maybe there was a situation I&rsquo;m not thinking of where it was important to have a Wizard mode on a public system that only the authors could access. I might want to keep some debugging features to ensure the MEGA65 version works as intended. But I can drop the code involved in encrypting the password. The beginnings of a MEGA65 version Based on a cursory code review, it looks like the fastest way to get Rogue running on the MEGA65 could involve the following steps: Replace Curses with a MEGA65-specific work-alike, supporting the subset of features used by Rogue. Drop Unix-specific integrations and features. Drop Unix-specific build automations, taking care to keep the output of code generation steps. Simplify or temporarily remove disk-based operations. The Calypsi implementation of the C standard library can probably handle it, but less rigamarole is needed on a single-user microcomputer. I was able to get a simple curses-like windowing system going pretty quickly, leaving out features like scrolling and display update optimization. The original code made some assumptions about the size of the int type being larger than Calypsi, so I had to make some judgement calls about whether it actually needed long values or was just using int for convenience. Some work was done on the original to put as much platform-specific code into just a couple of files, and I tried to drop those files entirely, either by offering MEGA65 equivalents in higher-level functions or just disabling features. A test of my MEGA65 curses-like implementation. After a bunch of building and fixing and building and fixing, I got every source file to compile, using this new library instead of Unix curses. The Calypsi compiler built a bunch of object files, but the Calypsi linker took one look at them and refused to build a PRG file! As we saw last Digest, the default linker configuration from the file mega65-plain.scm is capable of building a program up to 31 KB in size. According to the linker, my version of Rogue needs 112 KB of memory. The MEGA65 has 384 KB of RAM (not counting its 8 MB of Attic RAM), so there are some possibilities. But I will need to dig deeper into Calypsi&rsquo;s feature set, so I can tell it exactly how to use the MEGA65&rsquo;s memory architecture. Each program needs to make its own decisions about accessing memory and working with (or around) the KERNAL, so Calypsi can&rsquo;t do this automatically. There may be ways to reduce the memory footprint as well. I&rsquo;ll also need to consider a boot loader strategy. The default Calypsi configuration accommodates a program up to 31 KB in size, reserving 8 KB for the heap. The DLOAD command could load a runnable PRG file into memory up to 39 KB, at which point the data would collide with the KERNAL if it went any further. To load a larger program, I need a small program that loads the rest of the program according to my needs. In particular, I will need to load code and data into discontiguous regions of memory, so I don&rsquo;t collide with the KERNAL code and variables related to disk handling—the code that&rsquo;s loading the program in the first place. Until next time&hellip; Cliffhanger! Will I manage to get Rogue working on the MEGA65? Tune in next time to find out! Boy it sure would be a shame if the Digest got canceled before the next issue. Better support the Digest! Visit ko-fi.com/dddaaannn to find out how. Same Bat time, same Bat channel! — Dan
37 MIN
MAY 25, 2026
Calypsi C
Calypsi C. Dan&rsquo;s MEGA65 Digest for May 2026. (Audio) Calypsi C. C Programming: A Modern Approach, by K.N. King. I enjoy writing assembly language. No seriously, I do. I like learning about how the computer works down to the bits and bytes, registers, clocks, and signals. I like knowing exactly what my program is going to do, and using bare metal debugging techniques to figure out why it isn&rsquo;t doing what I want it to. I find it meditative to express algorithms in the simplest possible instructions. Writing a program in a low level language requires rigor, patience, and regular breathing exercises. But the end result is quite satisfying, like building a coffee table out of a nice piece of oak. But sometimes you want to build a house. Systems and structures from construction lumber delivered on a flatbed truck, complex designs that need to be set up quickly and iterated upon to get the right result. A game like Tactical Strike needs intricate sets of rules, both for gameplay and for rendering graphics and sound effects. Expressing those rules in assembly language is unnecessarily tedious and error prone, and doesn&rsquo;t lend itself well to prototyping and rapid refinement. I need a higher level language that&rsquo;s good at expressing these structures while minimizing errors. The most popular step up from assembly language is the C programming language. Especially on microcomputers like the MEGA65, C makes it easy to express structures for data and program flow without giving up bare metal control. To write a program in C, you use a compiler that translates your C program into equivalent machine code. As we saw in Cross Development for Fun and Profit, part 2, an optimizing compiler is often better than a human at writing assembly language, especially for large programs. In this Digest, we will start a new journey, writing programs in the C language for the MEGA65. We will introduce the Calypsi C cross-compiler by Håkan Thörngren (also known as hth313 on the Discord), and try out some simple examples. We&rsquo;ll also see how to access the MEGA65 hardware from a C program, and whatever else we may need for small-to-medium-sized projects. In a future Digest, we will follow up with tips and tricks for organizing larger projects that use more of the MEGA65&rsquo;s capabilities. MEGA65 News Elite for the MEGA65 Elite, for the MEGA65. The legendary space trading game Elite (1984) was so popular it was ported to every major platform of the era. Now thanks to xlar54, you can play Elite on your MEGA65. Elite co-author Ian Bell released the original binaries and source files for multiple versions of the game on his personal website back in 1999, on Elite&rsquo;s 15th anniversary. Not content with just a straight port of the C64 version, xlar54 combined segments of the original Elite binary code with his own MEGA65 graphics library. Check out the Github repo for details. Command your Cobra space ship in a fantastic voyage of discovery and adventure, a supreme test of your combat, navigational and entrepreneurial skills. Trade between countless planets, using the proceeds to equip your ship with heat-seeking missiles, beam lasers and other weapons - corporate states can be approached without risk, but unruly anarchies may be swarming with space pirates. Black market trading can be lucrative but could result in skirmishes with local police and a price on your head! However you make your money, by fair means or foul, you must blast onwards through space annihilating pirate ships and hostile aliens as you strive to earn your reputation as one of the Elite! M65Compiler While I was writing this month&rsquo;s feature article about the Calypsi C language cross-compiler tool chain, Craig Taylor (ctalkobt) released a new C language cross-compiler tool chain, which he just calls the M65Compiler. The project&rsquo;s goal is to specialize in producing efficient MEGA65 programs, able to take advantage of the 45GS02 CPU and abstract away upper memory access. The tool chain includes a C compiler, assembler, linker, and other useful tools. M65Compiler is new, and as of this writing ctalkobt is still building out the standard library implementation. Maybe in a later Digest we can compare the various C compilers with MEGA65 support. For now, we can support ctalkobt on this project release, and give it a spin. You can clone the project from Github, and build it for your PC with the traditional GNU build tools. C64 Core updates in alpha testing MJoergen and sy2002 are back, and they&rsquo;re making big improvements to the C64 core. In particular, they&rsquo;re responding to community requests for greater compatibility with unusual cartridges. Other enhancements include the ability to use the simulated REU in tandem with both hardware cartridges and CRT ROM files. This improves support for configurations used by some modern games, utility cartridges, and C64 OS. See this Discord post to download the latest alpha release and help test. We could especially use more testers with access to unusual hardware cartridges. And of course, watch the skies for the upcoming formal release of C64 Core version 6. Introducing Calypsi Calypsi is a cross-development tool chain for writing C programs for multiple vintage and retro computing platforms. The tools are closed source, and free to use. By the time we&rsquo;re done here, you&rsquo;ll want to support the project with a donation, so keep that link handy. The tool chain is available for several target platforms: the MOS 6502 and MEGA65 45GS02, the WDC 65816 with support for the Wildbits F256 (née Foenix F256), and the Motorola 68000 with support for the Foenix A2560 and the Amiga. There&rsquo;s also a version of Calypsi for the HP-41 Nut and NEWT processors for the HP-41 series of calculators, which I&rsquo;ll be excited to try another time. For the host platform, Calypsi is available for macOS, Linux, and Windows. These are cross-development tools: you author your project on your PC, use the tools to build the PRG, then transfer them to your MEGA65 (or the Xemu emulator) to run it. Setting up Calypsi C for the MEGA65 Download the 6502 version of the Calypsi C compiler for your host platform: Visit the Calypsi website, where you can find a link to the latest release. Locate and download the calypsi-6502 package with the highest version number for your host platform. macOS users, look for the .pkg file. Windows users will want the .zip file. Linux users, pick either the .deb, .rpm, or .pkg.tar.zst, as suits your system. Back on the releases page, scroll through the assets, and click on &ldquo;Show all&rdquo; to reveal more files. Download the user&rsquo;s guide, Calypsi6502Guide.pdf. The user&rsquo;s guide has detailed installation instructions for each platform. For macOS users, I will add my obligatory rant about unsigned binaries. When you double-click on the .pkg file, it won&rsquo;t open: macOS will complain that it can&rsquo;t verify the file. Click Cancel; resist macOS&rsquo;s desire to throw it in the trash. Open System Preferences, Privacy & Security, and scroll way down to where it says &ldquo;&lsquo;calypsi-6502-5.17.pkg&rsquo; was blocked to protect your Mac.&rdquo; Click &ldquo;Open Anyway,&rdquo; then click &ldquo;Open Anyway&rdquo; in the dialog that opens. Follow the prompts to authenticate and install Calypsi C. You may need to adjust your command path for your platform. On macOS, Calypsi installs to /usr/local/bin/, which is typically already on your command path. You can invoke the cc6502 command to test it, like so: > cc6502 --version Calypsi ISO C compiler for 6502 version 5.17 It&rsquo;s worth locating the Calypsi home directory. In the case of macOS, this is /usr/local/lib/calypsi-6502/. There&rsquo;s fun and useful stuff in here. Don&rsquo;t want it any more? Run this script: /usr/local/lib/calypsi-6502/uninstall Compiling a program, step by step Let&rsquo;s start with a classic. Create a C source file named hello.c with the following contents: #include int main() { printf("HELLO WORLD!\n"); return 0; } Even though there&rsquo;s almost nothing about this program that&rsquo;s specific to the MEGA65, Calypsi will convert this to a MEGA65 PRG. Indeed, that&rsquo;s one of the reasons the C language exists in the first place. With some constraints, a C program can be written in a way that it behaves as intended no matter what computer you run it on, as long as you have a C compiler for that computer. The language (in theory) is portable. You may have noticed that the iconic message HELLO WORLD! uses uppercase letters. This appears as a matching all-uppercase message in Commodore&rsquo;s default uppercase text mode, but there is some nuance. We&rsquo;ll take a closer look in a moment. There are two phases to converting a program from C source code into the target computer&rsquo;s machine code. The first phase is compilation, performed by a compiler. The compiler takes C source files (.c) as input and produces object files (.o) as output. Try this now, with the Calypsi cc6502 command: cc6502 --target=mega65 hello.c This produces the object file hello.o. An object file contains a mix of machine code for the target platform, in this case the MEGA65, and information about how the source file is organized. In this case, the source file contains a definition of a function named main() that takes no arguments and returns a value of type int. The object file represents this fact for later use. A project can have multiple C source files, and each source file is compiled to its own object file. The second phase is linking, performed by a linker. This takes all of the object files and sews them together to make the final machine code program. Run the Calypsi ln6502 command on our single object file to build the runnable MEGA65 PRG file: ln6502 --target=mega65 --output-format=prg -o hello.prg mega65-plain.scm hello.o The mega65-plain.scm file is a configuration file that is included with Calypsi. It tells Calypsi how to build simple MEGA65 programs. We&rsquo;ll cover this bit in a later issue of the Digest; for now, just know that it is required for Calypsi&rsquo;s linker. The linker produces the file hello.prg that you can run in the Xemu emulator or on real hardware. See the earlier Digest articles Cross Development for Fun and Profit, part 1 and part 2 for a reminder on how to get PRG files from your PC to your MEGA65. With Xemu, you can start the emulator, then drag and drop the PRG file onto the window. Alternatively, you can invoke Xemu from the command line such that it loads and runs the hello.prg program, like so: /Applications/Xemu/xmega65.app/Contents/MacOS/xmega65 -prg hello.prg Either way, Xemu starts up the MEGA65 ROM, injects the PRG file into memory, then invokes the RUN command. HELLO WORLD! is the output of the program. On real hardware, you won&rsquo;t see the address advisory on the screen. Xemu prints that message itself when it injects a PRG. Xemu injecting and running the hello.prg file. Setting up the MEGA65 starter project Managing a project of multiple source files is one of the strengths of the C language. Once you have lots of files, you&rsquo;ll want a better way to invoke the compiler and linker for the entire project without typing the commands out every time. By far the most popular way to do this is with GNU Make, a build automation suite. The make command is included with Linux, can be installed on macOS via the Xcode Command Line Tools (run this command to install: xcode-select --install), and is available for Windows in various ways. Once you have make installed, download the Calypsi MEGA65 starter project. Unzip the file to create a project directory. The most useful file here is Makefile, which knows how to invoke the compiler and the linker with all of the appropriate arguments. Change your command shell&rsquo;s current working directory to the project, then run make: cd Calypsi-MEGA65-hello-world make How to end a program The starter project keeps its C source code in the src/ subdirectory. The file main.c is identical to our hello.c, except for two additional lines: // Preserve zero page to make it possible to return to BASIC #pragma require __preserve_zp This #pragma line tells Calypsi to inject additional code that stashes the contents of the internal memory of the BASIC interpreter before the beginning of the program, just before calling main(), then restores them after the end of the program, just after return 0;. This is handy for playing with common examples of C programs that you might find in books, so you can run a program, then get back to a READY. prompt, so you can run it again, or inspect the state of the system with the MONITOR. We didn&rsquo;t need it in hello.c because the program was very simple, but other programs benefit from this #pragma if returning to the READY. prompt is important. The C language was originally written for the Unix operating system, which has powerful features for running programs as commands at the command prompt, also known as the &ldquo;command shell&rdquo; for how it acts as an intermediary between the program and the operating system. The MEGA65 does not have such a command shell, so some of these features are limited. For example, on a Unix-like system, return 0; returns an integer (int) value of 0 from the main() program to the command shell, indicating that the program completed successfully. On the MEGA65, changing this to a non-zero value (255) has no effect, whereas a Unix-like command shell would report the return value to the user as an error code. Of course, your program is not obligated to return to the READY. prompt, and many Commodore programs don&rsquo;t. Programs like games change too much system state to be able to return to BASIC cleanly. Your finished program may opt to just never exit, like a game that displays its main menu after a &ldquo;Game Over.&rdquo; During development, you might have a point in the program that intentionally enters an infinite loop: int main () { printf("STAY HERE!\n"); while(1) {} } Calypsi allows the main() function to have a void return type, so control flow can reach the end of the function without an explicit return statement. With an int return type, main() must return an integer, or never return. void main() { printf("HELLO WORLD!\n"); } ASCII and PETSCII Like nearly all example C programs, these examples print messages to the screen. When Calypsi encounters a character or string literal value in the source code, it uses the ASCII encoding to convert the characters to bytes for the final program. Commodore computers use PETSCII, not ASCII, so a little bit of care is needed. Every classic introduction to the C language starts with a program that calls the printf() function in the C standard library, like so: printf("Hello world!\n"); By default, Calypsi interprets this string of characters as ASCII codes, and has the MEGA65 print those codes directly. The MEGA65 interprets them as PETSCII, and the message that appears on the screen doesn&rsquo;t look like it does in the source file. The first character in the message is an uppercase H in the source code. In ASCII, this is code 72 (in decimal). In the Commodore default text mode of uppercase and graphics characters, code 72 is an uppercase H, so this appears correctly. The second character in the message is a lowercase e. In ASCII, this is code 101. The PETSCII character 101 is one of the vertical bar graphics characters—actually Shift + E on the keyboard. In uppercase-and-graphics text mode, all of the ASCII lowercase characters will appear as PETSCII graphics characters, which isn&rsquo;t what this code intends. The easiest thing to do is make all of the letters uppercase, and assume the program is starting in uppercase-and-graphics text mode: printf("HELLO WORLD!\n"); Now the letters appear on screen like they do in the C source code: HELLO WORLD! But what if we really want it to say Hello world! in mixed case? The default uppercase-and-graphics text mode doesn&rsquo;t have lowercase letters, so we need to switch to lowercase-and-uppercase mode. There is a PETSCII code for this, code 14. In BASIC 65, you might PRINT CHR$(14) to achieve this. In C, you can use the special sequence \x followed by the hexadecimal representation of the code, in this case 14 = $0E. It looks like this in the string: printf("\x0eHELLO WORLD!\n"); Now all of the uppercase letters in source appear as lowercase on screen: hello world! That&rsquo;s better, but we want the H to appear in uppercase. PETSCII keeps its uppercase letters at codes where ASCII keeps its lowercase letters, and vice versa. One way to capitalize the H in the final PETSCII message is to use a lowercase h in the source code: printf("\x0ehELLO WORLD!\n"); Mission accomplished: Hello world! Personally, I&rsquo;ve gotten used to this when using tools that don&rsquo;t know PETSCII, but it is a bit odd looking and difficult to remember. Calypsi v5.17 has a simple feature to help with this. If you give the compiler the command line option --string-literals=shifted-petscii, Calypsi will swap the letter casing in string and character literals. Add this to your Makefile, or use it when running the cc6502 command: cc6502 --target=mega65 --string-literals=shifted-petscii hello.c Now, as long as you&rsquo;re printing the PETSCII code to switch to lowercase mode, you can use proper letter casing in the source file, and get the intended result. printf("\x0eHello world!\n"); You only need to switch to lowercase mode once, and all characters on the screen will appear in this mode. You may also want to emit code 11 (\x0b), which locks the ability for the user to switch modes by pressing Mega + Shift. printf("\x0e\x0b"); // ... printf("Hello world!\n"); How to end a line The Olympia SM2 manual typewriter, with carriage return lever. The canonical C Hello World program ends the message with another special sequence, a backslash and a lowercase N: \n. The backslash tells the compiler that the next characters are an escape sequence, representing bytes that can&rsquo;t be typed into the string and still conform to C syntax. There are escape sequences to represent non-typeable ASCII control codes, raw byte values, and the C string syntax characters single quote, double quote, and backslash itself. Most C programs intend \n as a way to move the cursor to the beginning of the next line. The history of this maneuver on teletypes and screen terminals is fraught, to say the least. In a typewriter mechanism, moving the cursor to the beginning of the next line is two actions: one action to feed the paper up by one line, and another to return the carriage all the way to the right, so that the next typing position is in the first column. Manual typewriters had various designs for this over the years, but the one that&rsquo;s most familiar to me is a big lever on the carriage. Pushing the lever a little bit feeds the paper up by one line, and shoving the lever the rest of the way with some force returns the carriage to the first column. One big push performs both actions. The ASCII standard assigns separate terminal control codes for each of these actions: the line feed (LF) is code 10, and the carriage return (CR) is code 13. Some people like to tell a story of how this was necessary to accommodate the first mechanical teleprinters to support ASCII codes, such as the Teletype Model 33. As the story goes, the Teletype needed to keep up with characters fed to it at a certain speed. But in the time that it takes for the teletype to perform a &ldquo;carriage return,&rdquo; moving the print head back to its home position, it might receive another code. If the Model 33 received a printable character while it was moving the print head, it would smear the character wherever the print head was at the time. With CR and LF as separate codes, in that order, the teletype could perform the line feed while it is still moving the print head for the carriage return. This story is somewhat apocryphal, at least with regards to ASCII. The CR and LF codes pre-date ASCII—and the automated teleprinters that would use them. Emile Baudot five keys telegraph keyboard, 1884. Source: Journal telegraphique vol8 N 12 decembre 1884 reprod Eric Fischer History of Character code 1874-1968. (Via Wikipedia.) In 1833, physicists Carl Friedrich Gauss and Wilhelm Eduard Weber invented the first electromagnetic telegraph. They constructed a communication line between the Göttingen Observatory with the institute for physics in the town center. From 1837 to 1844, Samuel Morse, Joseph Henry, and Alfred Vail developed Morse code, a method of encoding textual messages over early commercial telegraph lines. Each line carried a single binary state (on or off), and Morse code used pulse duration and pauses to encode messages. By this point, inventors were already developing mechanisms to record received signals on paper tape, and speed up the transmission and receive process performed by human operators. Morse&rsquo;s original code transmitted only numbers, to correspond to entries in a code book. Vail expanded the code to include letters, numbers, and special characters that would spell out full messages. The single-bit nature of the code made it useful in other contexts, such as radio transmissions. From 1870 to 1876, French telegraph engineer Émile Baudot sought to improve on the speed of transmitting Morse code specifically over wires. He originally imagined a six-bit code, entered by an operator using a six key chorded keyboard, sent in parallel over six lines. Based on an idea from Gauss and Weber themselves, Baudot reduced this to five keys and five lines by splitting the six-bit code space into two five-bit code spaces, with reserved &ldquo;shift&rdquo; codes to switch between them mid-message. The five-bit Baudot code kept letters in the first set, and numbers and symbols in the second set. To make the most out of using up five lines, transmission workstations would seat four operators simultaneously, and a motorized mechanism would multiplex the messages on a rhythmic cadence. Converting between text messages and codes was a specialized task, and entering codes by hand was arduous. In 1901, Donald Murray proposed modifications to Baudot code to accommodate typewriter-like keyboard entry on the encoding side, and eventually mechanized typing on the decoding side. Murray&rsquo;s system performed the encoding and decoding offline: the transmitting operator used an alphanumeric keyboard to prepare messages as 5-bit codes on paper tape to be transmitted, and the receiving end would punch the codes back onto paper tape automatically, to be decoded and re-typed as text later. Murray imagined that the receiver could recreate full documents based on these encoded instructions, so he included the first-ever control codes in Murray code: one for carriage return, and one for line feed. Both Baudot code and Murray code were in active commercial use on competing networks. With multiple codes in play, the International Telecommunication Union committee on long-distance telegraphy, the CCIT, advocated for standardization at their 1st Plenary Assembly in 1926. The Baudot code was adopted as International Telegraph Alphabet Nr. 1 (ITA1). At the 2nd Plenary Assembly of the CCIT in 1929, the ITA1 standard added carriage return and line feed, inspired by the Murray code. The revised ITA2 was standardized in 1932. The ASCII standard, published in 1963, had teletypes in mind, but also data storage, computation, network transmission, and additional input and output devices. ASCII was a 7-bit successor to the 5-bit ITA2, replacing Baudot&rsquo;s shift mechanism with a dedicated character bit, and re-ordering codes for collatability, sorting letters alphabetically and numbers numerically. To facilitate the transition to ASCII, the Western Electric TWX network needed to support both ITA2 and ASCII simultaneously. TWX translation bridges used a real-time signal converter that could only convert single codes, and could not translate two ITA2 codes into a single ASCII code. So ASCII kept CR and LF as separate codes. Early computer mainframe operating systems, especially those by the Digital Equipment Corporation (DEC), stored both CR and LF codes as line endings in data files, and sent them verbatim to teletypes and terminals. Later microcomputer operating systems such as CP/M and MS-DOS retained this convention. Meanwhile, Multics took a different path, establishing LF alone as sufficient to end a line, for more optimal use of memory and data storage. Multics systems would translate LF to CR+LF only when sending data to a terminal that needed it. Unix and several others—notably the Amiga—followed Multics. Commodore 8-bits, older Macs, and others decided CR was sufficient for their micros. So much for standards. Having originated on Unix, the C language specifies that the \n escape sequence means the single code for line feed, ASCII code 10. To reconcile the divergent standards across platforms, the C standard library insists that the program declare whether it is reading from or writing to a terminal or data file in &ldquo;text mode&rdquo; or in &ldquo;binary mode.&rdquo; In text mode, a platform-specific implementation of the standard library translates between \n (code 10) in program memory and the platform-specific line ending received from or sent to the device. Starting with version 5.17, Calypsi offers text mode newline translation from the standard library for Commodore targets. When your program uses printf() and the like to send code 10 to the screen terminal or a text file, it is output as code 13, and everything works as intended. Using the C standard library The printf() function is provided by the C standard input/output (I/O) library. The program declares its intent to use this function with this line: #include Calypsi includes an implementation of the C standard library that interfaces with the Commodore KERNAL for terminal and disk operations. The Commodore KERNAL doesn&rsquo;t support all of the same features as a Unix-like system, so this is a best effort. You&rsquo;ll find good support for examples from beginner books on the C language. Here is a program that prints the dimensions of a rectangular prism, prompts for its width, then calculates and reports its volume. You could compile and run this program on nearly every computer with a C compiler. #include // Preserve zero page to make it possible to return to BASIC #pragma require __preserve_zp int main() { int height, length, width, volume; height = 3; length = 4; printf("HEIGHT = %d, LENGTH = %d\n", height, length); printf("WIDTH? "); scanf("%d", &width); volume = height * length * width; printf("\nVOLUME = %d\n", volume); return 0; } When you use the C standard library in your program, Calypsi tries to optimize the PRG file to contain only the parts that you use. This can still be quite a bit of code, even for small programs. With space optimization enabled (the default), this example compiles to a PRG of 12,831 bytes. Considering that a simple MEGA65 program—one that doesn&rsquo;t use memory mapping or disk loading tricks—has to fit in 40,958 bytes, that&rsquo;s quite a commitment of space. If I drop the scanf() call from this program and replace it with a hard-coded width, such as width = 5;, my PRG size drops to 4,936 bytes. Calypsi sees that I don&rsquo;t need any of the scanf() support code and leaves it out. Here is an example of reading data from a file, using the standard library: #include #include // Preserve zero page to make it possible to return to BASIC #pragma require __preserve_zp #define FILE_NAME "EXAMPLE.DAT" #define LOAD_ADDR 0x7000 #define MAX_LENGTH 1024 void main() { FILE *fp; fp = fopen(FILE_NAME, "rb"); if (fp == NULL) { printf("CAN'T OPEN %s\n", FILE_NAME); return; } int ch; char *load_addr = (char *)LOAD_ADDR; while ((ch = fgetc(fp)) != EOF && load_addr Given a SEQ file on the currently mounted disk in drive unit 8 named EXAMPLE.DAT, this reads that file (up to a maximum of 1,024 bytes) and stores it at address $7000. C primitive value types The C standard describes its fundamental data types in terms of minimum bit sizes. Especially on microcomputers and embedded systems where memory is at a premium, it&rsquo;s important to know exactly how much memory the compiler uses for each C type. The three integer value types are char, int, and long. You&rsquo;ll be using these most often. char is an 8-bit (one byte) value. It can be signed or unsigned. If the type just says char, it can store either signed or unsigned values. Signed values are stored as Two&rsquo;s complement, the most popular method that assigns exactly one value to each of the 256 possible representations. int is a 16-bit (two byte) value. It can also be signed or unsigned. On the MEGA65, multi-byte integers are stored Little Endian. This means you can use these types to write to multi-byte registers without issue. long is a 32-bit (four byte) value. There&rsquo;s also a long long type that is a 64-bit (eight byte) value. Obnoxiously, the integer sizes are merely minimums in the standard. A program using an int might get 16 bits on one platform and 32 bits on another. Calypsi implements the C99 standard, which includes the stdint.h standard library. This provides a set of type specifiers for integers that lock in their sizes. I always use these. It just overloads my brain to think the compiler might be giving something I don&rsquo;t expect. #include uint8_t get_border_color() { return *((uint8_t *)0xd020); } Calypsi supports the C language floating point value types float and double, which can store small fractional values or very large values with limited precision. As with other Commodores, the MEGA65 does not have a math co-processor, so all floating point math is implemented by Calypsi as software, similar to BASIC 65. This means floating point math can be disappointingly slow. Unless you really need it, you might prefer to use the integer types and implement your own fixed-point math. For example, it is highly recommended in computing to represent amounts of money as an integer number of cents (1599) instead of a fractional number of dollars (15.99), because you don&rsquo;t want floating point imprecision to accidentally round amounts of money up or down in a transaction. The C99 standard provides a Boolean (true/false) value type in the stdbool.h standard library. This library adds bool as a type and true and false as value macros. #include void main() { int value; bool isNegative; value = 300 - 750; isNegative = value Accessing hardware registers Playing with the C standard library is fun, but it won&rsquo;t be long before you&rsquo;ll want to access MEGA65 hardware registers from your C programs. This is easy enough with C pointers. Here&rsquo;s a simple example of assigning the address of the VIC border color register to a pointer variable, then using it to change the border color. void main() { volatile char *border = (char *)0xd020; // Change the border color to green. *border = 5; // Pause indefinitely. while(1) {} } You can use scarier-looking C syntax to avoid the variable: *((volatile char *)0xd020) = 5; Notice that this short program doesn&rsquo;t need any #include statements or the #pragma that supports returning to BASIC. This compiles to a PRG file of 545 bytes. Of course, the equivalent assembly language program might only be a couple of dozen bytes. The compiler includes a bit of overhead for setting up general purpose C programs. You might be able to reduce that with changes to Calypsi&rsquo;s configuration, but you usually won&rsquo;t need to. We can also read from a register in a similar way. The following program cycles the border color as long as the Mega key is pressed. void main() { volatile char *border = (char *)0xd020; volatile char *modkey = (char *)0xd611; while(1) { (*border)++; while (!(*modkey & 0b00001000)) {} } } It&rsquo;s a best practice to use the volatile keyword for all pointers to registers. This tells Calypsi that the hardware at that address may not behave like regular memory, so the optimizer shouldn&rsquo;t assume that it does. For example, if a hardware register is connected to the joystick port, reading it multiple times in a row might return different values as the player pushes the joystick around. If Calypsi believes that the address is pointing to regular memory, it might see two attempts to read it without any attempt to write to it in between, and think &ldquo;Oh, that memory doesn&rsquo;t change, so I won&rsquo;t bother to read it twice.&rdquo; volatile tells Calypsi to preserve every attempt the code makes to read and write at that address. Examining the compiled machine code You can learn a lot about a compiler from the machine code that it generates. Even for very short programs, Calypsi generates enough preamble and postamble that it&rsquo;s difficult to figure out what&rsquo;s going on from running the PRG through a disassembler. Calypsi is more than happy to help. Give the compiler the --list-file argument, and it will create a text file for each C source file, explaining everything it figured out about how to compile the C code to assembly language. The starter project already has this argument in its Makefile. As written, the build creates .lst files in the obj/ directory, alongside the .o files. Let&rsquo;s consider the shortest program so far, changing the border color: int main() { *((volatile char *)0xd020) = 5; return 0; } Put this in main.c, then build (make). Open the obj/main.lst file that it creates in your text editor. For the sake of brevity, let&rsquo;s just look at the assembly language instructions themselves: 0001 int main() { \ 0000 .section code,text \ 0000 .public main \ 0000 main: 0002 *((volatile char *)0xd020) = 5; \ 0000 a905 lda #5 \ 0002 8d20d0 sta 0xd020 0003 return 0; \ 0005 a900 lda #0 \ 0007 85.. sta zp:_Zp \ 0009 85.. sta zp:_Zp+1 0004 } \ 000b 60 rts Nice! This is pretty much what we hoped to see: a concise expression of the code in equivalent assembly language. It uses 0x instead of $ as the prefix for the hexadecimal address d020, but otherwise the code is familiar. The code for return 0; is instructive. It looks like main() stores its return value in a zero page variable before returning to the outermost support code. Earlier we put the border address in a variable to make the code easier to read. Does this change what machine code gets generated? Let&rsquo;s try it: int main () { volatile char *border = (volatile char *)0xd020; *border = 5; return 0; } It looks like yes, Calypsi cautiously decides to store the address in a zero page location, then uses indirect addressing. 0001 int main () { \ 0000 .section code,text \ 0000 .public main \ 0000 main: 0002 volatile char *border = (volatile char *)0xd020; \ 0000 a920 lda #32 \ 0002 85.. sta zp:_Zp \ 0004 a9d0 lda #208 \ 0006 85.. sta zp:_Zp+1 0003 *border = 5; \ 0008 a905 lda #5 \ 000a a000 ldy #0 \ 000c 91.. sta (_Zp),y 0004 return 0; \ 000e a900 lda #0 \ 0010 85.. sta zp:_Zp \ 0012 85.. sta zp:_Zp+1 0005 } \ 0014 60 rts Try tweaking the C source code and re-build the list file to get a sense of how Calypsi makes decisions. The linker can also take a --list-file argument, and the starter project includes this as well. In addition to the hello.prg file, it produced a file named hello-mega65.lst. This will come in handy when we take a closer look at how Calypsi manages memory, in a later Digest. mega65.h The MEGA65 I/O registers always have the same addresses and structures. It would be nice if we just had a pre-made list, so we can access all of these registers by name. Well, good news! Discord user wombat (Github user mlund) created such a library for the llvm-mos-sdk project, another C tool chain with MEGA65 support. Via the open source license, Calypsi includes this library as well. We can rewrite our border color example using the provided structures, like so: #include void main() { VICIV.bordercol = 5; } The mega65.h library provides structures for the VIC-II, VIC-III, and VIC-IV register layouts, as well as the SID chips, Hypervisor, the 6526 CIA chips, and the 45E100 ethernet controller. The library also includes bit masks for registers that only use certain bits of a byte register. There are no fancy abstractions on top of the registers. This is just a way to give the registers a convenient set of names for use in programs. The best documentation for the register structures is the header files themselves. Browse your Calypsi files to find them. On macOS, they are under this path: /usr/local/lib/calypsi-6502/contrib/MEGA65-SDK/include/ Reading these files requires a modest understanding of C structure definitions. The main thing to notice is that mega65.h provides top-level macros that refer to the register structures from their actual addresses, so the compiler doesn&rsquo;t bother with pointer variables and accesses memory directly. VICIV.bordercol = 5; is equivalent to: *((char *)0xd020) = 5; Structures like these also empower code editors to make these easy to find interactively, without having to look up or memorize addresses. Make sure your IDE knows about the .../include/ directory that contains mega65.h. For example, if you&rsquo;re using VS Code, add the file .vscode/settings.json to the workspace with this setting: { "C_Cpp.default.includePath": [ "/usr/local/lib/calypsi-6502/contrib/MEGA65-SDK/include/" ] } Now when you type VICIV. in the editor, it shows all of the named VIC-IV registers, with their value types. Some of these have inner structures, which you can also browse in the same way. mega65.h name completion in the VS Code IDE. Using multiple source files So far so good. But we won&rsquo;t get far piling all of our code into main.c. Let&rsquo;s look at a quick example of a project with multiple source files. If the entire program is in a single C source file, the compiler can figure out everything it needs to know to generate all of the program&rsquo;s machine code. Each function becomes a machine code subroutine, with additional code to handle argument passing and return values. Variables are reserved memory. The addresses for the subroutines and variable memory are determined by the linker when it calculates the final program. A C compiler only considers one source file at a time. This might feel a bit odd compared to modern languages, but modern languages run on modern machines with tons of memory and ultra-fast CPUs. C compilers were designed to run on mainframes and microcomputers with limited memory. By separating the compilation and linking phases, the intensive compilation process only needs to consider a small piece of the program at a time. To do this, the compiler needs help understanding when a function or variable being referenced in one C source file is actually provided by another C source file. It doesn&rsquo;t need the full definition to understand the reference, it just needs to know the name and the value types. You can provide these in a declaration. Try this. Create a C source file named numbers.c in the src/ directory with definitions for these functions: int twice(int x) { return x*2; } int thrice(int x) { return x*3; } With the starter project, we also need to tell the Makefile about the new module so it gets compiled. Edit Makefile and change the C_SRCS line, like so: C_SRCS = main.c numbers.c Next, write the following in src/main.c: #include void main() { int value = 11; int twice_result = twice(value); printf("TWICE: %d\n", twice_result); int thrice_result = thrice(value); printf("THRICE: %d\n", thrice_result); } Try building the project with the make command. When Calypsi compiles main.c, it prints two warnings: &ldquo;implicit declaration of function&rdquo; It doesn&rsquo;t know anything about the definitions of these functions in numbers.c when it is compiling main.c. By default, it assumes you know what you&rsquo;re doing, and that these functions will show up later in the linking process. No offense, but this is a generous assumption. It would be much easier for us if the compiler would report undeclared functions as errors. If we mistype a function name, it shouldn&rsquo;t assume that it&rsquo;s a real function. Moreover, these implicit declarations interfere with another of the compiler&rsquo;s important tasks: type checking. It happens to be the case that the twice() and thrice() are functions that accept an int-type value as an argument and return another int-type value. These &ldquo;implicit declarations&rdquo; assume as much, but these assumptions won&rsquo;t always be correct. We want the compiler to catch us if we accidentally try to provide an argument value of the wrong type, or use a return value as a wrong type. Older versions of C allow these error-prone implicit declarations. In the 1999 version of the C standard (C99), implicit declarations are specified as warnings. A standards-compliant C compiler is obligated to alert you when they happen, but the build still proceeds under the old assumptions. Warnings are bad, and if you&rsquo;re starting a new project, it&rsquo;s a good idea to take them seriously. The best way to take them seriously is to tell Calypsi to treat all warnings like errors and halt the build. To do this, provide the -Werror command-line argument to the compiler. I recommend adding this to your Makefile. Let&rsquo;s try declaring these functions in main.c. Add these lines to main.c above the definition of the main() function: int twice(int x); int thrice(int x); These declarations inform the compiler that if it ever sees code that uses the twice() or thrice() functions, it can safely assume that the definitions will be linked in later. And the compiler has the value type information it needs to check our work. If the code tries to use a function named &ldquo;qwice,&rdquo; that&rsquo;s probably a mistake. Try building again. The warnings are gone, and the program builds successfully. In practice, it&rsquo;s a pain to put declarations of every function at the top of every C file that uses them. C has a better way. This is what the #include statements we&rsquo;ve been using are for: they pull in all of a library&rsquo;s declarations into the compilation from a separate file. This type of file is called a header file for how it gets included at the top of each source file, and has a .h filename extension. Remove the declarations from main.c. Instead, create a file named src/numbers.h, and add the declarations there. While you&rsquo;re at it, use source comments to give these functions documentation on their use. /** Doubles the argument. **/ int twice(int x); /** Triples the argument. **/ int thrice(int x); Add this line to src/main.c just below the #include statement: #include "numbers.h" The #include statement uses quote marks ("numbers.h") instead of angle brackets () to indicate that the header file is among our project&rsquo;s source files. Why not just #include the numbers.c file and nevermind this declaration nonsense? The #include directive is doing something very simple: it is inserting the contents of one file into another. The C compiler only sees the result, which in this case would be as if we defined all of the numbers functions in every C source file where they are used. The compiler blindly generates code for each definition in each object file. When the linker tries to stitch all of the object files together, it finds multiple definitions for each of the functions, and doesn&rsquo;t know which ones to use. There can only be one definition for a function across all of the object files. Declarations give the compiler enough information to validate that externally defined functions are being used correctly. By providing all of a library&rsquo;s declarations in a header file, users of that library can #include the header file and use the library&rsquo;s functions safely. As a matter of convention, header files also provide human-readable documentation for how to use the library, without having to read the library&rsquo;s C source code. In summary: Each C source file .c contains definitions of functions and global variables. Each function and variable is defined exactly once in a project. Functions can reference other functions and variables defined in other C source files, as long as the declarations of those functions and variables are made available in the source file making the references. Declarations belong in a header .h file that accompanies the source .c file with the corresponding definitions, such as numbers.h for numbers.c. This allows the declarations to live in one easy-to-find place that can be #include-d in the source files that use them. Each source file .c is compiled to an object file .o. The object file contains the compiled machine code for the definitions in the source file, and unbound references to functions and variables it expects will be in some other object file. The linker connects all of the unbound references to their definitions to produce the final program. If it can&rsquo;t find a definition that matches a reference, it halts and reports an error. If it finds multiple definitions for the same reference, this is also an error. A simple demo of spreading code across modules in C. Learning C, revisited The C Programming Language, 2nd edition, by Brian Kernighan and Dennis M. Ritchie. Back in the July 2023 Digest, I recommended the two most popular books on learning C programming: The C Programming Language, 2nd edition (1988), co-written by Brian Kernighan and the C language&rsquo;s designer Dennis Ritchie, and C Programming: A Modern Approach, 2nd edition (2008), by K. N. King. As much as I love K&R, I prefer King. It&rsquo;s clear, thorough, and highly interactive, with practical examples, exercises, and advice for aspiring programmers. I&rsquo;ve read it cover to cover and have completed every exercise. It&rsquo;s great. Don&rsquo;t miss the official website for the book, including downloads for the example programs. We&rsquo;re living in an age of unreasonable textbook prices, and I wouldn&rsquo;t blame anyone for preferring free on-line resources. Thankfully, there are plenty. Harvard CS50 is a full course that introduces programming partly (but not exclusively) through the C language, with text and video. The C Book (1991) by Banahan, Brady, and Doran is a complete introduction, released freely online by the authors. Beej&rsquo;s Guide to C Programming has great technical coverage and plenty of examples, also free online. The comp.lang.c FAQ is a brain-tingling supplement to learning the language, though it&rsquo;s not a starting point. There are also excellent references for the language and standard library, worthy of a professional investment in the language (Harbison/Steele, Plauger, Hanson). If your interest is in programming for the MEGA65, I would hold off on these. Only a subset of the standard library is available, and anyone writing a game or large application for the MEGA65 is likely going to ignore the standard library and prefer custom libraries and direct hardware access, to best take advantage of the computer&rsquo;s features—and to fit programs into the MEGA65&rsquo;s limited memory resources. OK! That&rsquo;s quite a bit for a &ldquo;part 1.&rdquo; With not much more than a few files, we know enough to use the MEGA65 as a platform for learning the C language from common textbooks, and to get started controlling MEGA65 features from C programs. We&rsquo;re currently limited to the first 64 KB of the MEGA65&rsquo;s memory, both for storage space for the compiled program and for allocating memory for data and variables. To grow beyond this, we&rsquo;ll need to learn more about how to steer the Calypsi tool chain, how Calypsi manipulates the MEGA65 behind the scenes, and how to mix C and assembly language. In the next Digest, we&rsquo;ll dive deeper into Calypsi&rsquo;s feature set and see how to do common useful things on the MEGA65. This Digest is made possible by readers like you! Thank you to everyone who has supported the Digest with one-time and recurring donations. If you&rsquo;d like to see more MEGA65 articles like these, consider becoming a supporter. Visit: ko-fi.com/dddaaannn C you later!—nope, nope, not doing that. Uh, just have a good month! — Dan
56 MIN
APR 23, 2026
Pixie Power
Pixie Power. Dan&rsquo;s MEGA65 Digest for April 2026. (Audio) Pixie Power. Two soldiers from Tactical Strike. The MEGA65&rsquo;s Raster Rewrite Buffer allows programs to draw characters on top of other characters, with the option to treat certain pixels as transparent. This enables all manner of graphical effects and techniques involving layers. In the previous Digest, I was able to use the RRB to sketch the user interface of a military strategy game, with terrain, soldiers, vehicles, indicators, and user interface elements in a 20-by-12 grid of 16-color 16-by-16 tiles, all in overlapping layers. Each tile is drawn separately, so it&rsquo;s easy to imagine that this game could move the game objects across the terrain, like moving pieces across the spaces of a board game. In this Digest, we&rsquo;ll look at more features of the Raster Rewrite Buffer that open up even more possibilities. Specifically, we&rsquo;ll see how to position layered game objects not just in a tile grid, but in arbitrary pixel positions on the screen. This is similar to VIC-II hardware sprites, but managed entirely by the program manipulating the RRB. Graphics programmers sometimes call techniques like these &ldquo;soft sprites&rdquo; to distinguish them from sprite features built into the video chip. Years ago, the MEGA65 community proposed that we call them pixies. The power of pixies, coming up after these project announcements! MEGA65 News Dark Reaper Dark Reaper, by MirageBD. Dark Reaper is a new game by long-time MEGA65 contributor Lars Verhoeff (MirageBD), with an impressive smoothly-animated 3D maze engine, textures and lighting that really show off the MEGA65&rsquo;s dynamic range, and atmospheric music and sound. Collect keys, open doors, find secret passages, and above all: avoid the Reaper. The map is essential to finding your way around and not getting caught. DOS/65 DOS/65, MEGA65 port by Stefan Eilers. The original DOS/65 was a CP/M workalike for the 6502 processor, by Rich Leary in 1982. Stefan Eilers has ported it to the MEGA65. You can download it from Filehost. The filesystem is compatible with CP/M version 1.4 and later, though of course a 6502 computer cannot run 8080/Z80 programs. Don&rsquo;t miss Stefan&rsquo;s documentation, and the Github repo. MegaPC MegaPC, an 8086 emulator by xlar54. Extending his emulator series, xlar54 has published an 8086 PC emulator capable of running MS-DOS, with a monochrome display and two virtual floppy drives. You&rsquo;ll need a DOS boot disk. Popular DOS apps work, though not always at full speed. See the Github repo for more information. More Featured Files MegaSweeper65, by Hanyic Róbert. MegaSweeper65 by Hanyic Róbert, a new faithful recreation of Minesweeper, enhanced with MEGA65 graphics and sound. Use a mouse in port 2 to place flags and guess the locations of the mines. Download the playable demo on Filehost, then proceed to the itch.io page to purchase the full version for just $6 USD. Snac-Snake, a featureful snake game by ToneDeF, written in Eleven. Overlord by Drex has received a major update. Version 2.0 features multiple overlords, an overhead maze view, and more improvements. FullScreenScrool, by Hanyic Róbert, a demo of a full-screen scrolling playfield, pushing out the screen borders for more screen real estate. Control the scroll with a joystick in port 2. MegaCalc65 by BOBELE, an experiment in AI vibe coding in BASIC65. The result is a small VisiCalc spreadsheet clone, though BOBELE says this version&rsquo;s BASIC source is not very readable. piso&rsquo;s C16 / plus/4 core has had a major update and is now feature complete. Space Invaders core by AwesomeDolphin, an FPGA core based on the original Space Invaders arcade machine. You&rsquo;ll need to get the Space Invaders ROM files from the MAME ROM set. Blade Runner demo, a slideshow with music, by Drex. Features a slide-in effect for each image. Tactical Strike graphics demo DISPLAY5 layer stress test demo. I have been illustrating the most recent Digest articles on RRB graphics with a sketch of a game I&rsquo;ve been calling &ldquo;Tactical Strike.&rdquo; Several people have asked to see these demos in their full form, beyond the excerpts I&rsquo;ve been including in the newsletter. You&rsquo;ll have to forgive the crudeness of this model, I haven&rsquo;t had time to paint it or to build it to scale. But if you&rsquo;re interested: Download tactical.d81 d3-mega65.asm source d3-go64.asm source The tactical.d81 disk image includes several display demos written in BASIC 65, along with the Tactical Strike tileset (NCM characters and palette) as data files. Each demo sets up the 20 x 12 tile display in the same way, as described in the previous article. DISPLAY: Shows one tile at a time in the top left corner. Press A and D to cycle through them; press space to exit. DISPLAY2: Displays the entire tileset. Press space to exit. DISPLAY3: Displays the entire tileset, using a general purpose subroutine that can draw a given tile at a given grid position. (Run/Stop + Restore to exit. No particular reason, I think I just wanted the program to be shorter so I left out the reset code.) DISPLAY4: Simple GOTOX demo, draws a red soldier on top of flowers. DISPLAY5: Stress test of layer drawing. Press any key to draw another layer of random tiles. Adjust the LC = 11 in line 10 to watch it fall apart at different points on real hardware and in Xemu. The layer count is in the last row. Also there&rsquo;s a fix for the glitch in the lower right corner in other demos. DISPLAY6: A gameplay sketch, based on the Kenney Tiny Battle demo scene, using the full-layer technique. The little green corner in the piece of road over the water is just me being lazy: I put the road and water tiles in the same layer, but I could have added one more layer for the road to be properly above the water. DS-MEGA65 is a version of DISPLAY3 written in assembly language. None of these demos strictly depend on features of BASIC65—it&rsquo;s all register and memory manipulation—though I do use commands like WPOKE and SETBIT to illustrate the register concepts. This assembly language version also simplifies the code by embedding the tileset data directly in the program. Instead of loading the tileset into bank 4 from disk, I just tell the assembler to include it with the program at address $2800 in bank 0, and refer to those addresses in screen memory directly. Someone asked me if a program running in GO64 mode could use these features. It sure can! You just need to switch the VIC from VIC-II compatibility mode into VIC-IV mode, like so: ; Switch to VIC-IV mode. lda #$47 sta $d02f lda #$53 sta $d02f The only other difference between d3-mega65.asm and d3-go64.asm is the start address, and the corresponding address in the BASIC bootstrap preamble. The rest of the code is identical. To try the GO64 version, just load and run it like any other program in GO64 mode. MOUNT "TACTICAL.D81" GO64 YES LOAD "D3-GO64",8 RUN About face! So far, I&rsquo;ve been drawing the tiles as they appear in the tileset. In the case of soldiers and vehicles, everyone is facing to the right. That&rsquo;s not too bad given the board-game-like style of the display, but in the real game each team is going to start on opposite ends of the field and advance toward each other. Without changes, a soldier marching right to left will look like it is walking backwards towards the enemy team. Of course, one possible solution is to have two tiles for each soldier, one facing to the left and one facing to the right. But the VIC-IV can help us out here. In the common case where we just want the same image flipped horizontally, we can just ask the VIC-IV to use the same graphic, and draw it flipped. To draw any SEAM character flipped horizontally, set bit 6 of byte 0 of its color value. Bits to flip a SEAM character horizontally or vertically. Tactical Strike tiles are one NCM character across (16 pixels) and two characters tall (8 x 2 = 16 pixels). To flip the tile horizontally, Tactical Strike sets bit 6 of byte 0 of the color value for both characters. A soldier tile, flipped horizontally. Unsurprisingly, the VIC-IV can also flip a SEAM character vertically. To do so, set bit 7 of byte 0 of its color value. If Tactical Strike had a use for flipping a tile vertically—and perhaps it doesn&rsquo;t given the style of the game, but let&rsquo;s just consider—notice that a tile is two NCM characters stacked vertically. To flip the full tile vertically, it would need to set bit 7 of byte 0 of the color value of both characters, and it would have to swap the characters themselves, so the bottom character is on top and vice versa. A soldier tile, flipped vertically. Here&rsquo;s a trivial example of drawing a left-facing red soldier at a pre-calculated screen location: POKE $50004,$6E POKE $50005,$11 POKE $5002C,$6F POKE $5002D,$11 POKE $FF80004,$48 POKE $FF80005,$C0 POKE $FF8002C,$48 POKE $FF8002D,$C0 Here&rsquo;s a brief review of where these numbers come from: In this example, screen memory is at $5.0000 (established by setting the SCRNPTR register), and color memory starts at $FF8.0000. In this example, the red soldier is the third character on each of the first and second rows. LINESTEP is 40 = $28. SEAM characters take two bytes each, so the memory offsets for the two characters are $04 for the top character and $2C for the bottom character. The character set addresses for the red soldier are $4.5B80 and $4.5BC0, so the corresponding screen codes are those addresses divided by $40: $116E and $116F, respectively. Color memory declares that these are NCM characters, are flipped horizontally, use sub-palette $C, and use color $0 of that sub-palette for pixel value $F, like so: Color memory byte 0: $48 = %0100 1000 ^ ^ = is NCM = flip horizontally Color memory byte 1: $C0 = %1100 0000 ^^^^ = $F color for this NCM character ^^^^ = sub-palette for this NCM character See DISPLAY7 on the disk. What changes would you make to flip the soldier upside down? Forward march! Tactical Strike uses the Raster Rewrite Buffer to draw game objects onto the terrain. To do this, it populates the beginning of a row of screen memory with terrain characters, then uses a GOTOX instruction to reset the draw position to the game object&rsquo;s X position. This instruction is followed by the character for the game object itself, which is drawn on top of the terrain at the new X position. I wrote my tile drawing routine to keep all tiles arranged in a 20 x 12 grid. For example, to draw a red soldier in the fifth column on a row, the routine could use a GOTOX instruction to set the draw position at X = (5 - 1) * 16 = 48, the pixel coordinate of the left-hand edge of the fifth column. When the player moves the soldier, I want to animate the soldier marching across the field. I could just have him clomp across the grid locations, leaping 16 pixels at a time. That has a nice retro feel to it, but it&rsquo;s not the only option. GOTOX takes a pixel coordinate, not a grid coordinate, so I could slide the soldier left or right by changing the GOTOX value a small number of pixels per animation frame. For example, I could slide a soldier from one tile column to the next by one pixel per frame over 16 frames. In NTSC video mode at 60 frames per second, that would take 16/60 = 0.26 seconds. In PAL video mode at 50 frames per second, that&rsquo;s 16/50 = 0.32 seconds. Patrolling soldier, off the grid. See DISPLAY8, which animates a patrolling soldier, including horizontal flip. Last month, we discussed several different strategies for using GOTOX to draw layers, including drawing full layers with &ldquo;GOTOX = 0&rdquo; instructions between them. Notice that to animate individual game objects, each object needs its own GOTOX instruction: each row starts with a full screen-width of terrain, followed by a &ldquo;draw list&rdquo; of graphics objects, each object using one GOTOX instruction to set its position followed by the graphics character (or characters) for the object. This provides enough flexibility to locate each object independently from other objects on the row, at the expense of some screen memory. This is the general idea behind pixies. The program draws the background layer (or layers) into screen and color memory, one row at a time, followed by GOTOX instructions and characters for each pixie that appears on the row. Pixies can overlap, and must appear in screen memory in their desired order, from back to front. The VIC draws the entire row—background and pixies—by reading each row of screen memory from left (lowest memory address) to right (highest memory address), following all of the &ldquo;draw a character&rdquo; and &ldquo;GOTOX&rdquo; instructions in the order they appear. Up and down Positioning pixies at arbitrary horizontal positions is pretty straightforward: just give the GOTOX instruction the desired pixel coordinate to start drawing the next character. The VIC draws the full width of the character from the left edge to the right edge, potentially drawing it across what we normally think of as two character columns—or tile grid columns in the case of Tactical Strike. Red soldier drawn across two NCM character positions horizontally. You can also use the Raster Rewrite Buffer to position pixies at arbitrary vertical locations. This takes a bit more effort. We want something similar to horizontal positioning, where a character is drawn across two character rows, with some of the character on one row and the rest of the character on the next row. But the VIC draws one row at a time, and can&rsquo;t leave the current row to go draw somewhere else on the screen. It needs our help to understand how much of a given character to draw on a given row, and where to start drawing it. The mechanism for this is unusual, but it&rsquo;s designed in a way that&rsquo;s useful for common graphical effects. Red soldier drawn across three character positions vertically. Let&rsquo;s consider an example in detail. Say we want to draw the red soldier such that its upper-left corner is at pixel coordinate X=0, Y=3. The soldier tile is 16 pixels tall, using two 16-by-8 NCM characters in the character set. To appear at vertical position Y=3, these two characters have to be drawn across three character rows: The first row has three empty pixel rows followed by the top five pixel rows of the top character of the soldier. The second row has the bottom three pixel rows of the top character, and the top five pixel rows of the bottom character. The third row has the bottom three pixel rows of the bottom character, and five empty pixel rows. Red soldier at Y=3 across three character rows, desired outcome. The Raster Rewrite Buffer provides two separate features to make this possible. The character data Y offset Normally, when the VIC draws a character on a character row, it reads the first pixel row of that character&rsquo;s pixel data and draws it on the first vertical pixel position of the character row, then the second for the second, and so on. For FCM/NCM characters, it gets the address of the first pixel row of the character by taking the screen code and multiplying by $40, and reads eight bytes of character set data for each pixel row. You can tell the VIC to start reading character data on a different pixel row by setting a character data Y offset. An offset of +1 would tell the VIC to start reading the character data one row later: use the second pixel row of the character data for the first vertical pixel position, then use the third row of the character in the second position, and so on. This effectively shifts the character up by one pixel, cutting off the top. When the VIC gets to the eighth position, the +1 offset tells it to read the first pixel row of the next character in the character set. One way to think about this is to imagine the entire character set stacked vertically. When the VIC draws an FCM/NCM character, it uses the screen code times $40, plus the character data Y offset times 8, as the address of the first pixel row. You can use the screen code and the offset to position an 8-pixel-high window anywhere in the character set stack, and the contents of this little window are drawn into the character row. The offset can be in the range -7 to +7. Red soldier and neighboring characters, drawn with Y offset of -3. To draw the red soldier at vertical position Y=3, we use a character data Y offset of -3 for all three rows. Each character row works as follows: In row one, we use the screen code of the top character of the red soldier. The VIC draws three rows of the previous character, followed by five rows of the red soldier&rsquo;s top character. In row two, we use the screen code of the bottom character of the red soldier. The VIC draws the remaining three rows of the top character, followed by the top five rows of the bottom character. In row three, we use the screen code of the next character in the character set. Combined with the offset of -3, this uses the bottom three rows of the bottom character for the red soldier, followed by five rows of the next character. Red soldier at Y=3 using Y offset. Tada! The red soldier now appears at pixel coordinate Y=3! That&rsquo;s what we wanted, right? Row masking OK, rows 1 and 3 don&rsquo;t look quite right yet. They&rsquo;re stealing pixel rows from adjacent characters in the character set to fill out the set of eight. We need another feature to tell the VIC to not draw anything on those pixel rows. You can tell the VIC to skip drawing specific pixel rows by setting a row mask. This mask can enable or disable any of the eight pixel rows being drawn to the current character row. Combined with the character data Y offset, this completes our solution for drawing pixies at arbitrary vertical positions, like so: In row one, we use a row mask that hides the first three pixel rows, and shows the next five. This hides the three rows from the unwanted previous character, and only draws the top five pixels of the red soldier. In row two, we enable all eight pixel rows to be drawn: three bottom pixels of the soldier&rsquo;s top character, five top pixels of the soldier&rsquo;s bottom character. In row three, we use a row mask to show the first three pixels, and hide the next five. Only the last three pixels of the soldier&rsquo;s bottom character are drawn. In other words, all masked rows appear as transparent, and we can omit the neighboring pixels from the result of offsetting the character set data. Red soldier at Y=3 using Y offset, with row mask. Implementing Y offsets and row masks The character data Y offset and the row mask are part of the GOTOX instruction. This has two implications: they are set independently of the characters, and they apply to all subsequent characters on the character row, until they are changed by another GOTOX instruction, or they are reset at the end of the row. You set the Y offset using bits 4 through 7 of the GOTOX instruction&rsquo;s screen memory byte 1. Bit 4 is the sign bit: set it for a negative offset, or clear it for a non-negative offset. Bits 5-7 are the value 0 through 7. GOTOX bits for Y offset. To enable the row mask, set the GOTOX instruction&rsquo;s bit 3 of color memory byte 0. Then set the row mask using all eight bits of color memory byte 1. Each bit of the row mask corresponds to a pixel row, where bit 0 is the top-most row, bit 1 is the second row, and so on. Set the row mask bit to show the row. GOTOX bits for row mask. Here&rsquo;s the complete picture for drawing the red soldier at pixel coordinates X=0, Y=3. In this example, each row has 40 bytes for the background, 2 bytes for a GOTOX instruction, and another 42 bytes for whatever else is needed on the row. The o variable is the screen memory offset for that row&rsquo;s GOTOX instruction. The soldier characters follow the GOTOX instructions on each row. REM ROW 0: O=0*42*2+40 WPOKE $50000+O, $7000 : REM GOTOX SCREEN MEMORY WPOKE $FF80000+O, $F898 : REM GOTOX COLOR MEMORY WPOKE $50000+O+2, $116E : REM CHAR SCREEN MEMORY WPOKE $FF80000+O+2, $C008 : REM CHAR COLOR MEMORY REM ROW 1: O=1*42*2+40 WPOKE $50000+O, $7000 : REM GOTOX SCREEN MEMORY WPOKE $FF80000+O, $0090 : REM GOTOX COLOR MEMORY WPOKE $50000+O+2, $116F : REM CHAR SCREEN MEMORY WPOKE $FF80000+O+2, $C008 : REM CHAR COLOR MEMORY REM ROW 2: O=2*42*2+40 WPOKE $50000+O, $7000 : REM GOTOX SCREEN MEMORY WPOKE $FF80000+O, $0798 : REM GOTOX COLOR MEMORY WPOKE $50000+O+2, $1170 : REM CHAR SCREEN MEMORY WPOKE $FF80000+O+2, $C008 : REM CHAR COLOR MEMORY Just to be sure, let&rsquo;s walk through row 0: GOTOX screen memory low byte = $00 = %0000 0000: X is 0 GOTOX screen memory high byte = $70 = %0111 0000: Y offset is -3 GOTOX color memory low byte = $98 = %1001 1000: transparent, GOTOX, rowmask enabled GOTOX color memory high byte = $f8 = %1111 1000: hide rows 0-2, show rows 3-7 Character screen memory bytes = $6e $11: $116e x $40 is char set address $4.5B80 (red soldier top) Character color memory low byte = $08 = %0000 1000: This is an NCM character Character color memory high byte = $c0 = %1100 0000: NCM sub-palette $C; $F value is color $0 Try working out the other rows. See DISPLAY9 on the disk. Try enabling and disabling the row mask to see the difference. Note that Xemu cuts off the top row in this example; see notes near the end of the last Digest. Giant pixies The Y offset feature worked out nicely for Tactical Strike. Each 16-by-16 tile is two NCM characters arranged vertically, and the two characters for each tile appear next to each other in the character set. When setting the -3 offset for row 2 in our example, we got exactly what we wanted: three rows from the top character and five rows from the bottom character. This suggests a general rule for organizing character sets for large pixies: top to bottom, left to right. This makes it easier to use unmasked offset characters for the interior rows, taking advantage of the VIC&rsquo;s ability to read rows from adjacent characters, as part of pixie vertical positioning. Don't laugh at my airship. Building a pixie framework It may not look like it yet, but we now know enough VIC-IV graphics features to implement Amiga-quality games! Super Extended Attribute Mode gives us full-color characters with a 256-color palette of 23-bit color. The Raster Rewrite Buffer lets us draw character graphics in layers with transparency, at arbitrary pixel locations. We can use these techniques to implement &ldquo;pixies,&rdquo; and draw a large number of them to the screen, on top of detailed backgrounds. A given object can be multiple characters wide and tall, and we can include multiple frames of animation in our character set and switch between them just by changing screen codes. Knowing how it works and getting it to work are two different things. While we now know which VIC-IV features can be used to implement pixies with GOTOX instructions across multiple rows of characters, this isn&rsquo;t how we want to think about pixies in our game code. I just want to be able to say that a game object uses a particular frame of animation from my tileset, and appears at a given set of pixel coordinates. It&rsquo;d be best if some general purpose library could manage the screen and color memory, character codes and GOTOX instructions, Y offsets and row masks. RetroCogs, the implementor of the MEGA65 port of RogueCraft DX, is developing GameShell65, an assembly language framework with abstractions for pixies and other graphics techniques based on the RRB. It&rsquo;s in early stages, but there&rsquo;s plenty of code to learn from and re-use. Even if you don&rsquo;t plan to use it, check out the pixie documentation for hints on how to implement a workable pixie system. Several of the demos on the TACTICAL.D81 disk use a BASIC subroutine to do the heavy lifting for drawing tiles in layers. It might be possible to encapsulate pixie positioning logic in that routine as well, depending on what kind of animations and movement I want the game to have. Whether it&rsquo;s fast enough to do in BASIC, or if assembly language is needed, also depends on the game. Of course, there&rsquo;s still more to discuss about character graphics. There are several character rendering features we still haven&rsquo;t yet covered. Fast-paced games typically use Direct Memory Access features to manage screen and color memory. And there&rsquo;s more to say about full-screen scrolling and other special effects. These will have to wait for future Digests! If you want to see more articles like these, consider supporting the Digest. Visit ko-fi.com/dddaaannn for more info. See you next month! — Dan
25 MIN