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
- The Overlay System
- Screen Recovery
- Font Management
- Zero Page ← this article
- Copy Protection
- Localization
- File Format and Pagination
- Copy & Paste
- 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.
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
BASIC is $03 through $8F, and KERNAL is $90 through $FA. You can see this in the KERNAL and BASIC sources:
https://github.com/mist64/cbmsrc/blob/master/KERNAL_C64_03/declare
https://github.com/mist64/cbmsrc/blob/master/BASIC_C64/declare