Making a Better Simulated Evolution Port for the Apple IIgs

07:30 PM · Oct 17, 2026

Last week’s work was really about the new Apple IIgs target for vbcc and less about the particular things I was building with it. That said, it did result in a functioning Simulated Evolution port for the IIgs.

I can’t say I was too thrilled with the port, though. Even without the one-update-per-frame throttling, the simulator was noticeably slow, lagging out quite severely at even 20 or so bugs. Even the 8-bit 6502 editions did better than that. On the plus side, while the Atari ST and DOS ports took “three lazy evenings,” this IIgs port took only one lazy evening, so that’s a win for productivity.

All the same, while I think it did a great job of putting vbcc through its paces, I’m not happy enough with it to publish it as a full port in its own right. It would need some extensive reworking, and that’s a bit of a bummer because one of the specific features of the Simulated Evolution project has been showcasing just how many platforms the same source code could ultimately support. It stings a little to give that up.

But we don’t have to give that up—there are two shared source bases. Those C64 and Atari ports both share a simulation core in 6502 assembly language, and the IIgs’ 65816 should execute that just fine as long as we’re careful about our register width configuration. We should be able to work with this. I arranged my initial TODO list like so:

  • Write a shell for the Apple IIgs based on the C64 one that uses an identical simulation core but displays results in Super High-Resolution mode. We pick C64 here because its 160×200 multicolor display maps more closely to our graphics mode here than the Atari’s scrolling display list. Furthermore, since the original simulation core was not relocatable, this port would have to be in the similarly fixed-layout ProDOS 8.
  • Shift the code as minimally as possible to work as a relocatable GS/OS program. ProDOS 8 suffers from extra memory bus contention and must also share the zero page with ProDOS itself; in GS/OS we should see less contention and we should also be able to put more of our actual state into the smaller and faster direct page.
  • The C port was my first-ever use of calls from the GS/OS Toolbox libraries, even indirectly. This is a good opportunity to learn how that works at the assembly language level and do it right.
  • Getting this far into the project will have required me to switch my assembler at some point from Ophis to Merlin. Merlin is an older-school assembler, with many frustrations of the sort that led me to write Ophis in the first place. This will be an opportunity for me to push against those limitations and maybe find a few ways around them.

The ProDOS 8 port ended up taking me one high-intensity evening. I’ll write through that this week, and then next week we’ll dig into GS/OS and some of the stranger corners Merlin will let us reach.

As it turned out, I had to abandon Ophis immediately. Even though ProDOS 8 keeps itself in bank 0 and in 8-bit mode all the time internally, actually doing anything fun with the IIgs’ hardware requires accessing bank $E1 and generally using 65816 instructions and addressing modes. That meant working with Merlin, and that meant porting the simulation code over to Merlin, and that meant abandoning “shared source across ports” for this immediately. I could, at least, keep the object code equivalent; in addition to converting the simevocore.s file to Merlin, I also converted the C64 shell and its bitmap library. I could then confirm that my Merlin port produced bit-exact results to the Ophis original. With that in place, I could proceed with the ProDOS 8 application layer.

Serendipity

One of the “lucky breaks” I got when working on the C64 and Atari 800 versions was that the simulation core needed two bytes of zero-page storage, and that any bytes acceptable to the C64 were also acceptable to the Atari. It turns out that the bytes I chose ($FB and $FC) are also available on Apple IIs of any model running ProDOS 8! That’s a bullet dodged, but honestly it’s also a flaw in the core—memory layout for any given platform should really be entirely under control of the platform-specific shell layer.

There’s a funny other coincidence as well involving the Apple and C64 code: it turns out that the data kept off the zero page doesn’t need to be moved either. The C64 kept its data in the $1200-$1FFF range, with a hard, assembler-enforced barrier at $2000. This placed the data firmly between the program itself (loaded at $0800) and the bitmap display (where $2000 was the only reasonable place to put it). ProDOS 8, it turns out, also has a hard barrier at $2000: that’s where the program itself loads! $0400-$1FFF is free for program use, either as text screens or as program data. We’ve got plenty of room to do both.

