In the series about the internals of the geoWrite WYSIWYG text editor for the C64, this article discusses how the app consolidates keyboard input to keep up with fast typists.
- The Overlay System
- Screen Recovery
- Font Management
- Zero Page
- Copy Protection
- File Format and Pagination
- Copy & Paste
- Keyboard Handling ← this article
GEOS Keyboard Basics
GEOS does not use the keyboard driver in the C64’s ROM, but has its own implementation that uses ASCII-based 8 bit key codes: Codes $20 through $7F are the regular ASCII printable characters. All control keys (e.g. cursor keys) as well as non-ASCII keys (like “£”) are mapped into the control code space $00 through $1F:
|$07||NO SCROLL||C128 only|
All this defines the codes $00-$7F, which accounts for seven bits. The uppermost bit (value of $80) indicates whether the “Commodore” modifier key has been pressed with the key. The Commodore key is used in GEOS for keyboard shortcuts, much like the Command key on the Macintosh.
The keyboard driver runs in the interrupt context, which is triggered 60 times per second by a timer. It scans the keyboard and puts new key codes at the end of a 16 byte queue: This way, no keys will be lost if the application can’t get to handling incoming keys immediately – as long as it’s not more than 16 keys behind.
The application can get the next key from the queue by calling
GetNextChar. If there is a key in the queue, it will be removed from the queue and returned. Otherwise, a value of 0 will be returned.
All this is pretty much what’s going on in any operating system – including the C64’s original ROM in BASIC mode.
GEOS is an event-driven environment: The system’s “main loop” controls all execution and calls back the app based on events, like mouse clicks, key presses and timers.
The idiomatic way to handle the keyboard on GEOS is therefore to register for keyboard callbacks. On every main loop iteration, the system will check whether there is at least one key in the queue. If this is the case, it will dequeue the oldest key, store it in the system variable
keyData and call the function pointer
keyVector, which the app can set. If the vector is 0 (the default), no callback will happen and the key code will be discarded.
The app can then read the key code from
keyData, handle it and return. If there are more keys in the queue, the main loop will call the vector again, with the next key code, in the next iteration.
Alternatively, the callback function can read more keys from the queue by repeatedly calling
The core function of geoWrite is text input, so it needs to make sure it is responsive, and no key presses get lost.
And that’s tricky: On a 1 MHz CPU, redrawing several lines of proportional fonts can take seconds, so it is quite common that the app falls behind the user’s typing. So if the user types a character that leads to a few lines being redrawn, and the user types three characters in the meantime, these three characters may cause three redraws, in which time the user can type another nine characters. The app would fall more and more behind, and eventually drop characters.
To avoid this, the app has to catch up to the user’s typing. The good news is that once the app is behind on keys, it has several key codes to work with, so there are some optimizations that can be applied:
Consolidating Character Inserts
When inserting a single character, the text data after the cursor is moved up by one byte and the new character is added. Then, the screen is updated: The part of the current line to the right of the cursor is redrawn, and if the last word of the line overflows to the next line, that one has to be redrawn as well and so on.
The following animation shows this in action:
Instead of doing this work for every character in the keyboard buffer separately, a lot of work can be saved by doing it for all characters in one go:
- Only move the buffer up once, by the number of characters in the buffer.
- Copy the characters into the buffer.
- Redraw only once.
This is basically the same as what happens when pasting text from a text scrap.
You can see the effect of this strategy on the animation above. The “a” key was held down on the keyboard, generating a fast stream of “a” key codes.
- The first character is inserted, which triggers a redraw of just the line.
- In the meantime, two more characters came in, which are added in one go. This triggers the redraw of the whole paragraph, in this case, since the word “Commodore” had to be moved to the next line.
- This redraw was so expensive that seven more characters came in in the meantime. The line gets redrawn, and it does not overflow this time.
- Because this redraw was cheap, only two more characters came in. Again, it causes a redraw of only the current line.
We see in practice that whenever more text needs to be redrawn, more keypresses are buffered, but the system catches up quickly.
All printable characters as well as line break and TAB characters can be combined into a single string to be inserted. All other keys have to be special cased.
If the user types a character, and then hits the DEL key, the document will effectively be unchanged, but the text in memory was first moved up by one byte, then moved down by one byte again, and the screen was updated twice.
So if the keyboard buffer contains a DEL char, geoWrites removes the preceding character from the buffer. The two characters cancel each other out and no more work has to be done.
If a DEL is encountered while the buffer is empty, no character can be removed, but it’s not a character that can be inserted either. geoWrite then increments the count of DEL keys it has seen in the keyboard buffer that remained. There are some examples after the next paragraph.
Control Keys and Keyboard Shortcuts
Non-printable key codes like cursor keys and keyboard shortcuts are also special. If the queue contains “abc”, followed by C=T (Commodore Key + T; paste text) and “def”, geoWrite has to insert “abc”, paste the text in the text scrap, and then insert “def”. It can not combine the two text strings.
This is why whenever a control key or shortcut is encountered, geoWrite stops processing the queue. The DEL characters, the string so far and the control key will be evaluated, and the remainder of the keyboard queue will be handled once the
keyVector is called the next time.
Here are some examples of contents of the keyboard queue, the number of DEL characters at the beginning, the string to be inserted, the control key that was detected, and the contents of the keyboard queue after this processing:
|Kbd Queue Before||# DEL||Insert String||Control Key||Kbd Queue After|
The examples show that DEL keys that follow a character will effectively remove that key, but if the number of DEL key codes exceeds the number of characters before it, the extra DEL keys will be counted. And if there is a control key or keyboard shortcut, processing of the keyboard queue stops at this point.
This is the code that processes the keyboard queue and returns the number of characters to delete, the string to insert, and the detected control key:
processKbdQueue: lda #0 sta delCount ; no excess DEL keys so far sta kbdStringCnt ; kbd string empty sta curControlKey ; no control key lda keyData ; first character, as passed into keyVector @again: bmi @ctrl ; "C=" shortcut, so stop processing cmp #KEY_DELETE ; DEL key? beq @del ; yes cmp #KEY_INSERT ; SHIFT+DEL? beq @del ; yes (same as DEL in GEOS) cmp #CR ; return key? beq @nctrl ; yes, does not count as a control key cmp #TAB ; TAB key? beq @nctrl ; yes, does not count as a control key cmp #$20 ; below $20, i.e. non-printable bcc @ctrl ; yes, it's a control key, stop processing @nctrl: ldx kbdStringCnt sta kbdString,x ; add to kbd string inc kbdStringCnt @next: jsr GetNextChar ; are there more characters in the kbd queue? tax beq @rts ; no, return bne @again ; yes, repeat @del: ldx kbdStringCnt ; are there characters in the kbd string? beq @excss ; no, no characters to delete dec kbdStringCnt ; remove previous character bra @next ; and continue @excss: inc delCount ; count excess delete characters bne @next ; and continue @ctrl: sta curControlKey ; save control key @rts: rts
keyVector handler calls this, and then applies the three outputs (DELs, string, control key) to the document:
- First, if there are excess DELs, it deletes characters at the current cursor position according to the number of DELs. It just moves the remainder of the buffer down by the number of DELs.
- Then, if there is a string, it inserts it in one go by moving the buffer up by the length of the string, and copying the string into the text.
- If there were DELs or a string, the screen is updated.
- If there is a control key, it gets evaluated.
There are two special cases: If there are more DELs than characters on the current page, the extra DELs have to be handled differently. And if there are DELs and a string, there is an optimization performed in the first two steps: There is no need to move the buffer twice. For example
- if there is one DEL and two characters in the string, the buffer has to be moved up by one byte, and the insertion position is one byte before the cursor.
- if there are two DELs and one character in the string, the buffer has to be moved down by one byte, and the insertion position is two bytes before the cursor.
- if there are two DELs and two characters in the string, the buffer does not have to be moved at all, and the insertion position is two bytes before the cursor.
When designing software, a slow CPU can be made up for if there is a lot of memory, e.g. by caching data to avoid calculating it. And too little memory can be made up for by a fast CPU. GEOS and geoWrite had neither. They were written for an 8-bit CPU with 64 KB of RAM.
This scenario makes all design decisions interdependent:
- The memory constraint requires all code to be optimized for size, which makes it slower, and requires geoWrite to be split into 9 parts that are loaded from the slow disk drive.
- The font renderer could have been much faster, had it had the space for fastpath code and caches.
- Slow font rendering requires geoWrite to include lots of extra logic to be able to keep up with fast typists.
- The extra code increases the memory pressure, requiring other code to be written more densely or pushed into different records.
The GEOS engineers seem to have found a reasonable tradeoff. Yes, geoWrite is slow on an unexpanded C64, but it is a useful and very powerful tool for a 64 KB computer.