Hacking the Color BASIC PRINT command – part 3

See Also: part 1, part 2, part 3, part 4, part 5 and more to come…

Just because it works, doesn’t mean it’s correct.

Keep this in mind as I continue my exploration of using Color BASIC RAM hooks to change how the PRINT output works.

Previously, I created a small assembly language routine that would intercept BASIC’s output to the 32 column screen and then treat a lowercase “r” (as in “right”) as a transparent space. My proof-of-concept seemed to work.

Today, I present updated code that now supports “u” for up, “d” for down, “l” for left, and “r” for right, semi-simulating how you can embed cursor movements in a PRINT command on a VIC-20 (and, I assume, the C64 and maybe even the PET before it).

Let me walk you through what I came up with:

* lwasm consmove2.asm -fbasic -oconsmove2.bas --map

* Convert any lowercase characters written to the
* screen (device #0) to uppercase.

UP      equ     'u      character for up
DOWN    equ     'd      character for down
LEFT    equ     'l      character for left
RIGHT   equ     'r      character for right

I start with some definitions of the characters I want to use to move the cursor UP, DOWN, LEFT or RIGHT. I wanted to be able to easily change these characters. In a future installment, I may try to implement this using “DEF USR” so you could do something like X=USR0(“abcd”) and pass in a string containing four characters to use for up, down, left and right. For now, hard-coded.

DEVNUM  equ     $6f     device number being used for I/O
CURPOS  equ     $88     location of cursor position in RAM
RVEC3   equ     $167    console out RAM hook
VIDRAM  equ     $400    VIDEO DISPLAY AREA

These next defines are things that the Color BASIC Unravelled disassembly listings had defined. The first is the memory location that contains the current device number for output (0 for screen, -2 to printer, etc.). After that is where BASIC tracks the cursor position on the 32 column screen. Neat is the address of the RAM Vector for the console out routine. Lastly, VIDRAM is the start of the 32-column screen in memory (1024, or &H400 in hex).

    org $7f00

init
    lda RVEC3       get op code
    sta savedrvec   save it
    ldx RVEC3+1     get address
    stx savedrvec+1 save it

    lda #$7e        op code for JMP
    sta RVEC3       store it in RAM hook
    ldx #newcode    address of new code
    stx RVEC3+1     store it in RAM hook

    rts             done

uninstall
    * TODO

Here, I changed the start address of this program to &H7F00 instead of &H3F00. If trying this on a 16K machine, you would have to change it back. Once I know how big the final assembly code is, I’ll probably move it as close as possible to the end of the 32K RAM used by BASIC so it can leave as much RAM for the BASIC program as possible.

This init routine pulls out the three bytes at RVEC3 (which are either “rts” three times for Color BASIC, or patched to be a “JMP xxxx” 3-byte instruction if other BASIC ROMs (Extended, Disk, CoCo 3) are installed I save those to 3-bytes of reserved memory at the end of the program.

I then replace it with a JMP (&H73) instruction followed by the two byte address of my “newcode” that I want to run any time a character is output.

For now, there is no way to uninstall this routine, but I left myself a reminder I should create an uninstall routine. I think I may just make it so if you EXEC the first time, it installs, and if you EXEC again, it uninstalls, rather than using two different memory locations the user would need to know. We shall see.

newcode
    * Do this only if DEVNUM is 0 (console)
    tst     DEVNUM      is DEVNUM 0?          
    bne     continue    not device #0 (console)

    leas    2,s         remove PC from stack since we won't be returning there.

* Now this is the start of what Color BASIC ROM does for PUTCHR:
* PUT A CHARACTER ON THE SCREEN
LA30A
    PSHS    X,B,A       SAVE REGISTERS
    LDX     CURPOS      POINT X TO CURRENT CHARACTER POSITION

When BASIC needs to output a character, it ends up at the PUTCHR routine in the ROM, starting at address A282. The very first thing it does is “jsr RVEC3” which means it will now jump to this “newcode” routine. The character to output will be in register A.

The first thing I do is check the DEVNUM value to see which device number is being used. I use “tst” to check for zero. If the device is not zero (screen), I branch to a “continue” routine which will be the code that was originally in the RVEC3. (Thus, on Color BASIC it will just branch to an “rts” return, or it will branch to whatever “jmp xxxx” was in the vector before I hijacked it. More on this in a moment.

If I get past that, I will be doing some work and then jumping back into the ROM so the rest of BASICs output routine can run. In that situation, I will not be doing an “rts” from my custom code. I may be wrong on this, but my understanding is that I need to remove the return address off the stack (since I will not be returning), which I do by moving the S stack pointer up by 2. Is this what I need to be doing?

Since BASIC already checks for special characters like backspace and ENTER, I was able to pattern my code after what the ROM does for those and leave out the part where it puts a character on the screen. I take the first two line that BASIC would have done (from LA30A in the Unravelled listing) and put them into my code. This then lets me have my custom blocks checking for special characters the same way BASIC does for the ones it handles.

checkup
    cmpa    #UP
    bne     checkdown
    CMPX    #VIDRAM+32  second line or lower?
    blt     goLA35D     disallow if on top line.
    leax    -32,x       move up one line
    bra     done

The first check is for the UP character. If not seen, we branch to the “checkdown” routine just after this one. If it was UP, then we check to see if X is 32 bytes past the start of screen memory — the start of the second line. If you are on the top line, moving UP is ignored and we branch to a label that will then JMP back into the Color BASIC ROM to finish the normal checks that BASIC does.

Else, decrement X (cursor position) by 32 to move up one line, then branch to the “done” routine which will get us back into BASIC ROM code.

checkdown
    cmpa    #DOWN
    bne     checkleft
    cmpx    #VIDRAM+512-32
    bge     goLA35D     disallow if on bottom line.
    leax    32,X        move down one line
    bra     done

The check for DOWN follows a similar pattern, this time disallowing down if you are on the bottom line of the screen. This is different than how Commodore handles it, since down on the Commodore will cause the screen to scroll up. I plan to fix this later to make it act more like the VIC-20 does.

If we cannot move down, we branch to a label that will then JMP back into the Color BASIC ROM to finish the normal checks that BASIC does. If we can move down, the cursor position in X is increased by 32.

NOTE: During my testing, I found a VIC-20 bug. If you cursor to the bottom right of the screen then press RIGHT, the first time you do this is scrolls up and then the cursor appears one line before the last line. If you then cursor to the bottom right position again and go RIGHT, it scrolls up and now the cursor is on the bottom left position. Odd. I should find a VIC-20 BASIC disassembly and see if I can figure out what causes it do misbehave, but only the first time.

checkleft
    cmpa    #LEFT
    bne     checkright
    cmpx    #VIDRAM     top left of screen?
    beq     goLA35D
    leax    -1,X        move left one character
    bra     done

The process repeats again for LEFT. This is much like what BASIC does with backspace (BS) where it disallows it if you are in the top left position of the screen. If we cannot move left, we branch to a label that will then JMP back into the Color BASIC ROM to finish the normal checks that BASIC does. If we can, the cursor position is decremented by 1.

checkright
    cmpa    #RIGHT
    bne     goLA30E
    cmpx    #VIDRAM+511 bottom right of screen
    beq     goLA35D
    leax    1,x         increment X, skipping that location.
    bra     done

Lather, rinse, repeat. For RIGHT, I disallow moving right from the bottom right character position. This is another difference from VIC-20 which I plan to fix later. If we cannot, we branch to the same label that gets us back into the BASIC ROM routine. If we can move right, the cursor position is incremented by 1.

And now some of the labels…

goLA30E
    jmp     $A30E       jump back into Color BASIC ROM code.

done
    stx     CURPOS      update cursor position
goLA35D
    jmp     $A35D       jump back into Color BASIC ROM code.

A30E in the disassembly is the first instruction right after the two lines I cloned into my routine at my LA30A label. Thus, I start out with those first two lines, then do my code and either jump back in to let the ROM code continue, OR I modify the cursor position then jump to DONE so it can finish up.

“done” will update the saved cursor position that is in the X register, then jump to A35D which is the final line of the output ROM routine where the registers that were pushed/saved at the top of the routine are pulled/restored (including PC which acts as an rts to whoever called the routine).

continue
savedrvec rmb 3         call regular RAM hook
    rts                 just in case...

    end

Lastly, if the original check for console (device 0) was not true, that code jumps to this end part so it can continue on to whatever the vector was pointing to before we hijacked it.

Here is a BASIC loader for this program. I added LINE 5 to reserve memory for the assembly, else BASIC will clobber it with string storage;

5 CLEAR 200,&H7F00
10 READ A,B
20 IF A=-1 THEN 70
30 FOR C = A TO B
40 READ D:POKE C,D
50 NEXT C
60 GOTO 10
70 END
80 DATA 32512,32607,182,1,103,183,127,96,190,1,104,191,127,97,134,126,183,1,103,142,127,24,191,1,104,57,13,111,38,68,50,98,52,22,158,136,129,117,38,10,140,4,32,45,50,48,136,224,32,43,129,100,38,10,140,5,224,44,36,48,136,32,32,29,129,108,38,9
90 DATA 140,4,0,39,22,48,31,32,16,129,114,38,9,140,5,255,39,9,48,1,32,3,126,163,14,159,136,126,163,93,32611,32611,57,-1,-1

And here is the complete assembly listing:

* lwasm consmove2.asm -fbasic -oconsmove2.bas --map

* Convert any lowercase characters written to the
* screen (device #0) to uppercase.

UP      equ     'u      character for up
DOWN    equ     'd      character for down
LEFT    equ     'l      character for left
RIGHT   equ     'r      character for right

DEVNUM  equ     $6f     device number being used for I/O
CURPOS  equ     $88     location of cursor position in RAM
RVEC3   equ     $167    console out RAM hook
VIDRAM  equ     $400    VIDEO DISPLAY AREA

    org $7f00

init
    lda RVEC3       get op code
    sta savedrvec   save it
    ldx RVEC3+1     get address
    stx savedrvec+1 save it

    lda #$7e        op code for JMP
    sta RVEC3       store it in RAM hook
    ldx #newcode    address of new code
    stx RVEC3+1     store it in RAM hook

    rts             done

uninstall
    * TODO

newcode
    * Do this only if DEVNUM is 0 (console)
    tst     DEVNUM      is DEVNUM 0?          
    bne     continue    not device #0 (console)

    leas    2,s         remove PC from stack since we won't be returning there.

* Now this is the start of what Color BASIC ROM does for PUTCHR:
* PUT A CHARACTER ON THE SCREEN
LA30A
    PSHS    X,B,A       SAVE REGISTERS
    LDX     CURPOS      POINT X TO CURRENT CHARACTER POSITION
    
checkup
    cmpa    #UP
    bne     checkdown
    CMPX    #VIDRAM+32  second line or lower?
    blt     goLA35D     disallow if on top line.
    leax    -32,x       move up one line
    bra     done

checkdown
    cmpa    #DOWN
    bne     checkleft
    cmpx    #VIDRAM+512-32
    bge     goLA35D     disallow if on bottom line.
    leax    32,X        move down one line
    bra     done

checkleft
    cmpa    #LEFT
    bne     checkright
    cmpx    #VIDRAM     top left of screen?
    beq     goLA35D
    leax    -1,X        move left one character
    bra     done

checkright
    cmpa    #RIGHT
    bne     goLA30E
    cmpx    #VIDRAM+511 bottom right of screen
    beq     goLA35D
    leax    1,x         increment X, skipping that location.
    bra     done

goLA30E
    jmp     $A30E       jump back into Color BASIC ROM code.

done
    stx     CURPOS      update cursor position
goLA35D
    jmp     $A35D       jump back into Color BASIC ROM code.

continue
savedrvec rmb 3         call regular RAM hook
    rts                 just in case...

    end

And now I can write a program like this:

As I typed that in, after the four Xs I typed a lowercase “d” for down, then four lowercase “l” for left, then X and two “r” then another X, then “dlll” followed by the four Xs again. It looks weird as it moves the cursor around as I type, but I will be fixing that with more additions to this routine in a future installment.

For now, it lets me put this 4×3 thing on the screen without erasing stuff around or behind it:

Progress!

Please, folks, let me know in the comments if you see something I am doing wrong. Am I handling the stack pointer thing properly? I am just experimenting and … well, “it works for me.”

To be continued…

2 thoughts on “Hacking the Color BASIC PRINT command – part 3

  1. William Astle

    You are correct with the stack pointer handling. Because the RVEC is called with JSR, you need to clear the return address if you aren’t going back to where the RVEC was called from (or otherwise chaining to another routine that is handling the RVEC).

    Reply

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.