Adding a feature to the MEGA65
On every Commodore computer that has a Home key, I routinely hit Home when I mean to hit the Del key. The only way to move the cursor back to where I need it is with the arrow keys. With the MEGA65, I finally have the opportunity to do something about this, once and for all.
The problem
I’m used to a chunky backspace key. The keyboard you’re using right now probably has one, a big target in the upper-right corner of the keyboard you can slap when you type a wrong character.
I need a backspace key frequently on Commodore computers because I type faster than these old machines can handle. Specifically, they lack a modern keyboard feature called “n-key rollover” (NKRO) that helps the computer figure out what you mean to type when you’re typing so fast that you’ve hit the next key before letting go of the previous one. Despite its newness, the MEGA65 inherits this historical gap from its predecessors. (We may make a go of fixing this completely, but that’s for another day.)
So when I’m typing on a MEGA65 and I have typed so fast that it missed a key, I’m inclined to delete back to the error to fix it. But instead of hitting the Del key, I often accidentally hit the Home key, which relocates the cursor to the upper left corner of the screen, adding insult to injury. The Commodore screen editor doesn’t have an “undo” feature. I can’t just zap the cursor back to where it was and continue typing. Instead, I have to use the cursor keys to navigate down and to the right—typically all the way down and nearly all the way to the right because I’m typing lines of BASIC code at the bottom of a list of lines.
On the Commodore 128 and MEGA65, you can send the cursor to just after the right-most non-space character on a line (the “end of the line”) using ESC then K. There is no similar shortcut for jumping to the bottommost non-empty line.
The Commodore 64 and 128 can’t spare the memory or the processing power for a modern “undo” feature, where I could just Cmd-Z to undo anything I’ve done and wish I hadn’t. The MEGA65, with its 40 MHz processor and its 8+ megabytes of memory, potentially could. A fancy extension of the BASIC editor could potentially undo a limited set of typing. It’d need to account for other behaviors of the screen editor—it’d behoove the extension to clear the undo buffer every time you press Return, for example—but there’s potential.
But really, I just want one thing: the ability to undo having just pressed the Home key.
A related feature?
The Commodore 65 screen editor offers a way to remember the current cursor position and restore it later. Press ESC then ↑ (up arrow, not cursor up) to store the position, then later press ESC then ← (left arrow, not cursor left) to restore it.
This doesn’t quite do what I want because I’m trying to recover from moving the cursor accidentally. But it’s an interesting starting point for how an “undo Home” feature might be implemented.
I considered whether the feature I want could perhaps work with the cursor memory feature, such as automatically saving the cursor position when Home is pressed so that ESC ← would restore it. I decided against this because it’s easy to imagine someone intentionally homing the cursor with a different position intentionally stored, and I didn’t want to interfere with that behavior, at least not without much more usability data around how people use it. (Getting such data would be challenging, to say the least.)
So instead, a modest proposal: pressing Home stores the cursor position in an “undo Home” buffer. Pressing ESC then Home (a previously unused combo) restores the cursor to the position in this buffer.
Why don’t we do it in the ROM
The architecture of a Commodore computer allows a program to replace almost every piece of the base operating system by installing modified routines into the same memory locations where ROM is normally located, then telling the computer to use the RAM instead of the ROM for those addresses. That’s one way to write third-party extensions or replacements of operating system features.
The nature of the MEGA65 project offers an alternate solution. The MEGA65 team has the original (copyrighted, licensed) Commodore 65 source code, and is actively fixing, completing, and extending it with patches. If this is a feature that others might want, I could collaborate with the team to add this feature directly to the MEGA65 ROM. Then it would be present at boot, without having to load an extension app.
Despite all of the MEGA65’s RAM, the ROM operates under tighter conditions similar to the original C65, especially when it comes to space for new code. The MEGA65 team has been able to reclaim about 2 kB of code space through optimizations and removal of unused legacy features, but they must be judicious about how they add new features, if only to leave room for fixes to bugs we might discover in the future. This is the biggest barrier to adding larger editor features like a full undo typing buffer.
I proposed the feature to the team, and they granted me access to the patch source repo and encouraged me to give it a try. I didn’t tell anyone that this would be my first experience writing production assembly code, but how hard could it be?
Reading 32-year-old assembly language
The MEGA65 ROM source consists of six large files, in the language of the MOS 6502 lineage of CPUs. Specifically, the MEGA65 has its own extension of the CPU line, the “45GS02,” a successor to the 4510 multi-device chip containing the 65CE02 CPU. The 65CE02 was only ever used in the unreleased Commodore 65 and a single Amiga peripheral, which is kind of a shame because it added several interesting features to its predecessor, the 65C02. (Perhaps the lovable WDC 65C816 was the more compelling alternative at the time.)
It’s mostly familiar 6502 code, with the exception of occasional use of the 65CE02’s Z register. The original Commodore comments are terse yet readable, though like most large code bases it takes some time to get familiar with how it is organized. After a few guesses, I was able to locate the cursor memory feature. These lines allocate memory for the cursor position:
save_cursor_column *=*+1 ;esc^ saves x-position here
save_cursor_line *=*+1 ; y-position here
save_input_column *=*+1 ; where input began here
save_input_line *=*+1
The routine to save the position, activated by ESC ↑, simply copies the current positions to these locations:
save_position ;save current cursor position
ldx lsxp ;save current input line, column too
stx save_input_line
ldy lstp
sty save_input_column
ldx tblx
stx save_cursor_line
ldy pntr
sty save_cursor_column
rts
The cursor position is stored as screen coordinates, a column (0-79, or 0-39 in 40-col mode) and a row (0-24). This also stores the logical line position, which is needed in the edge case where the screen has changed from 80 column mode to 40 column mode between the storing and the restoring of the position. A logical line can be up to 160 characters, and in 40 column mode it can take up to four rows on the screen vs. two rows in 80-column mode. I’m guessing it better meets the user’s expectations to prefer screen coordinates, such as if the user restores the cursor position after having shifted the logical lines on the screen, and only needs to handle this edge case when the stored screen coordinate is no longer valid.
The restore routine, activated by ESC ←, calls a subroutine to move the cursor to the screen position. If it fails, it tries again using the logical line position:
restore_position ;restore saved cursor position
ldx save_cursor_line
ldy save_cursor_column
clc
jsr plot ;put cursor there
bcs 10$ ; whoops, it didn't go
ldx save_input_line
stx lsxp ;restore input line, column too????
ldy save_input_column
sty lstp
10$ rts
These routines are referenced from an escape_vectors
jump table indexed by their key codes. The escape
routine handles special cases, then falls through to this table to resolve ESC key combos. (All comments are by the original Commodore devs. Call their mothers, not mine.)
******
escape
******
cmp #esc
bne 10$ ; branch if not double <escape>
lsr datax ; else cancel <escape> sequencer by fucking up lstchr
jmp reset_modes ; and exit via 'toqm' to cancel all other modes too
10$ and #$7f ; ignore shift
cmp #'4'
beq column_40
cmp #'8'
beq column_80
sec
sbc #'@' ;table begins at PETSCII '@' & ends at ']'
cmp #32 ;32 chars @ to _
bcs 90$ ;invalid char, ignore sequence
asl a ;character is index to dispatch table
tax ;index
jmp (escape_vectors,x)
90$ rts
That’s where my restore routine will be called. To store the cursor when I press Home, I need to figure out how that is handled. It’s so quick you might miss it:
home ldx sctop ;put the cursor at top left of current window
stx tblx ;move to top of window
stx lsxp ; (for input after home or clear)
; ...
This home
routine is actually called every time a home control code is printed, not just typed, but that’s acceptable and even desirable. It’s a design motif of the screen editor that anything that can be typed can be printed and have the same effect. This is true for all of the ESC sequences as well.
Assembly code is simple, in the small. Full-sized applications quickly become all about managing the complexity of having many, many thousands of simple instructions. Thankfully, I just need to replicate simple logic in narrow places.
Fitting in a small change
We need another set of cursor memory, so let’s put it immediately after the existing set:
save_home_cursor_column *=*+1 ;home saves x-position here
save_home_cursor_line *=*+1 ; y-position here
save_home_input_column *=*+1 ; where input began here
save_home_input_line *=*+1
We’ll have similar store and restore routines. Let’s name them save_home_position
and restore_home_position
. As a first attempt, these are identical to save_position
and restore_position
with the memory addresses changed, so I won’t repeat them here.
To store the previous cursor position when someone presses Home, we call save_home_position
from the home
routine.
home jsr save_home_position ;remember where we were
ldx sctop ;put the cursor at top left of current window
stx tblx ;move to top of window
stx lsxp ; (for input after home or clear)
; ...
To restore when someone presses ESC Home, we check the value of the key pressed after ESC in the escape handler. This is just one more check after the '4'
and the '8'
:
cmp #$13 ; esc home
beq restore_home_position
As this was my first time messing with this code base, and my way of testing involved using the M65Connect tool to beam new ROMs over to my MEGA65 over a serial cable, I wanted to make sure these invisible functions are actually being called. The Commodore assembly language programming equivalent of print("HERE!")
is inc $d020
, which cycles the color of the screen border. I temporarily put this in the “store” routine and inc $d021
(background color) for the “restore” routine, which made it easy to see that the handlers were getting called, even if they weren’t moving the cursor correctly.
Let’s try to build it!
*** Error in file system.src line 4971:
Phase error label [vector_40$] pass 19: f031 pass 20: f034
make: *** [0C800-0CFFF.KERNEL] Error 1
Uh oh. What happened here?
The error message seems inscrutable, but we have enough context to figure out generally what happened. Assemblers make multiple passes over the source code to figure out how to replace symbols like save_home_position
with memory addresses calculated by counting up the bytes produced from the assembled instructions. Between two passes, something got pushed out of the way, in a way the assembler wasn’t expecting.
I didn’t bother to figure out exactly what was too big or how, but I got the idea. I added too much code. The ROM code was trying to keep major parts of the code in consistent places, with some wiggle room between parts for a few new instructions to be added here and there. But there wasn’t enough room for my new code to wiggle.
Thankfully, my new code is mostly a repeat of existing code. Time to get clever!
Getting clever
Instead of two identical save
routines that differ only by the address, we can rewrite the original save_position
to operate on an offset, then have two entry points with different offsets. The 45GS02 can use the X register as an offset for many operations. We put the two save buffers next to each other in memory, so the offset is the difference between their starting addresses. We also have to rewrite the original to avoid using the X register for other storage, but that’s easy in this case.
save_home_position ;save cursor position for home
ldx #(save_home_cursor_column-save_cursor_column)
ldy tblx
jmp save_position_continued
save_position ;save current cursor position
ldx #0
save_position_continued
lda lsxp ;save current input line, column too
sta save_input_line,x
lda lstp
sta save_input_column,x
; ...
We can do something similar for restore_home_position
. This one depends on the X and Y registers to call plot
, so it’s easier to just set the registers in different branches than to mess with offsets.
restore_home_position ;restore saved cursor position from home
ldx save_home_cursor_line
ldy save_home_cursor_column
jmp restore_position_continued
restore_position ;restore saved cursor position
ldx save_cursor_line
ldy save_cursor_column
restore_position_continued
clc
jsr plot ;put cursor there
; ...
Polish
While testing I noticed that it was common for me to hit Home more than once as I flail about the keyboard. As I wrote it originally, a second press of Home would rewrite the undo-home stored position with the new cursor position, which would just be (0,0). So I added an extra check to save_home_position
to bail if the current cursor position is (0,0), preserving the previous value.
The C128 and MEGA65 have a feature where you can define a WINDOW
on the screen where subsequent terminal messages are printed and scrolled. While a window is active, the upper left corner of the window is the new cursor home location, and pressing Home takes you there. But the cursor store routine has a bug: it uses raw screen coordinates, not window coordinates, so when used during an active window, the restored position overshoots by the distance from the screen corner to the window corner. I located the memory locations used for the window corner and subtracted their values during the save
routine. This window bug was in the original C65 cursor memory feature, and by rewriting the routines to share code, I fixed them both! I fixed a C65 bug!
There’s one more flaw that I decided not to fix, though I was sure to mention it to the team, just in case. The window feature lets the user cancel the window by pressing Home twice. It does this by keeping track of the previous key pressed for all key presses, and triggering alternate logic if the latest key and the previous key are both Home. Technically, a user would expect the new escape sequence ESC Home to be treated as a single thing, and pressing Home immediately after that shouldn’t trigger the window escape. But this simple logic sees ESC Home Home, and if it’s in a window this qualifies as escaping the window.
I figure it is unlikely that someone would knowingly undo Home then immediately press Home again, and this issue would only occur inside a WINDOW
. With ROM space at a premium, I declared this “will not fix.”
(WINDOW
is a cool feature and I can think of many uses for it from both a program and in immediate mode. I can’t say how many people know about it or use it regularly. It was introduced with the C128.)
Release!
Bit Shifter, chief MEGA65 ROM engineer, accepted my change into ROM v920362. Not only did I use rudimentary assembly language skills to implement an ergonomic enhancement that improves my experience with the MEGA65, but we added it to the MEGA65 software itself so everyone can benefit. It dazzles me to think that I have contributed a fix for an issue that I have with all Commodore computers to the Commodore software lineage.
The next time you press Home accidentally, give ESC Home a try. If you don’t have a MEGA65, it’ll make you want one even more. 😄