Adding a feature to the MEGA65

Lab Notes

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 MEGA65 Clr/Home key, right next to the Inst/Del key.
The MEGA65 Clr/Home key, right next to the Inst/Del key.

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.

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. 😄