Close

Act 5: Is That My Bug?

A project log for ProFile Emulator

FPGA-based ProFile drive emulator for the Apple ][, ///, and Lisa

jorj-bauerJorj Bauer • 09/28/2026 at 06:37•0 Comments

As mentioned in Act 4: I've got a bootable 32MB-capable system - except when I don't. I've found two published hard drive images that just won't boot at all: the Total Replay image, and Nox Archaist. Both absolutely worth the time to figure out why not.

The symptom is clear. Both images completely hang when you try to boot them, very unsatisfyingly like this - 


Without a distinct crash, this is harder than just disassembling what's happening. This means we need to take a look at the boot block for Total Replay and walk the whole boot cycle through the ROM. Given that these images are 32MB, it's possible that they're trying to load something from the end of the disk and failing - given my geometry hack and the $FF00 magic block rewrite override. Maybe THAT'S my bug! 

We can start with the debug output of the Teensy ProFile emulator, which dumps this more than a dozen times a second:

rd blk=$000000   block 0
rd blk=$00D848   "next directory block" = bytes +2/+3 of block 0 = 48 D8 (the loader's own TXA/PHA)
rd blk=$000002   block $D848's bytes +2/+3 are 02 00, so back to block 2
rd blk=$000003
rd blk=$000004
rd blk=$000005   null next pointer -> read block 0 "as a directory" -> repeat

So that hypothesis is dead; that's not my bug.  It's looping, not getting anywhere near the end of the disk, in a very sane boot pattern that just never completes. We're going to have to go deeper - down in the Total Replay boot block disassembly:

$0867  LDA #$1E
$0869  STA $45          ; driver buffer hi = $1E00
$086B  STA $3F          ; scan pointer hi = $1E
$086D  JSR READBLOCK    ; $08C3: ReadBlock via the card's ProDOS driver
$0870  LDA #$04
$0872  STA $3E          ; scan pointer = $1E04 (first dirent)
$0874  LDY #$00
$0876  LDA ($3E),Y      ; entry's storage/length byte      <-- walks the pointer
$0878  AND #$0F         ; name length
$087A  TAX
$087B  INX
$087C  CMP $09A4,Y      ; compare against "LAUNCHER.SYSTEM"
$087F  BEQ $0898
$0881  CLC
$0882  LDA $3E          ; next entry: pointer += $27 (39 bytes)
$0884  ADC #$27
$0886  STA $3E
$0888  BCC $088C
$088A  INC $3F
$088C  CMP #$FF         ; end of this block's entries?
$088E  BNE $0874
$0890  LDX $1E02        ; next directory block, ABSOLUTE from the buffer
$0893  LDY $1E03        ;   (this is why the hops work but the walk doesn't)
$0896  BCS $0867        ; read it, resetting only the high byte
$0898  INY
$0899  LDA ($3E),Y      ; match the name, character by character
$089B  DEX
$089C  BNE $087C
$089E  STX $FF
$08A0  LDY #$11
$08A2  LDA ($3E),Y      ; key pointer (index block) lo
$08A4  TAX
$08A5  INY
$08A6  LDA ($3E),Y      ; key pointer hi
$08A8  TAY
$08A9  JSR READBLOCK    ; read the file's index block

READBLOCK:
$08C3  STX $46          ; block number lo   (caller passes the block in X/Y)
$08C5  STY $47          ; block number hi
$08C7  LDA #$01
$08C9  STA $42          ; command 1 = READ
$08CB  LDA $45          ; save the buffer high byte...
$08CD  PHA
$08CE  JSR $D1D1        ; PLACEHOLDER: rewritten at boot to JSR $CnEA
$08D1  PLA
$08D2  STA $45          ; ...and restore it: the card's read loop advances $45 by 2
$08D4  RTS

What I see there is a ton of zero-page magic variable addresses, self-modifying code, and cleverness. 

The 6502 has distinct opcodes that make it faster, easier, and the code smaller when using the first $100 bytes of memory for variables ("zero-page addresses"). This memory space is, then, incredibly valuable: if you have a program running, which is using an operating system, which talks with drivers, all of them will need some set of it and - as with most hardware of this era - there's no real way to stake your claim that your special zero-page address is or is not already claimed. Handy charts like this existed in the 80s to help programmers figure out where to stash their variables, and what to avoid.

                        Lo Nibble of Address
Hi
Nib  0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
     --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
 0 | A~  A~  A   A   A   A   -   -   -   -   A   A   A   AI  A   A
 1 | A   A   A   A   A   A   A   A   A   A   A   A   A   A   -   M*
 2 | M   M   M   M   M3  M   MA3 MA3 M3  M3  M3  M3  MA3 MA3 M3  MA3
 3 | MA  M   MA  MA3 M   M3  M3B M3B M3B M3B M~  M~  MA3~MA3~MA3~MA3~
 4 | M3~ M3~ M3~ M3~ M3~ M3~ M3~ M3~ M3~ M3~ I3~ I3~ I3~ I3~ M~  M
 5 | MA  MA  MA  MA  MA  MAI AI  AI  AI  AI  AI  AI  AI  AI  AI  AI
 6 | AI  AI  AI  AI  AI  AI  AI  AI3 AI3 AI3 AI3 AI  AI  AI  AI  AI3
 7 | AI3 AI  AI  AI3 AI3 AI  AI3 AI  AI  AI  AI  AI  AI  AI  AI  AI
 8 | AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI
 9 | AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI
 A | AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI3
 B | AI3 AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI
 C | AI  AI  AI  AI  AI  AI  AI  AI  AI  AI  AI3 AI3 AI3 AI3 I   I
 D | AI  AI  AI  AI  AI  AI  AI3 I   AI3 AI3 AI  AI  AI  AI  AI3 AI
 E | A   A   A   A   A   A   A   A   A   A   A   -   -   -   -   -
 F | A   A   A   A   A   A   A   A   A   A   -   -   -   -  I   AI
 

M = Used by Monitor; * used in early Apple IIe ROMs-- now free
A = Used by Applesoft BASIC
I = Used by Integer BASIC
3 = Used by DOS 3.3
~ = Used by ProDOS ($40-$4E is saved before and restored after use)
B = Used by ProDOS BASIC.SYSTEM (also uses all Applesoft locations)
- = Free; not used

--Bryan Dunphy, Michael J. Mahon, Rubywand 

Yep, you read that correctly - taking in to account all of DOS, BASIC, ProDOS, and the Monitor there are only 14 bytes of zero-page RAM available that are conflict-free. Who knows what is or isn't used by individual device drivers, ROMs, or shim utilities. If your program has any substantial footprint, you're almost certainly going to have to accept that you conflict in some way - hopefully in a way that you don't care about. Like "I'm using ProDOS with AppleSoft BASIC, so the DOS 3.3  and Integer BASIC locations aren't important." Or, just as likely, "I'll save these variables when we start, and then restore them when we exit, so the operating system still works correctly."

All of those boot block zero-page accesses are important here - it could be that any one of them is in conflict with the ProFile ROM. Is THAT my bug? There's no reason why the author of the Total Replay or Nox Archaist boot blocks would ever know about this - these titles are eating 32MB of space, while the ProFile ROM only ever drove 10MB of disk. Without our geometry decoupling and block access hack there's no way this card would ever have been able to run such an image, just due to space requirements.

The author here was aware that this was the case: you can see it in READBLOCK, where $45 is being saved and restored across calls to the hardware. I suspect that's for a SmartPort device - probably something that cropped up during testing of the initial boot block.

And in Nox Archaist, we see the same general pattern, just looking for a different filename:

$082C  LDA #$1E
$082E  STA $45
$0830  STA $3F
$0832  JSR $0888        ; ReadBlock
$0835  LDA #$04
$0837  STA $3E
$0839  LDY #$00
$083B  LDA ($3E),Y
  ...  (identical walk: ADC #$27, INC $3F, CMP #$FF, LDX $1E02 / LDY $1E03)
$085E  LDA ($3E),Y
$0867  LDA ($3E),Y
$086B  LDA ($3E),Y
$086E  JSR $0888

Let's see what we have going on in Apple's 1984 ROM for the ProFile card:

; the driver bracket and dispatcher
$C81C  PHP
$C81D  SEI
$C81E  JSR $C829
$C821  BCC $C826
$C823  PLP
$C824  SEC
$C825  RTS
$C826  PLP
$C827  CLC
$C828  RTS
$C829  STA $3F          ; <-- $3F = $C5
$C82B  LDA #$00
$C82D  STA $3E          ; <-- $3E = $00   ($3E/$3F now point at $C500)
$C82F  LDA $C082,X
  ...

Welp, that looks like a smoking gun to me. The ProFile is storing its slot information in 3E and 3F; the boot block is using the same addresses as a scanner pointer. Neither is saving/restoring those memory locations, and both of them are listed as used by various parts of the ecosystem, but at boot time, it's not clear that any of those are important. The boot block sets up its pointer, calls out to read a block, and the readblock driver in the ProFile ROM happily overwrites the pointer. Whose bug is that, exactly? Nobody's. Everyone's. But today it's mine to untangle.

Either (a) we ask for these to be patched upstream, so that Total Replay and Nox Archaist both change their code; or (b) we patch the ROM so that it saves/restores what it's doing in zero page itself, protecting whatever calls it.

(a) involves waiting, so let's look at (b) first. In theory all it would take is a couple instructions to save $3F/$3E at the head of the driver, and restore at the end. Unfortunately the ROM is, as most are, completely full and we'd need a couple dozen bytes to do what I'm proposing. But if we rewind back to the full ROM disassembly, it begins like this:

$C800                  STR      '(C) APPLE COMPUTER INC. 1984'
$C81C DRIVER           PHP
$C81D                  SEI
$C81E                  JSR      DRV_DISPATCH
...

We don't need that string. The only thing it could be doing is helping to identify what peripheral is plugged in to this slot and, since the ROM does its own work alongside boot blocks in this area, it's really not important. Let's see how much I can cram in there:

$C800  LDA $3E          ; ZPSAVE: save the caller's pointer
$C802  PHA
$C803  LDA $3F
$C805  PHA
$C806  JSR $C81C        ; the ORIGINAL bracket, byte-identical
$C809  STA $31          ; park A ($31 is stock-ROM scratch already)
$C80B  PLA
$C80C  STA $3F          ; restore
$C80E  PLA
$C80F  STA $3E
$C811  LDA $31          ; restore A; PLA/STA/LDA never touch carry
$C813  RTS
$C814  STR '3E3FSAVE'   ; marker, in the space the copyright used to occupy
$C81C  PHP              ; ...the stock ROM continues here, unchanged

Then we just need to patch where the driver is called:

                 stock                      zpsave
$CFF6  STA $07F8                  $CFF6  STA $07F8
$CFF9  JMP $C81C                  $CFF9  JMP $C800      <-- the only other change
$CFFC  HEX 00 00 47 EA            $CFFC  HEX 00 00 47 EA

Now when ProDOS tries to dispatch to our READ, it actually calls the ZPSAVE wrapper which then calls the stock read dispatcher. Nice, clean, and ... mostly worthless. I haven't even tested it; one of the failures of my card was in the rom slot itself, and I have no interest in making that worse. For the same reason I really wouldn't suggest that anyone else do so. Which means we have an academic, theoretical fix that's not terribly useful and we don't even know if it definitively fixes our boot error. Not terribly satisfying (although, I have to say that cramming those bytes over a string really felt good.)

No, what we need to do is patch one of the images and see if it boots while *not* using $3E / $3F. First, a scan of the boot block tells me that $EB / $EC seem free. These are in the range of "nothing known claims them," so they're probably safe for the bootloader itself. What if we find and replace all of the former instances with the latter?

Total Replay
Memory Location
oldnewinstruction
$086C3FECSTA $3F
$08733EEBSTA $3E
$08773EEBLDA ($3E),Y
$08833EEBLDA $3E
$08873EEBSTA $3E
$088B3FECINC $3F
$089A3EEBLDA ($3E),Y
$08A33EEBLDA ($3E),Y
$08A73EEBLDA ($3E),Y

(And, for completeness, the same 9 operands for Nox Archaist at $0831, $0838, $083C, $0848, $084C, $0850, $085F, $0868, $086C.)

What does that get us (he asked, expecting another crash further down the boot process)?

Gold. YES, THAT'S OUR BUG. Reported upstream, already patched in both; the next Total Replay past v5.2 should include it; and if you're a Nox owner that needs an updated image, you should join the 6502 Workshop Discord server, introduce yourself in #the-scuttlebutt-pub channel, and then ask for the update in the #nox-tech-help channel.

Discussions