Hello VERA (BASIC vs C vs Assembly)
This short tutorial is a very simple comparison of three main languages used to develop software on Commander X16. Instead of printing standard Hello World message, let’s do something more interesting and specific to the platform but still try to keep it as simple as possible. There is nothing more unique to Commander X16 than using VERA and let's throw in reading a mouse .
I will not go into details on how to install development tools and what editor to use etc. but will focus on coding to highlight differences between approaches to make it easier to choose or transition between languages regardless of what your background is.
The only background required is a basic understanding of VERA (Versatile Embedded Retro Adapter). If you need a quick overview, I wrote a post here.
Goal
We will write simple drawing programs in default text mode. We will use the mouse for drawing and pressing the left mouse button will draw by filling the character over which we are hovering and the right button will erase it. That is it. It should be simple right?
The steps or algorithm that we will follow are as follows:
- Setup
- Turn on the mouse
- Read the mouse status
- Calculate the position of mouse in Video memory (VRAM)
- If left button is pressed draw the block
- If right button is pressed erase the block
Video of all three versions in action:
Setup
First step is to write some boilerplate preparation code both for C and Assembly programs, no need for any preparation for BASIC programs.
BASIC | C | Assembly |
---|---|---|
#include <cx16.h> #include <mouse.h> void main(void) { struct mouse_info info; static unsigned int m; mouse_load_driver(&mouse_def_callbacks, mouse_stddrv); mouse_install(&mouse_def_callbacks, mouse_static_stddrv); } |
.org $080D .segment "STARTUP" .segment "INIT" .segment "ONCE" .segment "CODE" ; I/O Registers VERA_LOW = $9F20 VERA_MID = $9F21 VERA_HIGH = $9F22 VERA_DATA0 = $9F23 VERA_CTRL = $9F25 ; Mouse Kernal routines MOUSE_CONF = $FF68 MOUSE_GET = $FF6B |
|
No preparation needed for BASIC programs. | Because mice were not something that was standard on computers in the 8 bit era the mouse implementation is in separate driver that needs to be linked and declarations included as separate header file. | In typical assembly program we have to define at what memory address it will start, that is what the .org directive is for. It is pointing to the beginning of the BASIC memory. We will not go into detail what each of the .segment directives mean but the result is that compiled program generates one SYS line in BASIC followed by the machine code. Therefore we can just run it using BASIC command RUN. Second section is for readability only, we could just use actual addresses when storing values to VERA registers or calling Kernal routines but it is good practice to make Assembly code more readable. |
Turn the mouse on
OK, we have everything ready and we can turn the mouse on. If we did everything correct during setup that should be fairly simple step.
BASIC | C | Assembly |
---|---|---|
10 MOUSE 1 | mouse_show(); | lda #1 ldx #0 jsr MOUSE_CONF |
MOUSE command is used to turn on or off the mouse pointer. 0 - Hide mouse 1 - Display default shape 255 - display but dont define the shape (custom mouse pointer) |
A register contains setting defining the visibility and shape of mouse pointer, values a re the same as in BASIC. Register X defines the scale (size) of the pointer. |
Read the mouse
Interestingly, actual reading the mouse position and state of the buttons is the simplest of all operations in this short exercise. Let's take a look:
BASIC | C | Assembly |
---|---|---|
20 TX=MX:TY=MY:TB=MB | mouse_info(&info) | ldx #2 jsr MOUSE_GET |
Commander X16 uses special variables that contain mouse values: MX - X position (from the left edge) in pixels MY - Y position (from the top edge) in pixels MB - Button state 0-no buttons, 1-left, 2-right, 4-middle |
In C the function mouse_info popluates struct containing position and button values. These structs are defined in mouse.h header file. | Kernal function MOUSE_GET requires one parameter in X register. It is pointing to Zero Page location where upon return from the function will contain four bytes: - Low and Hi byte for X position in pixels - Low and Hi byte for Y position in pixels Register A fill contain state of the buttons. |
Calculate
In order to draw at the position of the mouse on screen we have to calculate it in relation to the memory map. One thing to consider is the fact that mouse is essentially a sprite that moves on the screen in pixel precision. That in practice means that on default screen it can have X position value in range 0 - 639 and Y position in range 0-479 because default screen has resolution of 80 x 60 characters or 640 x 480 pixels.
The formula to calculate memory position of the character the mouse is pointing to is therefore:
Memory = Y/8 * 256 + X/8 * 2
Obviously we have to divide both X and Y with 8 because each character is 8x8 pixels in size.
Y has to be them multiplied by 256 because each line of text takes 256 bytes in memory.
X has to be multiplied by 2 because each character is followed by attribute byte defining it's color.
We also have to be aware that X and Y are 2 byte or 16 bit values and very importantly that BASIC does all calculations internally in floating point so we have to make sure to cut away any fractions when dividing X and Y positions using INT() function.
BASIC | C | Assembly |
---|---|---|
40 M=INT(TY/8)*256+INT(TX/8)*2 | m = ((info.pos.y & 0xff8) << 5 ) | (( info.pos.x & 0xff8 ) >> 2); | ; Divide X by 4 and use as LOW BYTE lsr 3 ror 2 lsr 3 ror 2 lda 2 and #$FE sta VERA_LOW ; Divide Y by 8 and use as MID BYTE lsr 5 ror 4 lsr 5 ror 4 lsr 5 ror 4 lda 4 sta VERA_MID |
The formula used in BASIC is essentially the same as we defined in the description above. We do have to use INT() function to get rid of remainder of the division with 8 though. |
In C we could use similar formula to the one in BASIC but we can do it also using bitwise operations that if properly optimized should be faster than division and multiplication. | As expected the Assembly version has most code but in my opinion is the most elegant one. Since we are already dealing with integers we can just use bitwise operations lsr and ror - to divide by two. X has to be divided by 4 (/8*2) to get the Low byte for VERA so we divide by 2 twice. Y has to be divided by 8 to get the Middle byte for VERA so we divide it by 2 three times meaning shift bits by tri bits. Because we are using the value as Middle byte it is automatically multiplied by 256. This is very clean and fast solution. |
Draw
After all the preparations and calculations we are finally ready to draw to the screen. The only other thing remaining is to implement IF statement to differentiate between left and right button to draw or delete the character.
BASIC | C | Assembly |
---|---|---|
50 IF TB=1 THEN VPOKE 0,M,160 60 IF TB=2 THEN VPOKE 0,M,32 |
if (info.buttons & MOUSE_BTN_LEFT) vpoke(160,m); else if (info.buttons & MOUSE_BTN_RIGHT) vpoke(32,m); |
lda #32 ldx 6 cpx #1 bne :+ lda #160 : sta VERA_DATA0 |
The code is self explanatory. If left button is pressed draw PETSCII screen code 160. If right button is pressed then draw PETSCII screen code for space. |
In C we also have vpoke function which is unique when building for Commander X16 platform. Alternatively we could write directly to VERA like in Assembly but in this instance using vpoke is appropriate. |
Assembly solution is again very simple and elegant. We load register A with default value and then check the buttons value in ZP location 6. If right button was pressed replace value in A with 160 and send to VERA. |
Build
Source code for each of the examples listed in full below and can be built using cc65 compiler using options below. If you just want to use binary feel free to download it from the links here:
BASIC | C | Assembly |
---|---|---|
cl65 -t cx16 Cdraw.c -o CDRAW.PRG | cl65 -t cx16 Adraw.asm -o ADRAW.PRG | |
Tokenized BASIC file download BDRAW.BAS | Compiled C file CDRAW.PRG | Compiled Assembly file ADRAW.PRG |
Complete Source Code
BASIC
1 REM ==== BASIC DEMO OF BLOCK DRAW ==== | |
2 REM SYSTEM: COMMANDER X16 | |
3 REM VERSION: EMULATOR R.38+ | |
4 REM AUTHOR: DUSAN STRAKL | |
5 REM DATE: DECEMBER 2020 | |
6 REM | |
7 REM IMPLEMENTATION OF BLOCK (CHARACTER) DRAW ON DEFAULT SCREEN IN BASIC | |
8 REM FOR COMPARISON TO EQUIVALENT C AND ASSEMBLY PROGRAMS. | |
10 MOUSE 1 | |
20 TX=MX:TY=MY:TB=MB | |
30 IF TB=0 THEN GOTO 20 | |
40 M=INT(TY/8)*256+INT(TX/8)*2 | |
50 IF TB=1 THEN VPOKE 0,M,160 | |
60 IF TB=2 THEN VPOKE 0,M,32 | |
70 GOTO 20 |
C
/* C Demo of Block Draw | |
System: Commander X16 | |
Version: Emulator R.38+ | |
Author: Dusan Strakl | |
Date: December 2020 | |
Compiler: CC65 | |
Build using: cl65 -t cx16 Cdraw.c -o CDRAW.PRG | |
Implementation of Block (Character) Drawing on default screen in Assembly for comparison | |
to equvalent Assembly and BASIC programs. | |
*/ | |
#include <cx16.h> | |
#include <mouse.h> | |
void main(void) { | |
struct mouse_info info; | |
static unsigned int m; | |
mouse_load_driver (&mouse_def_callbacks, mouse_stddrv); | |
mouse_install (&mouse_def_callbacks, mouse_static_stddrv); | |
mouse_show (); | |
while (1) { | |
mouse_info (&info); | |
m = (( info.pos.y & 0xff8 ) << 5 ) | (( info.pos.x & 0xff8 ) >> 2 ); | |
if (info.buttons & MOUSE_BTN_LEFT) | |
vpoke(160, m ); | |
else if (info.buttons & MOUSE_BTN_RIGHT) | |
vpoke(32, m ); | |
} | |
} |
Assembly
; Assembly Demo of Block Draw | |
; System: Commander X16 | |
; Version: Emulator R.38+ | |
; Author: Dusan Strakl | |
; Date: December 2020 | |
; Compiler: CC65 | |
; Build using: cl65 -t cx16 Adraw.asm -o ADRAW.PRG | |
; | |
; Implementation of Block (Character) Drawing on default screen in Assembly for comparison | |
; to equvalent C and BASIC programs. | |
.org $080D | |
.segment "STARTUP" | |
.segment "INIT" | |
.segment "ONCE" | |
.segment "CODE" | |
; I/O Registers | |
VERA_LOW = $9F20 | |
VERA_MID = $9F21 | |
VERA_HIGH = $9F22 | |
VERA_DATA0 = $9F23 | |
VERA_CTRL = $9F25 | |
; Mouse Kernal routines | |
MOUSE_CONF = $FF68 | |
MOUSE_GET = $FF6B | |
;****************************************************************************** | |
; MAIN PROGRAM | |
;****************************************************************************** | |
main: | |
lda #1 | |
ldx #0 | |
jsr MOUSE_CONF | |
loop: | |
ldx #2 | |
jsr MOUSE_GET | |
and #3 | |
beq loop | |
sta 6 ; store for later use | |
; set VERA registers | |
stz VERA_CTRL | |
stz VERA_HIGH | |
; Divide X by 4 and use as LOW BYTE | |
lsr 3 | |
ror 2 | |
lsr 3 | |
ror 2 | |
lda 2 | |
and #$FE | |
sta VERA_LOW | |
; Divide Y by 8 and use as MID BYTE (this only works if layer width=128 tiles) | |
lsr 5 | |
ror 4 | |
lsr 5 | |
ror 4 | |
lsr 5 | |
ror 4 | |
lda 4 | |
sta VERA_MID | |
lda #32 | |
ldx 6 | |
cpx #1 | |
bne :+ | |
lda #160 | |
: sta VERA_DATA0 | |
jmp loop | |
rts |
Shouldn't MOUSE_CONF be $ff68?
ReplyDeleteYou are correct Felipe, good catch. It was correct in the source code but in the snippet in the beginning I had a typo. I fixed it already.
Delete