ProDOS 8 also suffers from a very similar issue to the Atari 800 boot-disk problem: there isn’t a consistent external source of randomness to keep runs distinct. ProDOS actually has it worse—neither it nor GS/OS proper have any real system services at all, including even a simple frame count. (On the IIgs, that’s the Toolbox’s job, as we saw last week; on the 8-bit systems, there was no timer interrupt at all unless you installed peripheral hardware that imposed one.) The solution here is the same as on the Atari: provide a handsome title screen, wait for a keypress while running a counter, and use that counter to seed the RNG.

                REP   #$10           ; 16-bit indices
                LDX   #$0000
:KEYWAIT        INX                  ; Bump iteration counter
                BIT   $C000          ; Wait for key
                BPL   :KEYWAIT
                BIT   $C010
                STX   SCRATCH        ; Save iteration counter
                SEP   #$10           ; Back to 8-bit everything
                LDA   SCRATCH        ; Use iteration counter to seed PRNG
                LDX   SCRATCH+1
                JSR   SRND

No need to stay in 8-bit mode the whole time, after all, as long as ProDOS and the simulation core themselves are none the wiser. The title screen itself isn’t bad either:

Tribulations

Porting the source to Merlin, though, was rough. Ophis was very specifically designed to avoid a lot of choices that Merlin itself locked itself into back in the 1980s.

  • Merlin has two kinds of comments: instruction-level comments which are introduced by semicolons after an instruction, and full-line comments which are introduced by an asterisk on the first character of that line. I’ve developed a fairly consistent style over the years of three-semicolon whole-line comments at the front of files and functions, single-semicolon comments for individual instructions, and then two-semicolon comments to explain chunks of code mid-function, indented by the same amount as the instructions themselves. Merlin really doesn’t like this, and the formatting code in its support utility Cadius mangles it.
  • Merlin treats whitespace in general as field separators, and while it will grudgingly accept that LDA $2000, X is the same instruction as LDA $2000,X, it wants the latter and Cadius mangles the former, putting the X clear over with the comments. Worse, if you have spaces in a complicated mathematical expression, Cadius will not merely mangle your line, it will corrupt and destroy it.
  • Ophis offers nested namespaces, so labels could be various levels of “local” and still, for instance, be easily shared between functions. Merlin’s are strictly scoped to “between any two globally visible labels,” and this bit me in a few places where I was trying to keep some shared data tables out of the global namespace.
  • Merlin doesn’t really offer anonymous labels the way that Ophis, ca65, or vasm do. To the extent that it offers anything similar, they are “variable” labels that redefine themselves when redeclared, which in turn means that they only work for backwards jumps. That’s fine for loops but useless for if/then/else clauses… and it turns out that Simulated Evolution has quite a lot of if/then/else clauses in its logic.
  • Merlin’s support for for memory segments and RAM layout isn’t absent, but it’s pretty minimal. Its “dummy sections” can give you something approximating a proper BSS section, but multiple definitions are not automatically coalesced the way Ophis (or nearly any other assembler I’ve ever recommended) handles it. That’s simple enough to work around, here (part of making the C64 bit-exact means that we know precisely where all the data ends up and can just ask to put it there as needed), but it’s unpleasantly rigid and I will be fighting it later on in this project.
  • The ProDOS 8 environment mostly looks like an overclocked Apple IIe until you break out of the CPU’s 8-bit mode. That includes the routines supplied by the 8-bit compatibility firmware, like HOME and COUT. I used those to produce my initial title screen, and it took several crashes after that to realize that the monitor was re-entering 6502 emulation mode every time I output a character. I also struggled mightily with control of the 40- vs 80-column text modes; ProDOS 8 seemed to always start in 40-column mode for me (which is what I wanted), but it also acted as if the 80-column hardware had been disabled outright. I know that isn’t true generally—Applesoft BASIC is a ProDOS 8 program and it can swap modes with simple PRINT statements—but I couldn’t get any of it to work at all in my standalone programs. The defaults are fine anyway; I just gave up and let it stay there.

