Thornton 2 dot Com
1K61I

GEOS Operating

Ariel's GEOS Programmer's Reference Guide
Back: Preface Up: Contents Next: GEOS Kernal Routines


GEOS Development
Back: GEOS: Environment (WIP) Up: GEOS Development Next: GEOS: System (TODO)

This page is still a work in progress.

Table of Contents

The features GEOS exposes to your program include a dialog box system that is almost like a miniature application, a process library for periodic events, a library of arithmetic routines, and a library for standardized access to GEOS-specific features of the filesystem.

GEOS Processes

In GEOS, a process is a service routine for regular periodic events.

BSW called them processes, and Boyce and Zimmerman called them recurring timed events. Boyce and Zimmerman also remarked that this is the beginning of multitasking, but BSW cautioned against this interpretation: GEOS doesn't provide any sort of mechanism for the process context switching required for a multitasking OS or application, even for primitive cooperative multitasking.

A process in the GEOS process table has two parts: a service routine vector and a timer. The vector simply points to your process service routine, and the timer tells GEOS how many Interrupt Levels must occur between each run of the process. On NTSC computers, the timer value is in 1/60ths of a second, and on PAL computers, the timer value is in 1/50ths of a second. On both, the timer value is the number of vertical blank periods of the display.

A process can be in one of four states: running, blocked, frozen, and both blocked and frozen. A running process will run every time its timer times out. A blocked process won't run, but its timer will keep counting down and timing out. A frozen process has its timer suspended, also ensuring it won't run. When a blocked process is unblocked, GEOS will run it when the timer times out or immediately if it timed out while blocked. When a frozen process is unfrozen, its timer will resume, and GEOS won't run it immediately, even if more time than its timer value passed while frozen.

Although the timer is decremented on each Interrupt Level, the process polling is done during Main Loop. When your process is called, it is called as a subroutine of MainLoop, and it should end with an rts instruction.

GEOS can manage up to 20 processes in its process table. There's no constant for it in geosSym, but geoProgrammer 128 seems to have a MAX_PROCESSES constant for it. Additionally, Boyce and Zimmerman note internal use only GEOS variables between $86f1 and $87ce that are used for processes, reserving space for up to 20 processes.

If your program wants to use processes, you give GEOS a headerless table of your processes:

Process Table Structure
Group Group Size Offset Size Purpose
Process 0 4 bytes 0 word Vector to process 0's service routine.
2 word Timer 0 value: Interrupt Levels between each run.
Process 1 4 bytes 4 word Vector to process 1's service routine.
6 word Timer 1 value.
Process N 4 bytes (N * 4) Processes 1 to N are identical in structure to process 0.

Your table needs only as many entries as processes: as few as one, as many as 20. Your process table is installed by calling InitProcesses (note the plural) with r0 pointing to the table and A holding the number of processes in it. Note that GEOS will copy your process table to its own working area and leave yours alone.

;Example process table, installation, and initiation

C64=TRUE           ; Defined because these symbols aren't in geosSym
.if C64            ; but do seem to be in geoProgrammer 128
PSIZE=4
MAX_PROCESSES=20
FRAME_RATE=60      ;(based on NTSC flag; change to 50 if PAL)
.endif

; Define process table

ProcTab:

; Mouse check: Check mouse coordinates, change shape to match region
MOUSECHECK = (*-ProcTab)/PSIZE  ; process number 0
       .word  CheckMouse        ; proc 0 service routine
       .word  10                ; proc 0 timer: every 10 IRQs

; RTC: Update a real time clock
RTCLOCK    = (*-ProcTab)/PSIZE  ; process number 1
       .word  Tick              ; proc 1 service routine
       .word  FRAME_RATE        ; proc 1 timer: once a second

; Screen saver: Avoid CRT burn-in by turning off colors after 5 minutes
SCRNSAVER  = (*-ProcTab)/PSIZE  ; process number 2
       .word  ScreenSave        ; proc 2 service routine (we'd also want to
                                ; make an UnScrnSave routine to fix colors)
       .word  FRAME_RATE*60*5   ; proc 2 timer: every 5 minutes
                                ; (300 seconds, or 18,000 IRQs (NTSC))
                                ; (300 seconds, or 15,000 IRQs (PAL))

