Inside geoWrite – 4: Zero Page

In the series about the internals of the geoWrite WYSIWYG text editor for the C64, this article discusses how it makes maximum use of the scarce zero page space.

Article Series

  1. The Overlay System
  2. Screen Recovery
  3. Font Management
  4. Zero Page ← this article
  5. Copy Protection
  6. Localization
  7. File Format and Pagination
  8. Copy & Paste
  9. Keyboard Handling

GEOS Zero Page

The MOS 6502 CPU has special encodings for addresses that fit in 8 bits: Instructions that read from or write to addresses $0000 to $00FF in memory are encoded in two instead of three bytes:

a5 28      lda $28
ad 28 00   lda $0028

The two instructions have the same effect, but the first one is one byte shorter, and faster by one clock cycle.

Zero page space is scare and valuable, so it has to be used wisely. This is the GEOS zero page layout, roughly to scale:

-----------------------------------------
$0000  6510 CPU built-in I/O port
-----------------------------------------
$0002  Virtual 16 bit registers
       r0-r15

-----------------------------------------
$0022  Used by GEOS system


-----------------------------------------
$0040  Reserved for GEOS system




-----------------------------------------
$0070  GEOS app space            <<<<<<<<
-----------------------------------------
$0080  Used by GEOS disk driver
-----------------------------------------
$0090  Used by Commodore KERNAL ROM












-----------------------------------------
$00FB  GEOS app space            <<<<<<<<
-----------------------------------------

GEOS designates only a total of 21 bytes to the application. 30 bytes are used by the GEOS KERNAL itself, and 48 bytes are reserved for future versions of GEOS, so these areas are off-limits. The biggest part, 107 bytes, is used by the Commodore KERNAL ROM.

KERNAL Zero Page

The C64’s ROM consists of the 9 KB Microsoft BASIC interpreter and a 7 KB operating system: the Commodore KERNAL. When the machine is in BASIC mode, the KERNAL ROM takes care of the keyboard, the screen, RS232, tape, disks and printers.

For the most part, GEOS does not use the KERNAL ROM at all: It comes with its own keyboard, screen and mouse drivers. With disk drives and printers, it’s more complicated.

Disk drives and printers are daisy-chained on the Commodore Serial Bus. Unfortunately, byte transmission with the original protocol is painfully slow, which is why most applications and games come with their own speeder code which uploads alternative transfer code to the disk drive. GEOS also uses its own disk speeder called diskTurbo.

diskTurbo only replaces the data transmission protocol though, not the IEEE-488 TALK/LISTEN protocol, which is needed to negotiate which device is talking on the bus at which time. So whenever GEOS switches between disk drives, it calls the original code in KERNAL. And printers don’t allow uploading code to replace the bus protocol at all, so GEOS uses the KERNAL for talking to the printer as well.

To keep the original KERNAL happy, GEOS doesn’t touch any of its zero page variables – which is quite generous, since the serial code only touches a small part of the $0090-$00FA area.

In addition, GEOS reserves 16 more bytes for use by the diskTurbo driver at $0080-$008F. The Commodore 1541 driver uses two bytes in this space, for example.

So effectively, almost the whole upper half of the zero page ($0080-$00FA) is blocked because the code to access disks and printers uses parts of it.

geoWrite

Since the $0080+ area is only used by the system during disk and printer accesses, geoWrite can use it whenever it is not using the disk or the printer, as long as it restores its contents whenever it does need to use them.

It does this by swapping the 128 bytes in the zero page with a dedicated buffer. The area can now have one of two sets of contents: the geoWrite contents and the diskTurbo/KERNAL contents:

-----------------------------------------
$0000  6510 CPU built-in I/O port
-----------------------------------------
$0002  Virtual 16 bit registers
       r0-r15

-----------------------------------------
$0022  Used by GEOS system


-----------------------------------------
$0040  Reserved for GEOS system




-----------------------------------------
$0070  geoWrite variables        <<<<<<<<
-----------------------------------------    -----------------------------------------
$0080  geoWrite variables        <<<<<<<<    $0080  Used by GEOS disk driver
                                 <<<<<<<<    -----------------------------------------
                                 <<<<<<<<    $0090  Used by Commodore KERNAL ROM
                                 <<<<<<<<
                                 <<<<<<<<
                                 <<<<<<<<
                                 <<<<<<<<
                                 <<<<<<<<
                                 <<<<<<<< <-swapped->
                                 <<<<<<<<
                                 <<<<<<<<
                                 <<<<<<<<
                                 <<<<<<<<
                                 <<<<<<<<
                                 <<<<<<<<
                                 <<<<<<<<    -----------------------------------------
                                 <<<<<<<<    $00FB  GEOS app space (unused)