Smooth Sailing

The shell itself was pretty simple. Most of the C64 logic ported directly, with graphics routines being the big exception. Graphics on the IIgs are enormously simpler than they were on the C64 or Atari. Even the very simple C code from last week can be dramatically simplified once we’re working at the assembly language level. For a given (X, Y) coordinate in our 150×100 world, we needed to compute an offset into the screen array Y*320+X+5, then draw four pixels of color C by writing the value C*17 to screen[offset] and screen[offset+160]. I turned that into a series of shifts in the C code:

unsigned int i = (((y << 2) + y) << 6) + x + 5;

We can do better than this, because we can exploit the fact that we know that the simulation core is calling us in a pure 8-bit mode, and that it’s passing its X and Y coordinates in the index registers. When transitioning from 8-bit to 16-bit mode, the index registers are zero-extended to 16 bits. That means that after going into full 16-bit mode, we can execute .A = .Y * 256 by transferring Y to A with a TYA instruction, and then swapping the low-byte of A with its (now zero) high byte with XBA. We can then get the rest of the multiply-by-320 by shifting this right 2 spaces. The full address computation routine is not much more than this.

                MX    %00            ; This routine is pure 16-bit
SHR_ADDRESS     TYA                  ; Compute Y*320 + X, which equals...
                XBA                  ; ... Y * 256...
                STA   SCRATCH3
                LSR                  ; ... + Y * 64...
                LSR
                ADC   SCRATCH3
                STX   SCRATCH3       ; ... + X.
                ADC   SCRATCH3
                TAX
                RTS

One might notice that we never bother with CLC instructions here. That’s because we don’t need them; our shifts and additions guarantee a cleared carry bit assuming the initial inputs were 8-bit. One might also notice that this code doesn’t add the extra +5 at the end. That’s because it doesn’t need to; it, as well as the C*17 multiplication, are handled simply by picking better constants in the support routines like DRAW_BACTERIUM:

DRAW_BACTERIUM  PHA                  ; Preserve all registers
                PHX
                REP   #$30           ; 16-bit mode for SHR_ADDRESS
                JSR   SHR_ADDRESS
                SEP   #$20           ; Back to 8-bit memory
                LDA   #$22           ; Draw green
                STAL  $E12005,X      ; in two adjacent rows
                STAL  $E120A5,X
                SEP   #$10           ; Back to 8-bit everything
                PLX                  ; Restore registers we trashed
                PLA
                RTS

The bug-drawing routines can make use of similar simplifications; instead of drawing pixels in nested loops of 0-2, it can compute the address once, increment it to move one step right, and then add 320 to move one step down. (Really, we add 318 to move one step down and two left at once, to be ready for the next row of pixels with a single operation). Chunky pixels in linear memory are absurdly luxurious after the backflips required on simpler machines.

Results

The screen, overall, is basically identical to what we built last week. The speed, however, is a lot more sensible, even at 2.8MHz; animation looks smooth even up to 80 bugs or so, even though it’s easy to tell that the system is not running at a full 60 steps per second. This is better than I expected. I had recalled, in the past, that there was memory contention on the IIgs, and working in the I/O or Apple IIe-like spaces would throttle the CPU down to 1MHz for peripheral timing reasons. It seems that this effect is more tightly bound than I thought; GSPlus’s performance stats give an “effective MHz” in the status bar, and that was clocking in at a very comfortable 2.55 MHz. This is entirely good enough, as a port.

I don’t think I’m done with this yet, though. Digging through the work that vbcc and vlink were doing last week showed me a lot of places in the IIgs operating system that I entirely neglected in my first pass through. Next time, we’ll take a look at that.