NUM_PROC   = (*-ProcTab)/PSIZE  ; 3 processes

.if (NUM_PROC > MAX_PROCESSES)
.echo Warning: Too many processes in ProcTab
.endif

; Install process table

       LoadW  r0,#ProcTab       ; table pointer
       lda    #NUM_PROC         ; number of processes in table
       jsr    InitProcesses     ; install

; Activate initially-frozen processes

       ldx    #NUM_PROC-1       ; for each process N-1 to 0
10$:
       jsr    RestartProcess    ; reset timer, unblock, unfreeze
       dex
       bpl    10$               ; next

Process Management

When you install a process table, GEOS places all the processes in a frozen state. In order to start the installed processes, you have to iterate over each process number and initialize it, as at the end of the example code above. The GEOS Kernal routine for initializing or reinitializing a process is RestartProcess.

If you have a process that, as in the example above, acts as a screen saver timeout, then you can use RestartProcess to reset and restart the countdown every time your program senses user activity. Continuing the example above, you would do so by starting relevant service routines with:

MyServiceRoutine:
       jsr   UnScrnSave         ; Did we blank the screen already?
       ;service routine code here
       rts

;elsewhere in the code...

UnScrnSave:
       ;user did something, so don't blank screen for another 5 min.
       ldx   #SCRNSAVER         ; Screen saver timeout process number
       jsr   RestartProcess     ; Reset it
       ;code to check for and undo screen saver activation here
       rts

GEOS provides two routines related to frozen processes: FreezeProcess, which halts its countdown timer at the current value, thus ensuring that the process never runs, and UnfreezeProcess, which resumes its countdown timer from the frozen value.

GEOS also provides two routines related to blocked processes: BlockProcess, which prevents the process from running when its countdown timer times out, and UnblockProcess, which allows the process to run at timeout again. The differences between freezing and blocking a process are, first, that a blocked process's timer keeps counting down and timing out like normal, and second, if the timer timed out at least once while the process was blocked, GEOS will call the process's service routine as soon as it gets the chance regardless of the timer's current value.

GEOS polls its process table in both of its main contexts, Main Loop and Interrupt Level. During Interrupt Level, GEOS polls its process table, checks if a process is frozen, and decrements its timer if not. When the timer reaches 0, GEOS sets a "runable" flag on the process and resets the timer back to its initial value to count down again. During Main Loop, GEOS polls its process table, checking if a process is both runable and unblocked. When a process is both runable and unblocked, GEOS clears its runable flag and calls its service routine. Interrupt Level leaves the process timer alone when the process is frozen, and Main Loop leaves the process runable flag alone when the process is blocked.

The runable flag on a process is simply an on-off flag, not a queue or counter of how many times a process becomes runable. This means that, if a process timer times out while blocked, then no matter how many times it times out during the block, GEOS calls its service routine only once when the process is unblocked.

If you need to disable a process completely while guaranteeing it won't run the instant you reenable it, then the best way is most likely to both freeze and block the process, then unblock and unfreeze the process again when you no longer need it disabled.

If you need to activate a process immediately regardless of its current timer value, you can use the EnableProcess routine. It sets the runable flag on the process, which Main Loop will see on its next poll of its process table. GEOS will then call the process's service routine no matter what its timer value is, even if the process is frozen. However, GEOS will not call the process's service routine if it's blocked; like the timer actually running out, it won't get called until it's unblocked.

There may be scenarios in which you don't want your process to do what it normally does. In the example above, you may not want the CheckMouse process to change the mouse pointer's shape while a menu is open and the mouse is navigating through its submenus and options. In this example, your process would need to check menuNumber before doing what it normally does:

CheckMouse:
       lda   menuNumber    ;check menu level
       bne   90$           ;submenu open?  do nothing.
       jsr   DoCheckMouse  ;else, change mouse shape depending on region
90$:
       rts

Processes and Interrupts

Every vertical blank period of the display, an IRQ is generated, and GEOS switches to Interrupt Level. That's the theory, but the reality is that, because GEOS depends on the IRQ signal, Interrupt Level won't run while the CPU's interrupt disable flag is on. What this means for processes is that, if interrupts are disabled when the vertical blank IRQ is generated, then GEOS will miss the Interrupt Level, and no process timers will be decremented until the next IRQ.