-----------------------------------------    -----------------------------------------

This is the code that swaps the zero page area and the buffer:

swap_userzp:
        php                             ; save all registers and flags
        pha
        txa
        pha
        tya
        pha
        ldx     #$7F                    ; $7F..$00
@loop:  ldy     userzp,x                ; load zp byte
        lda     userzp_copy,x           ; load buffer byte
        sta     userzp,x                ; store zp byte
        tya
        sta     userzp_copy,x           ; store buffer byte
        dex
        bpl     @loop
        pla                             ; restore all registers and flags
        tay
        pla
        tax
        pla
        plp
        rts

It saves all registers and flags, so it can easily be called from anywhere in the code without messing up any state. Here is an example of using it:

        LoadW   r0, otherFnBuffer       ; load argument
        jsr     swap_userzp             ; **swap**
        jsr     OpenRecordFile          ; call KERNAL disk API
        jsr     swap_userzp             ; **swap**
        lda     #2                      ; load argument
        jmp     PointRecord             ; load KERNAL API that does not access disk

The OpenRecordFile API call accesses disk, so it’s surrounded by calls to swap_userzp. The geoWrite programmers were very aware of which API calls cause a disk access: The PointRecord API call is about file management as well, but it only updates data structures and does not access disk, so there is no need to swap the zero page.

For all of this to be correct

  • swap_userzp has to be called as the very first thing when the application launches.
  • swap_userzp has to be called before exiting the app.
  • swap_userzp has to be called for every API that may end up calling the disk driver, as well as all printer APIs.
  • calls to swap_userzp always need to be balanced.
  • zero page variables at $80+ cannot be accessed between the two swap_userzp invocations.

Adding the two calls to every disk API call is prone to error, and bloats the code, so there are wrapper functions for the commonly used disk access APIs:

_ReadFile:
        lda     #ReadFile-GetBlock
        .byte   $2C                     ; skip next
_ReadByte:
        lda     #ReadByte-GetBlock
        .byte   $2C
_CloseRecordFile:
        lda     #CloseRecordFile-GetBlock
        .byte   $2C
_InsertRecord:
        lda     #InsertRecord-GetBlock
        .byte   $2C
_DeleteRecord:
        lda     #DeleteRecord-GetBlock
        .byte   $2C
_AppendRecord:
        lda     #AppendRecord-GetBlock
        .byte   $2C
_UpdateRecordFile:
        lda     #UpdateRecordFile-GetBlock
        .byte   $2C
_OpenDisk:
        lda     #OpenDisk-GetBlock
        .byte   $2C
_FindFile:
        lda     #FindFile-GetBlock
        .byte   $2C
_GetBlock:
        lda     #GetBlock-GetBlock
        .byte   $2C
_PutBlock:
        lda     #PutBlock-GetBlock
        add     #<GetBlock
        sta     @jmp+1
        lda     #0
        adc     #>GetBlock
        sta     @jmp+2
        jsr     swap_userzp
@jmp:   jsr     GetBlock
        jmp     swap_userzp

Each of the wrappers loads the offset of the specific API entry point from the GetBlock entry point, and the common code adds it to the GetBlock address, and uses self-modification to call the API – of course calling swap_userzp before and after.

These wrappers are in the record 0 code, so that any overlay code can call it as well.

Discussion

Like most of geoWrite’s tricks, this is a tradeoff. It gains 123 zero page locations, and its use speeds up the code by maybe a low two-digit percentage and saves maybe 1 KB of code space. On a slow and memory-constrained system like the C64, this is significant. On the other hand, the disk access code gets a bit more complicated (which is countered by the wrappers), and every back-and-forth swap takes about 6000 cycles. But in the context of a disk access, this is negligible.

2 thoughts on “Inside geoWrite – 4: Zero Page”

  1. Commodore’s use of the zero page is… curious, to say the least. I can’t really find any certified list of exactly what addresses are used for BASIC; I’d like to be able to know what addresses I can use if I’m using the KERNAL, but not the BASIC ROM at all.

    Elite also backs up the zero-page between KERNAL calls because Ian Bell & David Braben ported the game [quickly] from the BBC and they didn’t want to work out what was and wasn’t free to use on the C64! If you want to see a super-clean, well thought out 6502 zero page, check out the BBC micro: https://tobylobster.github.io/mos/mos/S-s1.html#SP19

    Reply

Leave a Comment