The C64 OS Programmer's Guide is being written
This guide is being written and released and few chapters at a time. If a chapter seems to be empty, or if you click a chapter in the table of contents but it loads up Chapter 1, that's mostly likely because the chapter you've clicked doesn't exist yet.
Discussion of development topics are on-going about what to put in this guide. The discusssions are happening in the C64 OS Community Support Discord server, available to licensed C64 OS users.
C64 OS PROGRAMMER'S GUIDE
Chapter 4: Using the KERNAL → Service (module)
The C64 OS Interrupt Service Routine
The service KERNAL module is responsible for implementing the operating system's interrupt service routine (ISR). In addition, it provides a variety of miscellaneous routines that are central to almost every kind of C64 OS program: Application, Utility, driver, library, etc.
The interrupt service routine has two possible starting points:
KERNAL ROM Patched InIf the KERNAL ROM is patched in when the IRQ occurs, the ROM provides the CPU's IRQ vector at $FFFE/$FFFF which points to a routine elsewhere within the KERNAL ROM. This routine then jumps through the vector in workspace memory at $0314/$0315.
KERNAL ROM Patched OutIf the KERNAL ROM is patched out when the IRQ occurs, the CPU's IRQ vector is installed in RAM at $FFFE/$FFFF which points to a routine in the service module of the C64 OS KERNAL. It performs a similar task to the KERNAL ROM, but directs through a secondary vector in workspace memory at $0312/$0313.
The RAM-based ISR temporarily patches the KERNAL ROM back in, and patches it out again before returning from the interrupt. Because of how this is setup, without needing to mask IRQs, your code is always safe to patch out the KERNAL ROM. Both routines are carefully timed reconvene on the shared interrupt service routine in the same amount of time: 29 cycles exactly.
Order of Interrupt Service Routine
- Service the Graphics Library
- Poll Devices
- Service SID Music
- Update Jiffy Clock
- Scan the STOP Key
- Update Time
Each of these stages has a number of sub-stages.
Graphics LibraryWhen gfx.lib is loaded and linked, it installs itself in the ISR. The standard ISR calls the procgfx routine in the gfx.lib. If split screen is open, procgfx redirects the RAM and ROM IRQ vectors ($0312/$0313 and $0314/$0315 respectively) to point instead into the gfx.lib. On every frame, the gfx.lib toggles the vectors back and forth at the right times to service both the raster split screen and the system's standard ISR.
When the gfx.lib is unloaded, it automatically closes any split screen, exits any fullscreen graphics mode, and unhooks itself from the system's ISR. In your Application code, you only need to load and unload the gfx.lib like any other. Call its routines to configure the graphics context, and the library provide your Application with a user-configurable raster split screen. The interrupt service routines handle themselves. How to use the gfx.lib is covered under Advanced Topics.
Poll DevicesPolling devices is performed by the polldevices routine in the C64 OS KERNAL's input module.
Polldevices checks for an installed game controller driver, and calls the scan buttons routine of that driver. Game controllers do not result in queuing of events. They set standard button state values in memory for 1, 2, 3 or 4 controllers with up to 8 buttons each. A game is required to check these button state memory values as part of its main loop.
Next polldevices checks for an installed pointer driver, and calls routines in that driver to track the movement of the pointer and scan the device's buttons. If the pointer device's buttons are changing or are depressed, a full keyboard scan is not possible due to hardware constraints. Therefore, only a partial keyboard scan is performed for the CONTROL, COMMODORE, and LEFT-SHIFT modifier keys. Mouse events are synthesized from the changing states and enqueued.If the pointer driver is not interfering with the keyboard lines, a full key scan is performed. Key command events and printable key events are enqueued.
Network device polling may be introduced to this routine in a future version.
SID MusicWhen sidplay.lib is loaded and linked, it is not immediately installed into the ISR. The ISR calls a SID play vector. By default this vector immediately returns, by being pointed to RAW_RTS. RAW_RTS is a stable place in workspace memory where an RTS is found.
When sidplay.lib is instructed to play, it copies a pointer to the loaded SID file's play routine into the SID play vector. To stop playing music, sidplay.lib silences the SID(s) and restores the SID play vector to RAW_RTS.
Update Jiffy Clock and Scan the STOP KeyUpdating the Jiffy Clock and scanning the STOP key are both performed by calling UDTIM in the KERNAL ROM.
The state of the STOP key is updated on every call of the ISR. If the STOP key is depressed, with no other mitigating keys depressed at the same time, then $7f is written to workspace address $91. $7f remains in $91 for the next call of the ISR checks its state again and updates address $91. Calls to the STOP KERNAL routine return the value of $91 compared to #$7f.
Update TimeThis stage performs several steps. It uses a single incrementing jiffy count (apart from the Jiffy Clock) to count out fractions of a minute, computed correctly for NTSC and PAL.
CPU Usage History is recorded in a rolling table of 10 increments over 10 seconds. Each record is a snapshot of how many jiffies have passed (from $00 to $ff) since the last time the main event loop was processed.
CPU Busy indicator is displayed and incremented one animation frame every 1/3rd of a second, whenever more than 127 jiffies have passed since the main event loop was processed. On PAL this means the CPU Busy indicator displays after 2.56 seconds of the UI being unresponsive. On NTSC it displays after 2.13 seconds. Display of the CPU Busy indicator is under control of the system redraw flags, rcpubusy.
The seconds indicator in the menu bar clock blinks once per second. The indicator is set to a full colon on every half second and cleared at the end of every full second. When the menu bar clock is visible, calculation of the blinking seconds indicator is always performed, but the blinking animation can be disabled by changing the clear state character stored in t_blinks. By setting t_blinks to a full colon, no change is visible.
The full time is redrawn every one minute. The service KERNAL module holds the drawing logic for the time of day clock, and supports 12H and 24H time. Display of the time is under control of the system redraw flags. The time is not updated (even to the screen buffer) while in fullscreen graphics mode. The time is not updated when the menu bar is hidden. And the time is not displayed or updated unless the rclock flag is set. This flag allows the user to turn the display of the clock on and off.
The system redraw flags can be changed by calling setflags. And you can manually request the menu bar clock to be redrawn by calling redrwtod.
alert
Purpose | Draw the attention of the user by blinking the border twice. |
---|---|
Module offset | 0 |
Communication registers | None |
Stack requirements | 8 bytes |
Zero page usage | $02, $09, $d8, $d9 |
Registers affected | A, X, Y |
Input parameters | None |
Output parameters | None |
Description: This routine is used to visually draw the attention of the user. It does so by blinking the color of the border, twice. Call this routine whenever you want to indicate to the user that something they may have expected to work didn't work. If the user issues a keyboard command, and the key command event propagates its way through the whole system, it calls alert automatically to let the user know that the key command wasn't handled by anything.
The alert uses C64 OS KERNAL timers to schedule the changing of the border color. It changes the color from the current color to a complementary alternative color immediately, then sets a 100ms timer. When the timer elapses it restores the original color, and sets 100ms timer again. When the timer elapses it repeats this process once more.
The complementary colors are hard-coded according to the following table:
Initial Color | Alternate Color |
---|---|
black | dark grey |
white | yellow |
red | light red |
cyan | light blue |
purple | blue |
green | light green |
blue | light blue |
yellow | white |
orange | brown |
brown | orange |
light red | red |
dark grey | black |
medium grey | light grey |
light green | green |
light blue | blue |
light grey | medium grey |
confirq
Purpose | Configures the C64 OS interrupt service routine. |
---|---|
Module offset | 3 |
Communication registers | None |
Stack requirements | 2 bytes |
Zero page usage | None |
Registers affected | A, X, Y |
Input parameters | None |
Output parameters | None |
Description: This routine is used to configure the CPU's NMI and IRQ vectors in RAM ($FFFA/$FFFB and $FFFE/$FFFE respectively) to point to the interrupt service routines in the C64 OS KERNAL's service module. It also configures the following workspace interrupt vectors:
Address | Vector |
---|---|
$0312/$0313 | IRQ when the KERNAL ROM is patched out |
$0314/$0315 | IRQ when the KERNAL ROM is patched in |
$0318/$0319 | NMI |
This routine is one of the very last routines called by the booter (//os/:booter) and is used to transfer control from the KERNAL ROM to the C64 OS KERNAL. Under normal circumstances this routine never needs to be called. However, if a game or some other program replaces the C64 OS ISR with something custom, calling confirq restores C64 OS's standard control over the system.
For example, perhaps an Application embeds a custom FLI or IFLI image viewing routine. Such a routine requires very strict timing requirements. This routine could temporarily replace the ISR with a custom one. This could show the FLI image and still have time to scan for a key press to leave the viewing routine. Upon leaving the viewing routine, it can call confirq to re-establish C64 OS's control over the system and resume processing input, etc.
General System Routines
The following is a set of routines providing general system services. These routines are used to update the system-wide status bar, adjust the system's redraw flags, update the menu bar clock, enable graphics mode, process global keyboard shortcuts and prepare Applications, Utilties, libraries, drivers and more to be able to link to the KERNAL.
The alert routine should also be considered a general system service.
getargs
Purpose | Manipulate the stack to handle inline arguments. |
---|---|
Module offset | 6 |
Communication registers | A |
Stack requirements | 2 bytes |
Zero page usage | $45, $46 |
Registers affected | A, X |
Input parameters | A → total argument byte count |
Output parameters | argptr ← points to start of inline argument block |
Description: This routine is used to manipulate the stack from within a routine that uses inline arguments. When that routine returns, execution proceeds on the first byte following the block of inline arguments. Getargs also configures the zero page argptr to point to the start of the inline arguments block.
Inline arguments are useful in certain cases where the number of parameters that need to be passed to a routine exceeds what can be passed in registers alone. A block of several 1 and 2 byte arguments can be put in the code following the JSR to the routine. A routine which takes inline arguments must call getargs before returning, or execution will continue by trying to execute the inline arguments as though they were code.
The following is an example of how to call the fictional routine dataread and supply it with two 16-bit inline arguments. The first is a pointer to buffer and the second is a length to read.
No JSR/RTS → JMP Shortcutting
Experienced 6502 programmers know that when a JSR comes as the last step before an RTS, it usually saves memory and execution time to drop the RTS and replace the JSR with a JMP.
When using inline arguments, such as in the example above, it is not possible to replace this JSR with a JMP. The reason is because the return address that the JSR pushes onto the stack is used to locate the address of the inline arguments.
The following is an example of how the fictional routine dataread gains access to its two 16-bit inline arguments and manipulates the stack before it executes the RTS.
See Chapter 2: Development Environment → Common C64 OS Macros → Inline Arguments for more information about the inline argument macros.
updstat
Purpose | Mark the menu screen layer for redraw when the status bar changes. |
---|---|
Module offset | 9 |
Communication registers | None |
Stack requirements | 4 bytes |
Zero page usage | None |
Registers affected | A, X |
Input parameters | None |
Output parameters | None |
Description: This routine is used to mark the menu layer as dirty so that it will get automatically redrawn on the next redraw cycle. The menu layer is the layer on which the status bar is drawn. Therefore, this routine should be called whenever the Application changes the custom message it displays on the status bar. The system redraw flags are consulted to check if the status bar is showing or hidden. If the status bar is hidden, the layer is not marked dirty and does not get redrawn.
setflags
Purpose | Change the system redraw flags. |
---|---|
Module offset | 12 |
Communication registers | A |
Stack requirements | 7 bytes + |
Zero page usage | Custom |
Registers affected | A, X, Y |
Input parameters | A → System Redraw Flags |
Output parameters | None |
Description: This routine is used to change the system redraw flags. This is a series of flags that control the display of several C64 OS system-level features. To modify one flag without modifying the others, you should read from the redrawflgs workspace memory address, AND, ORA, or EOR to clear, set or toggle one or more bit flags, then call this routine passing the new set of flags in the accumulator. Do not simply write the new flags back to redrawflgs. When setflags is called, it sends a message to the Application allowing it to respond to the changes. Because the Application is synchronously messaged, an indeterminate amount of stack and zero page are used.
For example, the menu bar and/or status bar can be programmatically hidden using setflags. When the Application gets a message that the system redraw flags have changed, it can check the display status of the menu bar and status bar and adjust its Toolkit root view's top and bottom offsets. This allows the Application to dynamically resize its user interface in response to these changes.
The following are some examples of how to programmatically change the system redraw flags.
The system redraw flags and workspace memory address are defined by //os/s/:service.s. The following table lists the available redraw flags.
Constant | Value | Meaning |
---|---|---|
rnewgfx | %00000001 | Signals the graphics library (gfx.lib) that buffered graphics data have changed and need to be copied to video memory. |
renagfx | %00000010 | Signals the graphics library (gfx.lib) that fullscreen graphics mode should be entered or exited. |
rgraphix | %00000100 | Indicates the current state of fullscreen graphics mode. (READ ONLY.) |
rmodal | %00001000 | Indicates the modal state of the topmost screen layer. All menu items are temporarily disabled, and double clicking the clock or available memory in the status bar does not open a Utility, when the rmodal flag is set. |
rstatbar | %00010000 | Indicates the display of the status bar. Set to show the bar, clear to hide the bar. |
rcpubusy | %00100000 | Indicates the display of the CPU busy indicator. Set to use the indicator, clear to disable the indicator. The CPU busy indicator only shows when not in fullscreen graphics mode, the menu bar is visible, and the CPU has been busy for more than 2 seconds. |
rclock | %01000000 | Indicates the display state of the menu bar clock. Set to display, clear to hide. The menu bar clock is only visible, and is only updated, when not in fullscreen graphics mode, and the menu bar is visible. |
rmenubar | %10000000 | Indicates the display of the menu bar. Set to show the bar, clear to hide the bar. |
redrwtod
Purpose | Manually trigger an immediate redraw of the menu bar clock. |
---|---|
Module offset | 15 |
Communication registers | None |
Stack requirements | 5 bytes |
Zero page usage | None |
Registers affected | A, X, Y |
Input parameters | None |
Output parameters | None |
Description: This routine is used to trigger an immediate redraw of the menu bar clock. The redraw only happens if the clock is enabled in the system redraw flags. See setflags.
This routine should be used when, for example, the display of the time has just been changed from 12H to 24H mode, or vice versa. Or if the system time has just been changed. Calling this reflects those changes immediately, rather than having to wait up to a full minute for the changes to be visible.
setgfx
Purpose | Change the system high memory usage flags to configure graphics mode. |
---|---|
Module offset | 18 |
Communication registers | X, Y |
Stack requirements | 7 bytes + |
Zero page usage | $22, $23, $57, $58, Custom |
Registers affected | A, X, Y |
Input parameters | RegPtr → Pointer to a graphics context structure |
Output parameters | None |
Description: This routine is used to change the system's high memory usage flags and prepare the system for graphics mode. In order to use this routine, the graphics library (gfx.lib) must already be loaded. If the graphics library is not loaded, this routine returns immediately, the zero flag is set and the RegPtr returned is unmodified from the RegPtr that was passed in.
When calling this routine, you must pass a RegPtr to a valid graphics context structure. If a Utility is currently occupying high memory, the Utility is quit automatically. Workspace memory is configured in preparation for splitscreen and fullscreen graphics modes. The himemuse flags are set to indicate how high memory is being used. The Application is synchronously messaged about the change of high memory usage. And lastly the confgfx routine is called on the graphics library.
When the Application calls setgfx it should immediately thereafter install the bitmap graphics data into high memory; 8000 bytes of bitmap data from $E000 to $FF3F. The Application can respond to the message about the change to himemuse. If himemuse indicates that high memory is free, the Application can respond by calling setgfx again and re-installing the bitmap graphics data (from disk, from an REU or from a main memory buffer.) This allows an Application's graphics data and a Utility to switch places.
Copy only 8000 bytes not 8192
When installing bitmap graphics into high memory, it is tempting to use a routine such as memcpy to copy 8 pages, even though the last 192 bytes of the last page aren't necessary. This must not be done, because the final 192 bytes of memory, from $FF40 to $FFFF, hold the mouse pointer sprites, and the CPU's interrupt vectors. If these get overwritten by garbage a crash will immediately follow.
The workspace address himemuse, its flags, and the constants for the graphics context are defined in //os/s/:service.s. The following table lists available himemuse flags.
Constant | Value | Meaning |
---|---|---|
hmemfree | %00000000 | High memory is currently unused. |
hmemutil | %00000001 | A Utility is currently loaded. |
hmembuff | %00000010 | High memory is claimed by the Application as a generic buffer. |
hmembitm | %01000000 | High memory is occupied by bitmap graphics data. |
hmemmult | %10000000 | When combined with hmembitm, indicates that the graphics data is in multi-color mode. |
The graphics context is divided into two parts. The first part is required by the grahpics library. The second part is optional; it is only required when working with graphics data loaders and savers.
Constant | Offset | Size | Value |
---|---|---|---|
gvidmode ghimemflg |
0 | 1 |
A number representing the VIC-II's native video mode. (v1.04+) Deprecated: The bits in this byte are transferred to himemuse when the graphics context is installed. Therefore, the value of this byte is either "hmembitm" or "hmembitm.hmemmult", (i.e., the 2 flags logically OR'd together.) (v1.0 - v1.03) |
gcolhptr | 1 | 2 | A pointer to a buffer containing 1000 bytes of screen memory, used as color memory for HiRes and Multi-Color bitmap modes. |
gcolmptr | 3 | 2 | A pointer to a buffer containing 1000 bytes of color memory, used as color memory for Multi-Color bitmap mode. This pointer is not used if the graphics data is in HiRes mode. |
gbgcol | 5 | 1 | A single byte that holds the background color register of the VIC-II. This byte is not used if the graphics data is in HiRes mode. |
Extended structure for loaders/savers. | |||
gbmapptr | 6 | 2 | A pointer to a buffer containing 8000 bytes of bitmap data. |
gchbufsz | 8 | 1 | Number of allocated pages for screen memory buffer |
gcmbufsz | 9 | 1 | Number of allocated pages for color memory buffer |
gbmbufsz | 10 | 1 | Number of allocated pages for bitmap memory buffer |
The extended graphics context is only needed if using a C64 OS graphics datatype loader or saver. When using a loader, you pass a pointer to an extended graphics context structure to the loader's imgconf routine. The loader automatically allocates the memory blocks necessary to load in the graphics data, and sets the pointers to those blocks into the graphics context structure. It indicates the size of the allocations by writing the number of pages allocated for each block into the last three bytes of the extended structure.
An Application that uses datatype loaders can use this information to free the appropriate memory before loading in new graphics data. More information about datatype loaders and savers is found in Advanced Topics.
Graphics Context video mode options. (Available in v1.04+) Defined by //os/s/:dt.img.s
Constant | Value | Notes |
---|---|---|
vdm_stchr | 0 | Hires Character Mode |
vdm_mcchr | 1 | Multi-Color Character Mode |
vdm_ebchr | 2 | Extended Background Character Mode |
vdm_hrbmp | 3 | Hires Bitmap Mode |
vdm_mcbmp | 4 | Multi-Color Bitmap Mode |
getsfref
Purpose | Allocate and/or configure a memory page with the system file reference. |
---|---|
Module offset | 21 |
Communication registers | X, Y |
Stack requirements | 4 bytes |
Zero page usage | $1a, $1b |
Registers affected | A, X, Y |
Input parameters |
Y → 0, to auto-allocate 1 page and configure as a file reference. Y → A page number to configure as a file reference. |
Output parameters |
Y ← Index to the null terminator in the file reference path component. X ← Page number of the file reference. |
Description: This routine is used to initialize a memory page as a file reference to the current C64 OS system directory. This can be used as the starting point for creating a file reference relative to the system directory. The input and output parameters are designed to make it convenient for modifying the system file reference for appending a relative subdirectory.
If you want the system file reference to be returned in a freshly allocated, single page of memory, pass 0 in the Y register. A new page is allocated and configured as the system file reference. The page number of that new allocation is returned in the X register. The Y register is returned as an index to the null terminator in the path component of the file reference. This allows you to use the Y register to immediately extend the path without needing to first calculate the length of the existing path.
The following is a common pattern that uses one of the standard string macros.
See also, Chapter 3: Development Environment → Common C64 OS Macros → String.
The stradd macro is designed just for this use case. It takes a zero page pointer to a string to be appended to, and an absolute address to the string to append. The X register is the starting index into the absolute address, and the Y register is starting index into the zero page pointer. In setting frefptr's low byte to 0 using the X register, we've simultaneously prepared X to be the index for our string constant to append. And Y was returned already set at the index of the null terminator of the path, the exact place where we want to append to.
The constant libpath must end with a trailing slash and a null-byte of its own. The result of the getsfref and the #stradd is a file reference to the library directory of the current system directory, wherever that may be.
Another common usage pattern is for loading a multi-page relocatable binary that is located in a subdirectory relative to the system directory. For example, loading a driver. If the driver is 2 pages, you can manually allocate 2 pages, and then configure the first page as a system file reference by passing that page number in the Y register. Proceed to configure the file reference by appending the "drivers/" subdirectory, and setting the filename into the file reference.
This concoction, (2 sequentially allocated pages where the first is configured as a file reference,) can be passed to loadreloc. Loadreloc uses the file reference in the first page to find the file to load, which it proceeds to load in overtop of both allocated pages, neatly cleaning up the no longer needed file reference. A very efficient arrangement, pgalloc, getsfref, #stradd, and loadreloc are designed to be elegantly chained together.
Below is an example of using this to load a 2-page driver.
syskcmd
Purpose | Processes the lowest level system keyboard commands. |
---|---|
Module offset | 24 |
Communication registers | A, Y |
Stack requirements | 9 bytes + |
Zero page usage | Custom |
Registers affected | A, X, Y |
Input parameters |
A → PETSCII value. Y → Modifier key flags. |
Output parameters | None |
Description: This routine is called by the main event loop, implemented in the screen KERNAL module. It should not be necessary to call this routine manually.
When an input event is queued, the main event loop handles dispatching the event to the screen layers. The actual event is not sent anywhere. Instead one of the screen layer's vectors is jumped through to notify it that an event is on the queue. If that routine returns with the carry clear, propagation ends; The main event loop dequeues the event without jumping through that vector on the next screen layer. Notification of the event is passed from highest screen layer to lowest screen layer, until one of them handles the event and stops the propagation to the next layer.
There is an exception to this pattern with key command events. Before notifying any screen layer, the key command is read into the A and Y registers, and syskcmd is called. Syskcmd compares the event against the configurable global keyboard shortcuts. If it finds a match, it performs one of the four following actions:
- Toggles the menu bar by calling setflags
- Toggles the status bar by calling setflags
- Toggles fullscreen graphics mode, if himemuse indicates bitmap data is available.
- Takes a screen capture by loading the grab library (grab.lib).
If one of these actions is taken, the carry is returned clear to stop propagation, and no screen layers get notified about the event.
initextern
Purpose | Convert a KERNAL link table into a local KERNAL jump table. |
---|---|
Module offset | 27 |
Communication registers | X, Y |
Stack requirements | 2 bytes |
Zero page usage | $fb, $fc, $fd, $fe |
Registers affected | A, X, Y |
Input parameters | RegPtr → Pointer to the start of a KERNAL link table. |
Output parameters | None |
Description: This routine is usually called at the start of a unit of code that will later need to make KERNAL calls. This could be an Application, or a Utility, a library, driver, loader, saver, etc.
One routine to rule them all
This routine is special. Initextern is used to initialize a jump table to external routines, those found in the C64 OS KERNAL. But, if you need a KERNAL link table to call routines in the KERNAL, and initextern is a routine in the KERNAL, then there seems to be a chicken and egg problem.
The solution is that the KERNAL booter is assembled together with the KERNAL modules. The booter is what builds the KERNAL module lookup table, so it knows where each module starts. One of the jobs of the booter is to copy the address of initextern to an absolute address in workspace memory, for which there is a constant, aptly named, initextern.
The initextern constant is defined by //os/s/:app.s, and is set as $02fc. This address is fixed for all eternity. If this address were changed, it would break everything in C64 OS that makes a KERNAL call. Therefore, it is not going to change.
Initextern is the only KERNAL routine that is called via a workspace memory address, initextern, defined by //os/s/:app.s.
See Chapter 4: Using the KERNAL → Linking to the KERNAL for more information about how to use initextern.
The Application Loading Process
Loading a new Application always goes through the same set of steps. These steps return the computer a known, consistent and default state, allowing one Application to smoothly transition into another. This process can continue, with one Application loading the next, indefinitely, without having the restart the computer.
The processor port is configured to the default memory mode: KERNAL ROM and I/O mapped in, BASIC ROM mapped out.
The exceptions table is zeroed. This means that if the previous Application called loadapp from within one or more nested try/catch blocks, all of those blocks are eliminated.
Any splitscreen mode or fullscreen graphics mode is reverted. The split is closed and fullscreen graphics mode is exited. This can be seen by using the keyboard shortcut to Go Home from an Application that is currently in fullscreen graphics mode or splitscreen.
The currently running Application has its quitapp routine called. This gives the current Application the opportunity to clean up; unload libraries, free allocated memory, close any open files or network connections, save its state to global settings or locally to its App bundle, etc. It is the responsibility of each Application to clean up its own resources in its quitapp routine.
The booter's quitapp routine
When the booter calls quitapp, it returns the pointer to runhome. The booter manually calls runhome. This configures a new file reference to the Homebase Application and falls through to loadapp. Before loadapp begins to load the first Application it tries to quit the currently running Application by calling its quitapp routine.
At this point, there is no currently running Application. To handle this, the booter implements the same Application vector table that every Application must have. However, its quitapp routine is just a stub. It doesn't do anything, except return. But it serves the purpose of allowing the first Application to be loaded in.
The system file reference is then initialized, which sets the system drive's current partition and current directory to the partition and directory of the C64 OS system directory.
Next it loads the loader (//os/library/:loader). The loader performs a number of important steps which continue and extend the process of loadapp. Originally this functionality was part of the KERNAL, but to save memory it was migrated to a special system component. The code found in the loader only needs to be in memory during the transition from one Application to the next. After the next Application is loaded in, the loader no longer takes up any memory.
C64 OS controls the menu system. The loader is responsible for deallocating all the memory used by the previous Application's menus, and then loads in (and allocates memory for) the menu definitions file from the next Application. If the loader cannot find a valid menu definitions file in the Application bundle, it aborts loading this Application and attempts to load in the Homebase Application again.
The loader checks to see if the Application uses an init.o binary. This is an optional second binary. Applications often require a fair bit of code to build the user interface, load the libraries, icons and other resources. All of that initial code doesn't need to remain in memory. To support this, Applications can optionally split their logic into main.o and init.o. If the loader finds init.o, it loads it in.
Lastly, loadapp loads in main.o. If this fails, it aborts and attempts to load in the Homebase Application again. After loading main.o, it marks the memory area occupied by main.o as allocated, clears the stack, calls the Application's init routine, then re-enters the main event loop.
loadutil
Purpose | Open a Utility by name. |
---|---|
Module offset | 30 |
Communication registers | X, Y |
Stack requirements | 4 bytes + |
Zero page usage | Custom |
Registers affected | A, X, Y |
Input parameters | RegPtr → Pointer to a c-string with the name of a Utility. |
Output parameters |
C ← CLR if the Utility will be opened. C ← SET if the Utility is already open. |
Description: This routine is called to load and open a Utility. The filename of the Utility must be in a c-string. A RegPtr pointer to that string is passed to loadutil.
If a Utility is already open, the identity of the open Utility is compared to the name of the one to be opened. If they are the same, the carry is returned set, no further steps are taken, the Utility is already open. If a different Utility is open, the old one is ordered to quit. This gives it an opportunity to clean itself up and save its state. Then the new Utility is loaded.
If graphics data is currently in high memory, the himemuse flags are first set to free. This gives an opportunity for the graphics library to close the splitscreen or switch out of fullscreen graphics mode. The Application is not yet messaged about this change of state, to prevent it from trying to act on the freeing of high memory, to which it might otherwise attempt to re-install graphics data.
Next the Utility framework is loaded to the end of high memory, and linked against the KERNAL. The pointer to the string with the name of the new Utility to load is then forwarded to the Utility framework's uf_load routine. This takes over the job of loading, initializing, and assisting the Utility to run.
After the Utility is running, himemuse is updated to indicate that a Utility is loaded, and the Application is then notified of the change.
quitapp
Purpose | Quit the current Application and return to Homebase. |
---|---|
Module offset | 33 |
Communication registers | X, Y |
Stack requirements | 2 bytes |
Zero page usage | None |
Registers affected | X, Y |
Input parameters | None |
Output parameters | RegPtr ← Pointer to the runhome routine. |
Description: This routine is called to quit the current Application and load the Homebase Application in its place. In a well behaving C64 OS Application with a consistent user interface, there will be a menu option whose title is "Go Home", and with the standard keyboard shortcut: CONTROL+COMMODORE+H.
This menu action must be handled by the Application, and its only behavior needs to be to call quitapp. The service KERNAL module contains a routine called runhome, which is not available from its jump table. Quitapp does only one thing, it reads the address of runhome into a RegPtr and writes it into the main event loop's loop break vector (loopbrkvec), then returns.
The main event loop loops because the loop break vector points to the start of the loop. The loop executes, then the loop break vector jumps it back to the start of the loop, and it continues processing the main event loop indefinitely. Changing the loop break vector causes the main event loop to stop looping, and jumps off to the runhome routine. The runhome routine configures a new system file reference, which it appends with the relative path to the currently defined Homebase Application ("services/App Launcher/" or "services/File Manager/") and then falls through to loadapp.
The reason quitapp returns the pointer to the runhome routine is because when first booting up, the main event loop is not yet running. Thus, the booter starts by calling quitapp, then it manually JMPs to the runhome routine, and that kicks off the initial loading of the first Application.
loadapp
Purpose | Load a new Application, first giving the previous Application a chance to clean up. |
---|---|
Module offset | 36 |
Communication registers | X, Y |
Stack requirements | Undefined |
Zero page usage | Undefined |
Registers affected | A, X, Y |
Input parameters | RegPtr → File reference to an Application bundle. |
Output parameters | Undefined |
Description: This routine is deeply fundamental to how C64 OS works. It is used to transition from one Application to another.
The reason why the stack usage is undefined is because part of the process of changing Applications involves zeroing the stack usage. Anything on the stack when this routine is called, is wiped out without needing to be pulled.
The reason why the zero page usage is undefined is because this routine changes which Application is running. Therefore, any zero page values the previous Application was using become totally irrelevant. The new Application takes over all zero page addresses.
The reason why the output parameters are undefined is because, this routine never returns. It clears the stack, and its final step is to JMP into the main event loop. But the main event loop also never returns, so loadapp never returns either. Therefore, whatever calls loadapp doesn't ever get anything back from it, the code that calls it gets removed from memory.
Library Management
C64 OS supports a system of shared libraries. Libraries are loaded and unloaded using the following routines of the service KERNAL module. Libraries are always relocatable, and use the lower-level loadreloc routine to perform the actual library load and relocation. A maximum of 10 shared libraries may be loaded by the whole system at any given time.
It is possible to create relocatable components that are not officially libraries. For example, an Application may store extra components in its app bundle. The Application can use loadreloc to load and relocate these directly. These can function more or less like libraries. They are not shared with the rest of the system, they require slightly more work to load in, but there is no limitation on how they're named or how many can be loaded in at a time. Manually loaded relocatable components can have certain advantages. They can directly access the jump table of the Application to which they belong, and because they're not shared they can safely store static variables.
loadreloc
Purpose | Load and relocate a C64 OS relocatable binary. |
---|---|
Module offset | 39 |
Communication registers | A, X, Y |
Stack requirements | 16 bytes + |
Zero page usage | $1c, $1d, $63, $64, $f7, $f8, custom |
Registers affected | A, X, Y |
Input parameters | RegPtr → File reference to relocatable binary file. |
Output parameters |
A ← First page of relocation. C ← SET if an error occurred. |
Description: This routine is used to load and relocate a binary file that has been assembled to be compatible with the C64 OS relocatable binary requirements.
To use this routine, first allocate some number of consecutive pages which are enough to hold the size of the relocatable binary. For instance, if the binary is 4 blocks on disk, it will need an allocation of 4 consecutive pages using pgalloc. Next, initialize the first page of that block as a file reference to the relocatable binary file. This file is typically found somewhere in the system directory, but this is not a requirement.
Call loadreloc passing a RegPtr to the file reference, which has the required number of trailing allocated pages. The file is first loaded into the allocated pages, overwriting the first page which had been initialized as a file reference. Then the binary is relocated by rewriting all the high byte self-references to the new set of pages in which this code is found.
Drivers and Toolkit classes are loaded using loadreloc directly. Shared libraries are loaded using loadlib, which uses loadreloc under the hood.
The carry is returned set if an error occurred loading the file. After the file loads correctly, the routine cannot determine if the relocation itself is valid. As long as the file was created as a valid C64 OS relocatable binary, it should not be possible for the relocation process to fail.
After successfully loading and relocating the binary, the first routine is automatically executed, by JSRing to the zeroth byte of the first relocated page. In the C64 OS relocatable binary format, this zeroth byte is a JMP instruction. Typically this directs to an initialization routine. If none is required, it can immediately RTS. The carry is then returned clear from loadreloc to indicate that the relocation was successful.
How to create a C64 OS relocatable binary
A binary is relocatable if it is page aligned and begins with a JMP instruction whose high byte is to the same page where the JMP instruction itself is found. The byte value of that first page, and all subsequent pages occupied by the assembled binary, must not appear anywhere within the assembled binary, other than as high byte self-references.
To create a relocatable binary, begin the code with a JMP to somewhere within the first 255 bytes of the assembled code. Assemble to $1000 (* = $1000). Return to BASIC and load and run the C64Tool, relocator.
load"//os/c64tools/:relocator",8 run
When it asks for the name of a file, enter the name of the file that was just assembled to $1000. It scans the file and determines the number of consecutive pages it occupies. It then searches for a consecutive range of that many bytes, none of which are found anywhere within that assembled file.
Relocator then outputs a list of available page numbers to which this file may validly be assembled. Open the source code again and change its starting page to one of those valid pages. For example, if relocator listed that $73 is a valid page, you can reassemble the code by replacing the * = $1000 with * = $7300. Any such valid page that was listed by relocator can be used. The resultant assembled binary is now C64 OS relocatable. It may also be statically loaded to its assembled address.
In the above example, when C64 OS loads and relocates that file, it will not be loaded to $7300 at all. It will be loaded to wherever enough pages have been allocated to hold it. However, it could also be loaded without relocation, and in that case, this example would load and run correctly from $7300.
See the weblog post, Drivers and Relocatable 6502 Code, for a deep dive into how this relocation technique works.
Recheck with relocator when the code is modified
After modifying the code of a relocatable binary, it is necessary to check it with the relocator tool again. Reassemble to $1000, run that through the relocator tool again, and confirm which bytes are still valid bytes to which to assemble this file. The fewer pages that are being relocated, the more likely that a valid relocation page will remain valid even after a code modification. However, if the relocatable is 3, 4, 5 or more pages, there is a non-trivial likelihood that any modification of the code will change which pages are available to be assembled to.
If the page you were assembling to suddenly becomes no longer available, and yet you continue to assemble to that page number, this file is no longer a valid relocatable binary and using it in C64 OS will result in a crash.
loadlib
Purpose | Load a shared library, or link to one already loaded. |
---|---|
Module offset | 42 |
Communication registers | A, X, Y |
Stack requirements | 18 bytes + |
Zero page usage | $1c, $1d, $22, $23, $24, $25, $63, $64, $f7, $f8, custom |
Registers affected | A, X, Y |
Input parameters |
X → 1st library filename character. Y → 2nd library filename character. A → Lo Nybble = size of library from 0 to 15 pages. A → Hi Nybble = library loading flags. |
Output parameters |
A ← First page of relocation. This routine raises exceptions. |
Description: This routine is used to load and relocate a shared library. If the library is already loaded, it increments its reference count and returns a pointer to the start the first page where the library is found.
C64 OS shared libraries must be installed in //os/library/. All installed libraries must be uniquely identifiable by the first two characters of their filename, and must end with ".lib.r".
To load a library, load the two identifying filename characters into X and Y. The accumulator is divided into two nybbles. The lower 4 bits hold the page size of the library from 0 to 15. In practice, from 1 to 15. The upper 4 bits hold special flags shown in the table below. Then call loadlib.
First, the table of loaded libraries is checked for a matching library. If a matching library is already loaded, it is not loaded a second time. Instead, its reference count is incremented. A maximum of 10 shared libraries may be loaded at the same time. If this library is not already loaded, but there are already 10 other loaded libraries and exception is raised.
Otherwise, the library is loaded in and relocated, its reference count is initialized to 1, and its meta information (identifying characters, reference count and size) are stored in the shared library tables in workspace memory. The first page of relocated library is returned in the accumulator.
Constant | Value | Meaning |
---|---|---|
slnoinit | %10000000 | The library's first routine, its initialization routine, is called automatically by default. If this flag is set, the initialization routine is not called. |
After a library is loaded, it is necessary to link your code to the library's jump table entries that you need to call. How to do this is covered in Chapter 5: Using Libraries.
unldlib
Purpose | Unload a shared library, or decrement is reference count. |
---|---|
Module offset | 45 |
Communication registers | A, X, Y |
Stack requirements | 7 bytes + |
Zero page usage | $24, $25, custom |
Registers affected | A, X, Y |
Input parameters |
X → 1st library filename character. Y → 2nd library filename character. A → library unloading flags. |
Output parameters | None |
Description: This routine is used to unload a shared library. If you load a shared library, you are required to unload it later when your Application or Utility is quit. Drivers can load libraries, and even libraries can load other libraries. When that driver or library gets unloaded, they in turn must unload the libraries they loaded.
When a shared library is loaded for the first time, its reference count is initialized to 1. When something tries to load a library that is already loaded, its reference count is incremented instead. When unldlib is called, the reference count is decremented. Only if the reference count drops to zero does the library actually get removed from memory, and uninstalled from the shared library tables in workspace memory. If a library's reference count is decremented, but remains 1 or greater, it remains in memory, and continues to occupy one of the 10 available slots.
Load the library's two identifying filename characters into X and Y. Load the accumulator with special unload flags shown in the table below. Then call unldlib.
Constant | Value | Meaning |
---|---|---|
slunload | %10000000 | Set this flag to explicitly call the library's unload routine. The library must have an unload routine or this flag should not be used. If the library has an unload routine, this flag must be used. |
A library may optionally have an unload routine. A library that needs to clean up resources, such as deallocate memory, close a file, or unload other libraries, does so in its unload routine. If an unload routine is used, it must be the second entry in the library's jump table. When a library that has an unload routine is being unloaded, you must pass the slunload flag, or the library will leak resources.
KERNAL Modules in Alphabetical Order
KERNAL Modules in Module Lookup Table Order
Return to Using the KERNAL → KERNAL Modules
Table of Contents
This document is subject to revision updates.
Last modified: Apr 20, 2023