Normally, this isn't an issue, but this could become one if you don't account for two possibilities. First, if your program has sei instructions in it, there's the possibility, however slim, that it'll run just a few CPU cycles before the IRQ, and the timing of your processes will be thrown off by 16.7 ms (NTSC) or 20.0 ms (PAL). Second, some functions like disk I/O disable IRQs for very long periods of time, up to whole seconds, which will throw off process timings significantly.

Another problem could arise for processes with very low timers. If the timer value is too low, then Interrupt Level could time the process out and flag it as runable more than once per Main Loop polling cycle, making it impossible for your process service routine to run on every timeout.

Also, it's impossible to guarantee a precisely synchronized relationship between processes. An IRQ could be received in the midst of freezing or unfreesing synchronized processes, causing GEOS to decrement timers for less than all of them. Or Interrupt Level could make a process early in the process table runable while Main Loop is calling and waiting for the service routine for a process late in the process table to finish, in which case GEOS won't call the earlier's service routine until the next time through Main Loop.

Letting Your Routines Sleep

GEOS has a routine for letting a routine do nothing for a fixed period of time without halting the system: Sleep. When your routine calls Sleep, GEOS saves your routine to call again later, picking up from the instruction just after the jsr Sleep instruction. Sleep is like a temporary, one-use process on the fly for your program.

However, there's a significant price for your routine sleeping. No matter what your routine's parent routine was before the Sleep call, Main Loop is the parent routine after waking up. Also, while your routine is sleeping, GEOS is processing events and calling service routines, activities which change the CPU flags, CPU registers, the stack, and GEOS pseudo-registers all to unknown states on return to your process. After sleeping, your routine wakes up to a changed world.

Before calling Sleep, your routine must save temporary values in its own variable space, pulling any values pushed to the stack and copying out any values stored in GEOS-provided pseudo-register variables.

GEOS Mathematics Routines

GEOS has several math routines for doing multiplication, division, and bit shifting with 8-bit and 16-bit integers. Most of the routines are called by passing the math parameter addresses instead of their values, and instead of loading specific pseudo-registers in advance. For example, to divide two 16-bit unsigned integers that happen to be in a1 and a4, and you happen to want (a1 = a1 / a4), the call would look like this:

ldx  #a1      ; address, not value, of dividend word
ldy  #a4      ; address, not value, of divisor word
              ; (note the Immediate addressing mode)
jsr  Ddiv     ; unsigned 16-bit division
              ; a1, which held dividend, now holds quotient
              ; (r8 holds remainder)

GEOS calls 8-bit math "single-precision" and 16-bit math "double-precision."

Addition and subtraction with integers works the same for both signed and unsigned integers, but multiplication and division need to treat the sign specially.

In addition to math routines, geoProgrammer has math macros to automate a lot of 16-bit arithmetic algorithms. The Hitchhiker's Guide to GEOS also offers macro definitions that aren't in geoProgrammer.

Signed and Unsigned Integers

In the 6502, a signed integer's sign is stored in bit 7, and negative integers are expressed as the two's-complement form of its absolute value. To get the two's complement form, you first have to get the one's-complement form, then add 1. The one's-complement form of a binary integer is simply subtracting each bit from 1, or inverting each bit.

For example, the absolute value of the integer -6 is expressed in 8-bit binary as %00000110 ($06, 6). Getting the one's-complement of 6 consists of subtracting each bit from 1, or inverting every bit, giving %11111001 ($f9, 249). Getting the two's-complement form is just adding 1 to the one's-complement form, so %11111001 becomes %11111010 ($fa, 250). Thus, the integer -6 is expressed in 8-bit binary as %11111010, the form in which adding 6 would give 0, just like decimal arithmetic, albeit with a discarded carry into the nonexistent bit 8 in two's-complement form.

Subtracting two integers is basically adding the minuend to the sign-inverted form of the subtrahend. For example, the expression 3 - 9 is the same as 3 + (-9). Thus, in binary, it's 3 (%00000011) plus the two's-complement of -9 (%11110111). Adding them gives the two's-complement of -6 (%11111010). For the reverse, 9 - 3, which gives positive 6, it's the same as 9 + (-3), which in binary is 9 (%00001001) plus the two's-complement of -3 (%11111101), which when added gives 6 (%00000110) and a carry that can be discarded.

