![]() |
Commodore GEOS Programming Tips(last updated 2020-10-20) |
![]() |
This is a collection of GEOS programming tips 'n' tricks that I've collected over the years. I continue to learn this fascinating subject, and new tips are being added as I come across things that I think might be helpful to others.
Assembling and Linking GEOS Applications
General Programming Tips
API usage tips
geoDebugger commands
geoDebugger macros
geoProgrammer Quirks
Compute!'s Gazette geoProgrammer example
Don't use expressions to get the length of a data area within an instruction. For example:
lda #bufEnd-buffer ... buffer: .block 128 bufEnd:The assembler may generate incorrect object code on subsequent instructions if you do. Instead, do it like this:
lda #bufLen ... buffer: .block 128 bufEnd: bufLen = bufEnd-buffer
If your program is made up of several source files and you are including symbol files and macros, take care not to pass symbols to the linker more than once; it fills up the symbol table needlessly. If you have a large project and you don't do this, you'll find that as you go further and further into your VLIR modules with the debugger, you'll see fewer and fewer symbols. Another good idea is to create your own symbol file with only the ones you need rather than including geosSym.
The includes for your first source file should look something like this:
.if Pass1 .include shadowSym .include shadowMac .endif
All subsequent source files (including VLIR modules, even if they are made up of a single source file) should have their includes declared like this:
.if Pass1 .noeqin .noglbl .include shadowSym .include shadowMac .glbl .eqin .endif
To create the icon for an application (24x21 pixels), you can
paste it directly into the source code for the application's header
module; this will work if it you cut it at 3x3 cards in geoPaint
(24x24 pixels). geoPaint will not copy any finer than on card
boundaries. On the other hand, if you are creating a new file
programmatically and building a header template in code (to pass
to SaveFile
in r9), that will not work and you have to
convert the icon to .byte
directives and enter it by
hand.
When designing your own icons with text labels, if you want to make them look like the system icons, give them a size of 6 cards (48 pixels) wide by 16 pixels high, and use University 12pt bold font; the icon border should have a one-pixel shadow. You can line them up in geoPaint by setting alternate 8x8 cards to a white background in a checkerboard pattern. This makes copying them to photo scraps easy, but there's no good way to type text into them in geoPaint. Your best bet is to add the text near the icon but outside it, then use pixel edit mode to manually create each character. It's a lot of work, but it gives you the best-looking icons.
Both DoIcons
and DoMenu
have support for
setting the mouse position when they are called. DoIcons
has
X and Y mouse position coordinates within the icon table,
and DoMenu
takes a value in .A indicating which menu item the
mouse should be positioned over after calling it
(zero-based). However, repositioning the mouse can be disabled by
supplying zeroes for the mouse position in the icon table, while there
is no such ability in DoMenu
. The DoMenu
API
description in the Hitchhiker's Guide to GEOS provides a code example
for a workaround: disable interrupts, push the mouse position onto the
stack, call DoMenu
with 0 in .A, then restore the mouse
position and the interrupt status. Works great!
If you're modifying menus while they're active (changing the
values of pointers in the menu table), you don't have to
call DoMenu
again to make the changes visible. For example,
you may want to disable a menu item by changing the text pointer to
reference italicized text and replace the dispatch routine with
an rts
. However, this only works if the menu is not currently
visible. If you're changing the text of a menu item while it's pulled
down, you need to call ReDoMenu
.
If you're checking for a double-click, you'll find the symbol
for where the countdown value is stored in geosSym
(dblClickCount
), but not the constant for the default value
(CLICK_COUNT
). The correct declaration is
CLICK_COUNT=30For more information on detecting double-clicks in various situations, see "MainLoop and Icon Event Handlers" in the "Icons, Menus, and Other Mouse Presses" section of the Hitchhiker's Guide to GEOS (it's on page 5 of that section). Read the "Other Mouse Presses" section on page 18 if you're using
otherPressVector
. Oh, and if you're using otherPressVector
, make sure to disable interrupts while you're changing it:php sei LoadW otherPressVector,myCode plp
GEOS maintains date and time variables, so if the machine's time is set correctly, you can read them easily. These are the official equates:
year==$8516 month==$8517 day==$8518 hour==$8519 minutes==$851a seconds==$851bNote that
minutes
and seconds
are
plural.
GEOS uses the time-of-day clock on CIA #1 to store hour, minutes, and seconds. So if you want to set the time, you need to set the above variables as well as setting the TOD clock for hour, minutes, and seconds. Note that writing to the hours register stops the clock until you write to the tenths-of-second register! Here's some sample code from my UltimateTime autoexec:
jsr enableIO php sei lda $dc0f ;CIA #1 and #$7f ;set time, not alarm sta $dc0f MoveB cHour,$dc0b ;this stops the clock! MoveB cMinutes,$dc0a MoveB cSeconds,$dc09 MoveB 0,$dc08 ;this restarts the clock plp jsr restoreIOConsult a reference like the Abacus Advanced Machine Language Book for the format these registers expect.
You can tell whether an autoexec is running
during bootup or as the result of the user double-clicking its icon by
checking the value of firstBoot
($88C5, it's listed in
geosSym). The only documentation I found for it is in the variable
reference at the end of the Hitchhiker's Guide to GEOS, where it says
"This flag is changed fron 0 to $FF when the deskTop comes up after
booting." I wrote a test program to verify this, which you can find
source and executable for on this D64
image. You can also view the source
as ASCII plaintext.
InitForIO
is not re-entrant. It saves state,
which is restored by DoneWithIO
. What this means is that if
you call InitForIO
a second time without having
called DoneWithIO
in between, the saved state is overwritten
and you can't return to it. When you do call DoneWithIO
, the
kernel will not be banked back in and the machine will crash as soon
as any GEOS APIs are called.
InitForIO
is quite expensive, since its
main purpose is to set up for disk access with the GEOS fastloader
running (in fact, it resides within the disk driver, as the first jump
table entry). If you want to do any I/O on the IEC bus, you need to
call it, but if all you want to do is bank in the I/O chips (e.g. for
accessing the SID registers), you might want to try something like
this:enableIO: php sei lda $01 sta ioSave ;save memory configuration and #$f8 ora #$05 ;bank in I/O sta $01 plp rts restoreIO: php sei lda ioSave ;restore previous configuration sta $01 plp rts ioSave: .block 1Note that these routines are also not re-entrant!
One other
thing to consider: InitForIO
trashes both the accumulator and
the Y register (although X is preserved), while the above only trashes
the accumulator.
GetString
keeps running during MainLoop
,
until the user hits Return. If you need to terminate string input
manually (e.g. as part of a menu or icon handler), just use this code,
which essentially fakes a keypress:
LoadB keyData,#$0d lda keyVector ldx keyVector+1 jsr CallRoutine
Since GetString
keeps running
during MainLoop
, you may have a problem if you
call PutString
at the same time, because the two APIs share
some common system variables. I encountered this when a process
handler called PutString
while GetString
was
running; this is the code I use to save and restore those
variables:
PushB $87cf ;stringLen PushB $87d0 ;stringMaxLen PushB alphaFlag ;save cursor state PushW leftMargin PushW rightMargin PushW StringFaultVec ... jsr PutString ... PopW StringFaultVec PopW rightMargin PopW leftMargin PopB alphaFlag PopB $87d0 PopB $87cf
Be careful about adjusting windowTop
(which sets the
upper bounds for text clipping). This limit is considered when menus
are drawn, so if you set it lower than your menus, their text won't be
redrawn if the user drops one down and then faults (moves his mouse
off the menu). Set it only when needed, and restore it afterward.
The section on "Text, Fonts, and Keyboard Input" in the
Hitchhiker's Guide is pure gold for learning advanced
techniques... but there's a coding error in the section on
clipping. On page 12 of this section, there's a string fault routine
that is supposed to avoid the bug where on a right margin
fault, PutString
keeps trying to find a character that will
fit. The routine is supposed to advance the pointer to the null byte
at the end of the string; unfortunately, there's an off-by-one error,
and it will leave the pointer on the last valid character instead. Try
this:
PutStrFault: ldy #0 10$ inc r0L ;advance to next character bne 20$ inc r0H 20$ lda (r0),y ;read it bne 10$ ;end of string? rts ;yes, we've faked out PutString
Be aware that the VLIR record handling APIs change the
positions of other records! AppendRecord
inserts an empty
record after the current record and moves the rest down
one; InsertRecord
does the same but inserts the record before
the current record. DeleteRecord
deletes the current record
and moves all following records up one. You can delete a record and
leave a "hole" in the records by calling WriteRecord
with a
length of zero (see the Hitchhiker's Guide to GEOS).
When creating a VLIR file, you may want to initialize every
record so that you can just use WriteRecord
later (without
the reordering that AppendRecord
and InsertRecord
cause). Just call AppendRecord
on the newly-created file in a
loop until it returns OUT_OF_RECORDS
(9) in the X
register. What this does is change the index pointer of each record
from $00,$00 (never used) to $00,$ff (empty).
SaveFile
to create a new
file, you need to check first to make sure a file by that name doesn't
already exist. Otherwise, the file will be written anyway and you will
end up with two files with the same name on the same disk! This seems
like an egregious bug,
so caveat
utilitor.
DoDlgBox:
If you're using DBGETSTRING
in a
dialog box, don't use an OK icon. The string won't get null-terminated
until the user hits Return, so if he types a large number of
characters and then backspaces over some of them, they will all be
returned as part of the buffer (this is why GEOS filename dialogs
don't have an OK icon). Check for
DBGETSTRING
in r0L when DoDlgBox
returns
instead.
DoDlgBox
and DB_USR_ROUT:
When you include a
user routine in your DB table, it is called in the order the commands
are listed in the table. This raises all sorts of interesting
possibilities for drawing portions of a dialog box. In fact, you can
use it to debug the dialog box as it's being drawn by setting a
breakpoint in the user routine: it will break into the debugger at
that point in the table, and you can use F7 to see the partially drawn
dialog box!
Don't use LdFile
. The BSW programmer's guide
says that the variables loadOpt
and and loadAddr
can
be used to control whether the file's load address is overridden, but
doesn't say where these variables live (geosSym doesn't either). Boyce
says $886B and $886C, but that didn't work for me, so I looked in the
Hitchhiker's Guide, which had this to say:
"All versions of LdFile
to date under Commodore GEOS are
unusable because the load variables... (loadOpt
and loadAddr
) are local to the Kernal and inaccessible to
applications. Fortunately this is not a problem because applications
can always go through GetFile
to achieve the same
effect." Caveat
utilitor.
In my experience, GetRandom
isn't very
random. I wrote these routines instead, which use the SID chip to
generate pseudo-random numbers (the
article Examination of SID noise waveform
was very helpful). Call primeRnd
during program
initialization, then use sidRnd
when you need a
pseudo-random number. The I/O chips are banked in and out using the
technique described above.
For example, to roll a 20-sider, pass 20 in r2; the result in r1 will be in the range 0-19.
;----------------------------------------------------------- ; Prime SID chip to generate random numbers. ;----------------------------------------------------------- primeRnd: jsr enableIO lda #0 sta $d40e ;voice 3 frequency low lda #$80 ;frequency to $8000 sta $d40f ;voice 3 frequency high sta $d412 ;noise waveform, gate off 3 jsr restoreIO rts ;----------------------------------------------------------- ; Pseudo-random number generator (uses SID chip). ; pass: r2, high limit (1-based) ; return: r1, pseudo-random number (0-based) ; destroyed: .A, .X, .Y, r1, r8, r9 ;----------------------------------------------------------- sidRnd: LoadW r1,65535 ;r2 loaded on entry ldx #r1 ldy #r2 jsr Ddiv MoveW r1,r2 ;r2 = 65535 / high limit jsr enableIO lda $d41b sta r1L ldx #7 ;delay at least 32 cycles 10$ dex bne 10$ lda $d41b sta r1H jsr restoreIO ldx #r1 ldy #r2 jsr Ddiv ;r1 = r1 / r2 rtsAnd remember, kids: as John von Neumann said, "Anyone who considers arithmetical methods of producing random digits is, of course, in a state of sin."
Here is the entire macro (or rather, macros), and here are some excerpts with comments:
if @(arg0)=80,print"type: SUB_MENU"[cr]This line expects an address and reads the byte at that address, using the
@
operator. The value is tested against $80 (hex is the default radix).
if @(arg0)=c0||(@(arg0)&3f)!=0,print @(arg0):"unknown type: "[cr]Another lookup. If only bits 6 and 7 are set, or if any of the low bits are set, print the value preceded by the heading "unknown type: ".
print @@(arg0):8b'"menu item text: "[cr]This one prints eight bytes in character format (
8b'
) with the
heading "menu item text: ", starting at the address
(@@
) located at the argument.
.macro showitem setu 1,arg0+(5*u.fn)[cr] menuitem u.1[cr] .endm .macro menuitms print @(arg0+6)&3f:."no. items: "[cr] setu 0,(@(arg0+6)&3f)-1[cr] for .0:u.0,showitem (arg0+7)[cr] .endmThese two macros show several features. In
menuitms
, we
set user register zero (u.0) with the setu
command. We
are setting it to the number of items in the menu (lower six bits at
offset six in the menu structure) minus one, to use as a counter. We
then call showitem
in a for loop. This macro was broken
out separately after getting the infamous nesting error. We pass the
first byte of the menu item list (offset seven); the called macro sets
user register one (u.1) to the beginning of the current menu item
structure using the loop counter u.fn
(menu item
structures are five bytes long), and calls the menu item display
macro.
Here's some sample output (note that the dispatch addresses are all the same for desk accessories):
>menu geosMenu top/bottom: .15 .99 left/right: .0 .100 type: VERTICAL|CONSTRAINED no. items: .6 menu item text: info.set type: MENU_ACTION dispatch: $0437 menu item text: text man type: MENU_ACTION dispatch: $0561 menu item text: photo ma type: MENU_ACTION dispatch: $0561 menu item text: ScreenPh type: MENU_ACTION dispatch: $0561 menu item text: ruler1.5 type: MENU_ACTION dispatch: $0561 menu item text: printIt1 type: MENU_ACTION dispatch: $0561 >I guess I could refine the macro that prints the menu item text...
Don't let your source files get too big. I once had an issue where all eight source files making up an app assembled correctly, but the linkage editor failed on an "Expression cannot be resolved" error. The error message then printed the offending line of source... which contained only the single character 'p'. My first thought was that I had bumped the keyboard and introduced a stray character in the source, but then it wouldn't have assembled. I went over the source file with a fine-toothed comb and didn't find anything, so I finally decided to split the file into two, and voilà! the linker was happy.
If you are writing a VLIR application, make sure the header
source matches the linkage directive file. If the link file says
.vlir
and the header says .byte SEQUENTIAL
, your
resident module will contain the VLIR index instead of its code.
Even though geoProgrammer 1.1 fixed a lot of bugs, it will still act squonky once in a while, especially with large projects. I once got a "symbol defined more than once" from the linker that was definitely not an error; I finally just looked up the symbol and replaced it with the absolute address on that line, and the error message went away. Sometimes you have to get a little creative.
One thing you can count on regardless of which geoProgrammer version you use is that the page and line numbers in an error listing will sometimes be wrong. This is partly because the assembler seems to count expanded macros as multiple lines of source, but you'll also see incorrect page numbers. For example, I once assembled a source file that had a branch out of range near the end of page 6; the error message pointed to page 7, line 84. I made a mistake when I fixed it, and on the next assembly, the error message said page 6, line 84. I've also seen an error message that said "page 4, line >2" (no, that's not a typo).
You may also get a "Branch out of range" error when there doesn't appear to be one; this usually happens in an area with a lot of local labels where global labels are far apart. Experienced GEOS programmers know that adding another global label amongst the locals will usually result in a clean assembly.
Ex
and ExHdr
, then run the linker, passing the
file ex.lnk
.CG-Example.dbg
to start geoDebugger.DoIcon1
, a key handler is set up with the
address of DoKey
. Have a quick look at that code by
typing a DoKey
and cursoring down
until you hit the call to jsr GraphicsString
, then
hit Return. We're looking at the expansion of the
macro LoadW r0,DrawBox
.sym DrawBox
; we see that the code is
passing the correct address to GraphicsString
.setb DoKey
, then
go
to start the program.DoKey
. So far, so good.t
(top-step). We're OK until we reach the call to jsr
GraphicsString
, but then get a
crash. Type rboot
to reboot GEOS, then restart
the debugger as above.GraphicsString
doesn't like the
data we're passing it. Let's examine it by using the
command d DrawBox
, which will give us a hex
hex dump of the table.GraphicsString
. Let's compare the data in the
table with what the API is expecting.05
means NEWPATTERN
, and it's followed
by a zero byte for the pattern. No problem there.56
... wait, that's not a valid
command! We have found the bug! Looking ahead in the source code,
we see a RECTANGLETO
command, which should be
preceded by a MOVEPENTO
command. Maybe that's all
that was missing, as the following bytes would then make sense
(56 00
and 5A
). Comparing the source
with the original in the Compute! article, we find that this is
indeed the case. Insert the missing line (.byte
MOVEPENTO
) in the Ex
file, reassemble
it, and rerun the linker as above. The bug is squashed!Not to be outdone, Bruce responded with an enhanced version of the program, which you can find here. Bruce scores the extra credit points (note his use of picW and picH after the icon graphic)! Now who else wants to join the fray? What else can you think of to make this program an even better example of using geoProgrammer?