Note that when a negative integer is in two's-complement form, the high bit is set, indicating a negative number.

The representation of negative integers as the two's-complement form of their absolute values allows the 6502 to use the same logic for both addition and subtraction.

In the 6502, in additions, the carry flag needs to be cleared with clc before adding the first byte, in order for adc to add without a carry. In subtractions, the carry flag becomes a not-borrow flag, and so it needs to be set with sec before subtracting the first byte, in order for sbc to subtract without a borrow.

The GEOS Kernal routines and geoProgrammer macros for math extend 8-bit arithmetic to 16 bits. In 16-bit form, the sign of signed integers is in bit 15, which is bit 7 of the high byte.

Unsigned 8-bit integers can have values between 0 and 255. Signed 8-bit integers can have values between -128 and 127. Unsigned 16-bit integers can have values between 0 and 65,535. Signed 16-bit integers can have values between -32,768 and 32,767. If the result of an operation exceeds below the minimum value or above the maximum value, the operation overflows and the result wraps around. For example, adding 3 to the unsigned 8-bit value 255 gives 2 (not 258), and adding 3 to the signed 8-bit value 127 gives -126 (not 130, because 130 [%10000010] is the two's-complement form of -126).

In GEOS, the addition and subtraction of signed integers is the same as addition and subtraction of unsigned integers. However, GEOS treats multiplication and division of signed integers differently from unsigned.

The GEOS Kernal has two routines dealing with signed integers:

Routine Summary
Dabs Gives the absolute value of a 16-bit integer.
Dnegate Gives the sign-inverted form of a 16-bit integer.

Incrementing Integers

The 6502 has three instructions for incrementing integers:

Instruction Summary
inc Increment an 8-bit integer in memory.
inx Increment an 8-bit integer in X.
iny Increment an 8-bit integer in Y.

The GEOS Kernal doesn't have any routines for incrementing integers. However, BSW provided a macro for incrementing integers:

Macro Summary
IncW Increment a 16-bit integer.

Decrementing Integers

The 6502 has three instructions for decrementing integers:

Instruction Summary
dec Decrement an 8-bit integer in memory.
dex Decrement an 8-bit integer in X.
dey Decrement an 8-bit integer in Y.

The GEOS Kernal has one routine for decrementing integers:

Routine Summary
Ddec Decrement a 16-bit integer.

BSW also provided two macros for decrementing integers:

Macro Summary
DecW Decrement a 16-bit integer.
DecW2 Decrement a 16-bit integer faster.

Adding Integers

The 6502 has one instruction for adding integers:

Instruction Summary
adc Add an 8-bit integer and C to A.

BSW also provided five macros in geosMac for adding integers:

Macro Summary
add Add an 8-bit integer to A.
AddB Add two 8-bit integers.
AddW Add two 16-bit integers.
AddVB Add an 8-bit integer to another in memory.
AddVW Add an 8-bit integer to a 16-bit integer in memory.

Subtracting Integers

The 6502 has one instruction for subtracting integers:

Instruction Summary
sbc Subtract an 8-bit integer and !C from A.

BSW also provided three macros in geosMac for subtracting integers:

Macro Summary
sub Subtract an 8-bit integer from A.
SubB Subtract an 8-bit integer from another in memory.
SubW Subtract a 16-bit integer from another in memory.

Arithmetic Shifting

The 6502 has two instructions for arithmetic shifting of integers, effectively multiplying or dividing by a power of two:

Instruction Summary
asl Shift an 8-bit integer left.
lsr Shift an 8-bit integer right.

The GEOS Kernal has two routines for arithmetic shifting of integers:

Routine Summary
DShiftLeft Shift a 16-bit integer left.
DShiftRight Shift a 16-bit integer right.

Multiplying Integers

The GEOS Kernal has three routines for multiplying unsigned integers:

Routine Summary
BBMult Multiply two unsigned 8-bit integers, giving a 16-bit product.
BMult Multiply an unsigned 16-bit integer by an unsigned 8-bit integer, giving a 16-bit product.
DMult Multiply two unsigned 16-bit integers, giving an unsigned 16-bit product.

GEOS doesn't have any routines for multiplying signed integers. The Hitchhiker's Guide to GEOS offers a routine that multiplies two signed integers:

;***********************************************************************
;DSmult       double-precision signed multiply.
;
;pass:        x - zpage address of multiplicand
;             y - zpage address of multiplier
;
;returns:     signed result in address pointed to by x
;             word pointed to by y is absolute-value of the
;                    multiplier passed
;             x, y unchanged
;
;Strategy:
;      Establish the sign of the result: if the signs of the
;      multiplicand and the multiplier are different, then the result
;      is negative; otherwise, the result is positive. Make both the
;      multiplicand and multiplier positive, do unsigned
;      multiplication on those, then adjust the sign of the result
;      to reflect the signs of the original numbers.
;
;destroys:    a, r6 - r8                                 (mgl)
;***********************************************************************
DSmult:

       lda    zpage+1,x           ;get sign of multiplicand (hi-byte)
       eor    zpage+1,y           ;and compare with sign of multiplier
       php                        ;save the result for when we come back
       jsr    Dabs                ;multiplicand = abs(multiplicand)
       stx    r6L                 ;save multiplicand index
       tya                        ;put multiplier index into x
       tax                        ;for call to Dabs
       jsr    Dabs                ;multiplier = abs(multiplier)
       ldx    r6L                 ;restore multiplier index
       jsr    Dmult               ;do multiplication as if unsigned
       plp                        ;get back sign of result
       bpl    90$                 ;ignore sign-change if result positive
       jsr    Dnegate             ;otherwise, make the result negative
90$:
       rts

Dividing Integers

The GEOS Kernal has two routine for dividing integers:

Routine Summary
Ddiv Divide an unsigned 16-bit integer by another, giving an unsigned 16-bit quotient.
DSdiv Divide a signed 16-bit integer by another, giving a signed 16-bit quotient.

GEOS doesn't check for division by zero. If you attempt to divide by zero, then the operation will succeed without error, but a wrong answer will be returned. The Hitchhiker's Guide to GEOS offers a pair of routines that wrap the Kernal routines in divide-by-zero error detection (with errors in the original text corrected):

;***********************************************************************
;NewDdiv      -- Ddiv with divide-by-zero error checking
;NewDSdiv     -- DSdiv with divide-by-zero error checking
;
;Pass:        x      zp address of dividend
;             y      zp address of divisor
;
;Returns      x,y    unchanged
;             zp,x   result
;             r8     remainder
;             a      $00    -- no error
;                    $ff    -- divide by zero error
;             st     set to reflect error code in accumulator
;
;Destroys     r9
;
;***********************************************************************
;Set Apple=FALSE for GEOS and GEOS 128, set Apple=TRUE for Apple GEOS
;
DIVIDE_BY_ZERO = $ff

NewDdiv:
       lda    zpage,y             ;get low byte of divisor
       ora    zpage+1,y           ;and high byte of divisor
       bne    10$                 ;if either is non-zero, go divide
       lda    #DIVIDE_BY_ZERO     ;return error
       rts                        ;exit
10$:
       jsr    Ddiv                ;divide
       lda    #$00                ;and return no error
       rts

NewDSdiv:
       lda    zpage,y             ;get low byte of divisor
       ora    zpage+1,y           ;and high byte of divisor
       bne    10$                 ;if either is non-zero, go divide
       lda    #DIVIDE_BY_ZERO     ;return error
       rts                        ;exit
10$:
.if Apple
       stx    Xsave               ;save x because Apple destroys it
.endif
       jsr    DSdiv               ;divide
.if Apple
       ldx    Xsave               ;restore x register ageos destroyed
.endif
       lda    #$00                ;and return no error
       rts
.if Apple
       .ramsect
       Xsave  .block 1            ;temp x save variable for ageos
       .psect
.endif

Comparing Integers

The 6502 has three instructions for comparing the equality of two integers:

Instruction Summary
cmp Compare an 8-bit integer in A with one in memory.
cpx Compare an 8-bit integer in X with one in memory.
cpy Compare an 8-bit integer in Y with one in memory.

BSW also provided four macros in geosMac for comparing integers:

Macro Summary
CmpB Compare two 8-bit integers.
CmpBI Compare two 8-bit integers (one immediate).
CmpW Compare two 16-bit integers.
CmpWI Compare two 16-bit integers (one immediate).