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
Creating an Application: Tutorial 1: Hello World
We'll start this tutorial with a trivial example in order to understand the basic outline of an Application, before progressing into more complicated features. The first thing we'll do is create a Hello World Application.
Create the Application Bundle
An Application requires an Application bundle. We can start by creating the Application bundle in the system's applications directory.
With JiffyDOS: @cd//os/applications @md:Hello World @cd/Hello World
First make the applications directory the current directory. Then make a new directory called "Hello World". This new directory is our Application's bundle. Its name is the name of our Application. Then make the new bundle directory the current directory.
Use of mixed-case
C64 OS uses upper and lower case characters, so it usually looks better when an Application's name uses standard English capitalization for proper nouns. (i.e., Chess rather than chess, App Launcher rather than app launcher, Hello World rather than hello world.)
The name of every installed Application has to be unique. The reason is because all installed Applications must have their bundle in the common //os/applications/ directory. Therefore, you should put some thought into choosing an appropriate name for your Application.
Create the about metadata file
The Application bundle is required to have an about metadata file. The filename is "about.t" found in the root of the bundle. It holds the information displayed by the "About This App" Utility. Its format is four lines, in PETSCII, separated by \r (0x0d):
name of app version# copyright year author
An example would be:
Hello World 1.0 2023 John Doe
The name of the bundled should match the name that is found in the first line of the about metadata file. The file format has to be in PETSCII, so the easiest way to create this file is with Novatext, which is included in the //os/c64tools/ directory.
Native development pro-tip
Put a copy of your most common tools into the root directory of your development partition. This makes it easy to load the tool with the simplest, absolute, path //.
Copy the version of Novatext you want to use to the root directory.
@c//:novatext=//os/c64tools/:novatext 1.2.2
Now, to invoke Novatext you can use a short command such as:
↑//:nova*
Open Novatext, without changing out of the current directory. (See the pro-tip note about how to arrange your development tools.)
↑//:nova*
Type in the four lines, pressing RETURN once at the end of each line. The year field does not need to include the copyright symbol, and should be a 4 digit year. You may have to test viewing your App bundle with About This App to make sure the field name for author and version number are not too long, and make abbreviations where appropriate.
Save the file by press CONTROL+S, when it asks for a name, type: about.t and press RETURN. You may now exit Novatext by pressing CONTROL+Z, and pressing Y to confirm.
CONTROL+S about.t CONTROL+Z y
You should now have an about.t file in your Application bundle. You can list the directory to confirm. And you can preview the contents of that file from the READY prompt using JiffyDOS's text printing command.
@$ @t:about.t
It is also possible to create this file without using Novatext. You can open the about.t file for write, using BASIC, and then print the four lines into the file, then close the file. Such as in the following example:
open2,8,2,"@:about.t,s,w" print#2,"Hello World" print#2,"1.0" print#2,"2023" print#2,"John Doe" close2
You now have a bundle with with about metadata. You could, at this point, use the About This App Utility to view this bundle, and it will show you the name, version, copyright and author of this Application. You can also attempt to open this Application too. However, since the bundle is incomplete it will abort part way through and return you to homebase.
Create the Icon file
You will notice when you look at this bundle with About This App, or if you try to launch this Application, that the icon appears as a box with rounded corners and a question mark inside. This is the standard placeholder for an Application that is missing its own icon.
The "missing" icon.
Application icons are in HiRes bitmap byte arrangement, 24 x 24 pixels, or 3 characters by 3 characters. This is sometimes referred to as a 3icon, which stands for 3 x 3 characters. Each character is composed of 8 bytes, for a total of 72 bytes. The following is a diagram of the byte order and character order for the icon. There is no color map associated with a 3icon, and the file has no header.
3icon byte layout.
In lieu of an icon drawing Application, (a great idea for a C64 OS Application by the way,) a simple assembly language program can be used to input the hexadecimal values of the icon bytes and write them out to a file.
The example program below can be saved to the Application bundle as icon.charset.a. The data comes in 3 groups of 3, for a total of 72 characters, in hexadecimal notation at the end of the program. It is not necessary to assemble this program to an object file, it can be assembled and run directly from TurboMacroPro using the command ←3.
After assembly is complete, press "s" to start the program. It will write out the data, and overwrite any existing file, with the name icon.charset. This is the standard name for the 3icon in the Application bundle. If the icon doesn't look right, you can later return to this program, modify the data and run it again with ←3, to overwrite and update the existing icon.
The icon data in the program above generates the following example icon.
Hello World sample 3icon.
An Application icon is technically optional. The Application can be launched and run without a custom icon; it will use the aforementioned question-mark icon in its place. However, designing a nice icon for your Application is an important part of making an Application that feels polished and complete.
Create the Menu Definitions File
Unlike the Application icon which is not strictly required, the menu definitions file is a required resource. The Application will not launch if the menu definitions file is missing.
The menu system is built into the C64 OS KERNAL, and dynamically draws itself based upon a tree of linked menu structures. In some C64 operating systems (i.e., GEOS), the tree structure for menus must be hardcoded as a series of linked tables which get assembled into the Application binary. C64 OS is more dynamic and consequently much more flexible.
Each Application has a menu definitions file, called menu.m, in the root of its bundle. This file is in a human-readable, human-editable, text format. You can view its contents from the READY prompt with JiffyDOS:
@t:menu.m
You can also view and edit menu.m using Novatext. As with the about.t metadata file, it is possible to create the menu.m file using open, print, and close statements from BASIC. However, menu.m is usually larger and more complex than about.t, and therefore, it is more convenient to use a proper text editor such as Novatext.
The format of menu.m has been designed so the human-readable source data can be loaded into memory and then each byte can be transformed, in place, to convert the source data into linked menu structures. The code that performs this transformation is built into the C64 OS Application loader, (//os/library/:loader). The result is very memory efficient, while also giving you the flexibility to modify the menu definitions without needing to reassemble the Application's code.
There are three kinds of entries in menu.m: Headers, Actions and Spacers.
HeadersA header is a menu entry that opens a submenu, rather than sending an action code. The submenu of a header has some number of items, some of which may be other headers. A header entry must specify the number of immediate items in its submenu. This number does not include the number of items in the sub-sub-menus.
A header entry starts with the title of the menu item, followed by a semi-colon (;), followed by a code that represents the number of items in the submenu. The code has been shifted into lowercase PETSCII alphabet range, a → z, such that a = 1, b = 2, c = 3, etc. This allows two digit numbers up to 26 to be entered as a single byte. Immediately following the sub-item count, must follow a carriage return ($0d). There must not be any spaces in the line.
The following is an example of a header, that indicates that its submenu contains 5 items. (With a carriage return implied at the end of the line.)
File;e
If a header entry indicates that its submenu has 5 items, then the file must contain 5 entries following the header, or the file will not be parsed correctly.
ActionsAn action is a menu entry that, when triggered, sends an action code to the Application via a menu command message. Messages, and menu command messages in particular, are discussed later in this tutorial.
An action entry starts with the title of the menu item, followed by a full colon (:), followed by 3 codes bytes. Immediately following the 3rd code byte, must follow a carriage return ($0d). There must not be any spaces in the line.
The three code bytes are, modifier keys, PETSCII value, and action code. The modifier keys and PETSCII value are used together to establish a keyboard shortcut to this action item. The action code is a single byte which is sent in a message to the Application when this menu item is triggered, either by mouse or by its keyboard shortcut.
Each modifier key is represented by a bit, and the bits can be combined to make modifier key combinations.
Bit # | Bit | Modifier |
---|---|---|
bit 0 | %00000001 | SHIFT |
bit 1 | %00000010 | COMMODORE |
bit 2 | %00000100 | CONTROL |
The complete table of all possible values, therefore, runs from 0 to 7. These byte values are shifted to PETSCII character values "0" to "7" so they can be easily typed into menu.m.
Value | Bits | Modifier Combination |
---|---|---|
0 | %00000000 | No Keyboard Shortcut |
1 | %00000001 | SHIFT (Invalid Combination) |
2 | %00000010 | COMMODORE |
3 | %00000011 | COMMODORE + SHIFT |
4 | %00000100 | CONTROL |
5 | %00000101 | CONTROL + SHIFT |
6 | %00000110 | CONTROL + COMMODORE |
7 | %00000111 | CONTROL + COMMODORE + SHIFT |
Specify a value of 0 for the modifier keys code byte to indicate that no keyboard shortcut is available for this menu item. When 0 is set for the modifier keys, the PETSCII value code byte is irrelevant, it can be set to anything.
SHIFT cannot be used on its own as the modifier for a menu item. The reason is because, at a low level, when SHIFT is the only modifier the KERNAL queues a printable key event, rather than a key command event. However, SHIFT can be used in combination with either COMMODORE, CONTROL or both.
The next code byte is the PETSCII value used in combination with the modifier keys to trigger this menu item with a keyboard shortcut. This can be any PETSCII value that can be produced by a single unmodified keystroke. For example, if you want the keyboard shortcut to be COMMODORE+I, the modifier keys byte should be "2", and the PETSCII value should be "i". Note the lowercase i. If you want the shortcut to be COMMODORE+SHIFT+I, then the modifier keys byte would be changed to "3", and the PETSCII value would remain "i". In a key command event, the SHIFT key's bit is set in the event as a modifier, but it does not change the PETSCII value of the letter i from lowercase to uppercase.
Every keyboard shortcut should be unique. If two menu items share the same shortcut, then pressing the shortcut will only trigger the first menu item found, when searching recursively from the root menu item.
The 3rd code byte is the action code. This can be any single byte value. It is generally recommended to make it a byte value that is human readable PETSCII, but this is not strictly necessary. Every action code must be unique.
Reserved Action Code
There is one reserved action code. Integer value 0, ($00 or 0x00.) This action code has a special meaning, it is handled by the menu system at the KERNAL level, and the action code is not sent on to the Application.
The following is an example action menu entry:
Get Info:2ii
The title comes before the full colon. The full colon indicates that this is an action menu entry. Following the colon come three bytes and then the carriage return ($0d). 2ii means COMMODORE+I is the keyboard shortcut, and it sends the byte "i" to the Application when this item is triggered. The PETSCII value for the keyboard shortcut and the action code are the same in this case. They do not have to be the same, but they are allowed to be the same, and it often makes sense for them to be the same for mnemonic reasons.
SpacersFor organizational purposes, to make menus easier to read, spacers may be inserted between menu items in a submenu. Spacers may not be used in the root menu (i.e., the top of screen, horizontal menu bar.)
A spacer entry in menu.m consists of an asterisk as the only character on the line. There must be no spaces around it, the just an asterisk and then a carriage return to end the line. In the following screenshot, the result of a spacer can be seen in the open menu. It separates "File Manager" from the group of "Desktop X" commands that come above it.
Example of a menu spacer.
In addition to looking good by grouping menu items together, spacers can also be used to help isolate items that you don't want the user to pick accidentally. In this example, accidentally picking the wrong desktop is not a big deal. But if you accidentally picked File Manager, you would leave App Launcher altogether. By separating it from the others, it minimizes the risk of accidentally selecting it while intending to select Desktop 5.
Terminating the Menu Definitions FileEach line must end with a carriage return ($0d). Following the last line there must be a single blank line to end the file. In other words, every value menu.m file must end with two carriage return bytes in a row. The terminating carriage return is required, or the menu parser will not know that it has reached the end of the data.
The following is a very simple menu.m file. It offers a "System" menu header that will appear in the menu bar. It opens a submenu that contains just a single item, "Go Home". The carriage return characters are indicated by {CR} in grey, so you can see where they are.
System;a{CR} Go Home:6h!{CR} {CR}
The Go Home menu action has a keyboard shortcut, CONTROL+COMMODORE+H, and sends the action code "!" to the Application when triggered. More complex menu definition files are examined in the Appendix: Menu Definitions.
Designing Well-Behaved Menus
The menu system in C64 OS is powerful and flexible. With multiple pull-down menus, and up to three levels of submenus, a very large number of items can theoretically be tucked away into a very compact space. Because menus do consume memory, the menu system supports more menu actions than would ever be used in practice.
There are several good practices you should follow when designing an Application.
- Organize the items logically
- Use spacers to group related items within a submenu
- Assign good mnemonic keyboard shortcuts to the most common items
- It is often bad design to assign a keyboard shortcut to every single item
- Use multi-modifier keyboard shortcuts for riskier items
- If a risky item presents an intermediate dialog box, add a ... to the item title
- Consider omitting a keyboard shortcut altogether on the riskiest items
- Follow conventions established by existing C64 OS Applications
If you menu item will scratch a file, because that action is very risky, (i.e., it risks data loss if used incorrectly,) steps should be taken to minimize accidentally triggering it. Any or all of the steps mentioned above can be taken, depending on the nature of the risk. You could use a spacer to separate the risky item from the others. A risky item can be placed last in a submenu, so that the mouse is not required to pass over it to reach some other item. No keyboard shortcut guarantees the item will not be triggered by mistake if user is just guessing about a shortcut. But if it is still useful to have a shortcut on a risky item, it is better to use multiple modifier keys. This requires the user to make a more intentional move to trigger it.
Every Application requires at least one menu option, the one used to leave the Application and take the user back home. By C64 OS convention, it does not use the terminology "Quit" or "Exit" to leave an Application. Instead, it uses the terminology "Go Home". Also by convention, the Go Home menu option should be the last menu option found in the first submenu. The conventional keyboard shortcut to Go Home is CONTROL+COMMODORE+H. This keyboard combination can be used by all first-party C64 OS Applications to return to Homebase.
The Application's Main Binary
Up to this point, we have created the Application's bundle and populated it with standard bundle resources. From this point forward, we will discuss the creation of the Application's main binary.
The Application's bundle must contain an assembled binary called main.o. If this file is missing, the Application will not launch. This file is loaded and executed as part of the process of launching an Application.
The structure of main.a
It is a good idea to provide structure and organization to your code. Obviously, the more complex the Application is, the more parts it will have, which necessitates further and better organization. Our Hello World example Application is about the simplest possible, but it still has a general structure.
- Headers and Includes
- Data Structures and Content
- Code
- Initialization
- Cleanup/Teardown
- Message Handling
- Event Handling
- Drawing
- KERNAL and Library Externs
This is a just a general outline. It's not strictly necessary for things to come in this order, but it's not a bad idea if they do.
Headers and IncludesIt's best to put all of the include statements for headers, constants, macros, libraries, etc., at the very beginning. This guarantees that those things will be defined before they get used. And it makes it easier to see what your Application is making use of if they all come together at the start of the program.
It is a very good idea to put the name of the file in the comment on or close to the first line of the file. That is because, TurboMacroPro does not remember the filename of the file it has loaded. Once you have a few files, it can be easy to forget exactly how the one you're working in was named. When in doubt, a quick check of the comments at the top of the file can save you the giant headache of having saved the file to the wrong name.
The first include, almost always, is to bring in modules.h from //os/h/. This defines the the syscall macro that will be used later for linking your Application to the KERNAL. It also defines a series of very convenient include macros.
The series of #inc_s calls that follow, are the macros for including constants and other macros found in the //os/s/ directory, and ending with the .s extension. Note how the macro takes just the name of the file without its extension.
The first include, app, is needed by essentially every Application (and every Utility.) It defines the base address where an Application is assembled to, it defines the offsets of the main vector table used by every Application, and the vector table used by every Utility. And lastly, it defines all the message commands that can be exchanged between Applications, Utilities and the system.
Colors brings in some useful color constants, so we don't have to use raw numbers. Ctxdraw brings in workspace memory addresses needed to perform drawing using the KERNAL's context draw system. Our Hello World Application will need to do this. Io brings in constants for the Commodore 64's main I/O chips, as well as some common blocks of memory used by C64 OS.
Pointer and Switch both bring in command and useful macros that we will put to use later in the code.
In keeping with being organized, the includes for KERNAL module constants is separated out from the others, just for clarity. Screen and Service are included because Hello World will draw some things on the screen.
Data Structures and ContentMuch of how an Application plugs into the services provided by the operating system is handled by creating standardized structures. The more complex your Application, the more likely it is to have more and different data structures. In this very simple Application we have:
- The main vector table for plugging into the OS at the lowest level
- A screen layer for hooking into low-level mouse and keyboard events and the ability to draw an interface
- A draw context, because we will use the context drawing system
The first line, *= appbase, declares where this Application is assembled to. Appbase is defined by the app include, so at this point we don't need to worry about where that address is.
The 5 vectors following the declaration to start assembling at appbase are the Application vector table. The vector table must come immediately at the start of the Application's main binary. Other structures are more flexible about where they are allowed to be in memory, because they get pointed to by other structures.
The Application vector table defines the following routines:
- Initialization
- Message handler
- Teardown/Cleanup
- Will be frozen to an REU
- Was just thawed from an REU
Every one of these must point to a valid routine. The KERNAL, and other processes, call these without checking if they are valid first. If you do not want or need to implement one of these routines you can use one of the standard carry-reponse routines. These are defined by the service include, and are found in workspace memory.
Routine | Carry | Notes |
---|---|---|
sec_rts | SET | Use when you want the response to be a set carry. |
clc_rts | CLEAR | Use when you want the response to be a clear carry. |
raw_rts | Unmodified | Use when you want to return without modifying any flags. |
You can see in the vector table for the two routines related to freezing and thawing from REU, because we are not doing any special work for these, they are specified as raw_rts. When some KERNAL code jumps through these vectors, the routine they reach will immediately RTS without modifying any processor flags.
Following the vector table is the screen layer structure and then a draw context structure, which we will discuss below. Lastly, there are two variables: a string, hello_s, and an integer, strcolor. In some Applications when there are many structures, state variables, and many string constants, it often makes good organizational sense to group these into sections in the code.
Initialization
When the Application is launched, the KERNAL jumps through the App's initialization vector. This should point to a routine where all of the set up code resides. Typical steps for initialization involve, initializing the KERNAL link table, allocating memory for a screen buffer, loading and linking libraries, loading icons, loading custom Toolkit classes, and building the user interface by instantiating Toolkit classes and nesting and configuring the objects.
In this very simple example Hello World Application, we will link to the KERNAL and push a screen layer, but we're not doing much else. No libraries, no Toolkit classes, and no Toolkit user interface is being constructed.
The call to initexterns initializes the KERNAL link table which is discussed in Chapter 4: Using the KERNAL → Linking to the KERNAL.
The call to layerpush comes from the Screen KERNAL module, and is discussed in Chapter 4: Using the KERNAL → Screen (module) → layerpush. A RegPtr to the layer structure is passed to layerpush.
A layer structure holds 4 vectors to 4 routines, plus a single byte which is set by layerpush with the index that this layer received when it was pushed to the screen layer stack. The screen layer's 4 vectors are the linchpin for integrating an Application with user input and output. The vectors are:
- Draw Notification
- Mouse Event Notification
- Keyboard Command Event Notification
- Printable Key Event Notification
These are all considered notifications because they are called to inform the Application about something; now is the time to draw; a mouse event has just been enqueued; a key command event has just been enqueued; a printable key event has just been enqueued.
This sample Application is so simple, it does not respond to any user input events. Each of these vectors is populated by the standard carry-response routine, sec_rts. Setting the carry is how these routines inform the KERNAL that the event was not handled by this screen layer and that it should therefore be propagated to the next screen layer.
We do need to know, however, when it is time for our Application to draw to the screen. The draw notification vector of our screen layer points to the routine drawmain. Drawmain is not called as part of the Application's initialization. It will be called repeatedly by the main event loop, later, throughout the life of the Application.
Teardown/Cleanup
The teardown/cleanup routine is officially defined in the vector table as "willquit". This vector is jumped through by the KERNAL when the Application is about to be quit and expunged from memory.
This is the routine in which things that were done during initialization get undone. Memory pools that were allocated, for any purpose, have to be freed. Shared libraries that were loaded have to be unloaded. Timers that were queued have to be canceled. There are some things that are handled automatically. For example, it is not necessary to pop the screen layer that was pushed, as that is handled automatically by the system. If Toolkit classes were loaded, it is only necessary to free the memory that was allocated for them, it's not necessary to unload them. Although, it may be necessary to delete certain instances of some Toolkit classes first. These are documented on a class-by-class basis.
In this example Application, because it is so simple, there is actually no teardown or clean up required. However, the placeholder code is created, because in the majority of real Applications there will be something to do here. We will see this routine put to use in the next, more complex, example Application.
Message Handling
Message Handling is lower-level than events. While events are delivered via screen layers that have to be explicitly pushed to the screen layer stack, messages come directly to the Application via the vector table.
You must have a message handling routine, but you are not required to handle any message types. In fact, the majority of messages sent to your Application will be of a type that will not be responded to. This is normal; the nature of messages is to loosely couple processes together.
For example, the Colors Utility could be manually opened by the user. It sends color selection messages to the Application. You are not required to handle these messages. However, if it's possible or sensible to respond to these messages then you should support them. Simply by responding to color selection messages, your Application becomes integrated with the system standard color picker and that makes your Application more of a C64 OS family member.
If a message is handled, in any way, it should be acknowledged by returning with the carry clear. If any message is not handled, either because it is not recognized or because the current context makes it an inappropriate time to handle it, the carry should be returned set.
Although it is never strictly required to handle a message, there are two message types which in practice need to be handled. These are the Menu Enquiry and Menu Command messages, which we'll look at next.
Messages are sent to the Application by jumping through the message handling vector with a message command in the accumulator. All currently valid message commands are defined by the app include.
In msgcmd, our message handling routine, we'll make use of the C64 OS switch macro defined by the switch include. This macro provides a flow control mechanism similar to a switch statement in C. Using this macro is optional but it is designed to make your code shorter and simpler.
The only parameter of the switch macro is the number of cases that follow. This number must precisely match the number of cases provided. Following the switch macro call is a byte table of case matching values, and following those is a table of pointers to case handling routines in the RTA format. If the parameter to the switch macro is 3, then there must be 3 entries in the byte table, followed by 3 corresponding routine pointers.
The switch macro switches on the value of the accumulator. It the compares the accumulator with each value in the byte table. If a match is found, it jumps to the routine at the corresponding index in the routines table. If a match is not found, flow continues immediately following the end of the routines table.
You can see here, following the routines table is an SEC to set the carry and an RTS. This is how it responds correctly (by returning with carry set) to every unrecognized message type. Before moving on to menu-related messages, let's follow the case of a color selection message.
Processing a Color Selection MessageThe user opens the Colors Utility and clicks a color. The Utility puts that color code in the X register and puts the message command mc_col in the accumulator. It then sends the message to the Application by jumping through appbase+appmcmd (the Application's message command vector.) Our msgcmd routine is called, the switch macro searches for mc_col in its case table. It finds it and jumps to the corresponding message handler, setcolr. The switch macro does not corrupt the contents of the A, X or Y registers, so the color code is still in the X register.
The new color code is written to our Application's global state variable, strcolor. Because this state variable has an impact on the appearence of our (albeit, very simple) user interface, we need to inform the system that our screen layer is dirty and requires a redraw. To do this, we read the index assigned to our screen layer into the X register and call markredraw, in the KERNAL. This marks our layer as needing to be redrawn.
Warning: Redraw vs. Markredraw
It is very important to note that the redraw process itself does not happen in response to the message! To attempt to redraw immediately, just because the color selection message arrived, would be very bad. It would corrupt the screen and could even lead to a crash.
The correct response to the arrival of the message is to make a change in the Application's model. The strcolor variable is a part of this Application's simple model. Because the model has changed, the system needs to be informed that the layer requires a redraw. So the App notifies the system that the redraw is required, but the redraw itself will wait until the main event loop, at the proper time, jumps through our screen layer's draw vector.
Handling Menu Messages
Menu-related messages come to our Application the same way that color selection and other messages arrive; they come via the message handling vector in the Application's vector table. The messages are sent to the Application by the KERNAL, as the user interacts with the menu system. If the Application doesn't handle those messages, the menu system will have no interaction with the Application.
There are two kinds of menu messages: Menu Enquiries (mc_menq) and Menu Actions (mc_mnu). Menu items may have a combination of two different states: disabled and selected. A disabled menu item is shown in the disabled color (defined by the theme) and cannot be triggered either by mouse or by keyboard shortcut. Selected is whether or not a menu item displays a checkmark to the left of its title. It is possible for a menu item to be both selected and disabled at the same time.
Menu items in C64 OS do not maintain their own state, instead they delegate their state to the Application. Although this may seem confusing to beginners, this pattern affords a great deal of flexibility and simplifies many situations that would otherwise be overly complex.
Every time the menu system needs to draw a given menu item, plus every time the menu system needs to analyze wether a menu item responds to a keyboard shortcut being processed, a menu enquiry message is sent to the Application. The menu item's action code is sent with the message in the X register. The Application must reply to the message indicating the current state of this menu item.
Menu EnquiryThe Application only needs to track the state of those menu items which are capable of being disabled or selected. For any menu item that isn't selectable and ought to always be enabled, the Application can simply return #0 in the Accumulator. Our simple Application has only one menu item, and it's always enabled and never selected, so our mnuenq routine just loads the accumulator with #0 and returns. We will see in the next sample Application how to make use of selection and disabling menu items.
Menu ActionWhen a menu item is triggered, either by mouse or by keyboard shortcut, a menu action (mc_mnu) message command is sent to the Application, with the menu item's action code in the X register. It is usually convenient to use the switch macro to switch on a number of different menu actions. It is useful to note, because a menu enquiry always precedes a menu action, the logic for whether a menu item should be enabled or disabled has already been performed. Thus, when a menu action arrives, your Application is guaranteed to be in a state that is ready to process that menu action.
In our simple Hello World Application, the only menu action code was "!", the menu item to Go Home. The switch statement's routine pointer, in this case, holds a reference to our KERNAL link table that links directly to the quitapp KERNAL call. No intermediate routine is required.
Drawing
Our Application has now, so far been initialized, which links to the KERNAL and pushes a screen layer structure to the screen layer stack. And it has a message handler that can respond to color selection change messages to update the data model and mark the layer as needing a redraw. The message handler also supports menu enquiries and responds to the one of the menu options by calling quitapp in the KERNAL.
The screen layer that was pushed during initialization has pointers to routines for drawing and for handling the three low-level input event types. Our Application is so simple that we're not handling any input events. But we do have a draw routine. The screen layer points to drawmain.
We never manually call drawmain. With the screen layer pushed, the system will call our draw routine some indeterminate number of times, and only when it is appropriate. All we have to do is implement the draw routine to perform the actual drawing.
You should review Chapter 2: Architectural Overview → The Drawing System to get a general idea for what drawing is all about.
In this Application, we are not using Toolkit, and we are not using a pre-rendered template. We are also using an unbuffered screen layer. We are performing manual drawing, using the KERNAL's context drawing routines, which are being applied directly to the operating system's screen compositing buffer. Following is the code, and below that an explanation for how this code works.
The C64 OS context drawing system is similar to using CHROUT ($FFD2) to write characters to screen memory, but more advanced with many more options and abilities.
The context drawing system is designed to make it easy to output characters to row/column coordinates. The size and memory address of the draw buffer are abstracted so that absolute addresses never need to be used. Outputs to the character buffer are synchronized with output to a color memory buffer. The system can perform automatic data transformations, such as translating PETSCII data to screen codes.
Additionally, the context drawing system can create constrained boxes, like a small window, inside the draw buffer. Outputs automatically clip to the bounds of the defined box and the internal coordinate system of one of those boxes can even be scrolled. But our Hello World Application will not make use of any of these advanced features.
Configure the Draw Context
Every time, before your Application is able to draw using the context drawing system, it must first configure the draw context. This is done by loading a RegPtr to a draw context structure and calling setctx in the KERNAL's Toolkit module.
Our Application defined its draw context structure right at the beginning in the data structures and content section of the code. The draw context consists of 11 bytes; 2 pointers to character and color buffers, 3 bytes which specify the buffer width, and the width and height of the bounding box, and lastly two 16-bit words that specify the top and left virtual scroll offsets of the bounding box.
Our draw context structure is very simple because, we are not using a screen layer buffer, we are not using a bounding box, and we're not doing any scrolling. The draw context looks like the following:
The first 2 pointers, scrbuf and colbuf, are defined by the io include. These pointers are to the operating system's screen and color compositing buffers, which means they are not private to our Application. We cannot rely on the content that we draw to these buffers to remain there. If a menu is opened, for example, it will overwrite the data in these buffers as the menu layer gets composited with our Application's layer.
The next three bytes, screen_cols, screen_cols and screen_rows are defined by the screen include. These are the dimensions of the C64's 40x25 character screen. These numbers are used because the screen compositing buffers are the full size of the screen. If our screen and color buffers were private, they could be smaller or larger than the real screen. Although, the context drawing system does limit these dimensions to 8-bit values, allowing a maximum layer size of 256 columns by 256 rows, (or 2048 pixels by 2048 pixels.) Lastly, the two 16-bit scroll offsets can just be set to zero and ignored.
Set Draw Properties and Clear the ContextEach character drawn results in some transformation. Depending on the context's settings, the character might be converted from PETSCII to screencode, and it might be reversed, then the character is output and the current color is filled into the corresponding coordinate in the color memory buffer. After drawing a character the cursor position is updated. The cursor can be configured to move horizontally (left to the right) or vertically (top to bottom.)
The draw properties are a set of bit flags loaded into the X register and a color code loaded into the Y register. Followed by a call to setdprops in the KERNAL's Screen module. In this Application, we read the color from our model variable, strcolor, which is updated by color selection messages from the Colors Utility.
It is not always necessary to clear the context. For example, if a redraw is going to completely cover and overwrite the context's draw area, then it's not necessary to do a clear context first. In our case, we are only drawing a short string ("Hello World!"), therefore it is necessary to clear the buffer of whatever else was already there. And remember, because this is not a private buffer, the clear operation is removing data that was composited from other processes over top of our data.
To clear the context, we load the fill byte into the accumulator, in our case, we load a space. And then call ctxclear in the KERNAL's Screen module. The fill byte fills the character buffer (according to the parameters of the current draw context), and the currently set color from setdprops is filled into all the corresponding color memory locations.
Hello World screenshot (with About This App open.)
Set Coordinates and Draw
With a configured draw context, freshly cleared, and draw properties already configured, we only need to draw our "Hello World!" string. To do this, we must specify the row and column coordinate for the first character to be output. These are referred to as the local row and column. The word local is used in contradistinction to global. The screen has a global set of rows and columns. A draw context's private buffer has a different (i.e., local) set of row and column corrdinates. And, in more complex scenarios, the local row and column could even be local to a smaller, offset, bounding box within the draw context's private buffer.
The local coordinates are given as 16-bit values, and it is necessary to provide the row first, then the column. Both coordinates are set using the same routine, setlrc in the KERNAL's Screen module. However, the carry is used to indicate whether you're setting the row or the column, the 16-bit value is passed as a RegWrd.
There are a couple of mnemonics in play here to make things easier to remember. The routine is called setlrc (set local row/column), to remind you that the row setting comes before the column. The "c" of carry is like the "c" of column. When the carry is set, it means column, and therefore when the carry is clear it means row.
In our Hello World Application, we only draw the string in the center of the screen, at a fixed row and column. Simple ideas that you could play around with would be to change these coordinates into model variables. You could then update these model coordinates from mouse events, allowing you to drag the message around the screen. Another idea would be to create a new menu option, perhaps "reset", which puts the message back to the center of the screen. Remember, anything that modifies the model values needs only call markredraw. Then, when the draw routine is called, use the model values to determine where to draw the message.
Once the corrdinate values are set, we need only loop over the message variable and output each character with a call to ctxdraw in the KERNAL's Screen module.
Each time you call ctxdraw, the draw cursor is progressed one row (or one column, if so configured in the draw properties). The character to be output is loaded into the accumulator. Calls to ctxdraw do not disrupt the X or Y registers, which leaves them free to be used by your Application to perform loops and indexing. After outputting all the characters of our message, there is nothing more to do but RTS.
Linking to the KERNAL
The final part of the Application is to link to the KERNAL. While it is not strictly necessary to link to the KERNAL last, it is organizationally good to stick to a convention of where code segments should be found.
It is only necessary to link to those KERNAL calls that are needed by the Application. In our Hello World App, we depend on 8 KERNAL routines.
• markredraw is used when the color selection message changes our model.
• layerpush is used during initialization to put our screen layer onto the screen layer stack.
• setlrc is used to set the coordinates for drawing the message.
• setdprops is used to configure the color and other properties prior to drawing.
• ctxclear is used to clear the buffer of previous composited content before drawing our message.
• ctxdraw is used to output the characters of our message to the screen buffer.
• quitapp allows our Go Home menu item to tell the Application to be quit.
• setctx is used to prepare the draw context for drawing our message.
Note that there are only 9 JSRs in the entire program, and every one is to a KERNAL call, with one exception. At the top of init is a JSR to initextern. This is not a call to the C64 OS KERNAL, but is a call to a low-level C64 OS component. Its address is defined by the app include, and the address is at a statically allocated, fixed address down in workspace memory. The address is static because this is the routine that allows our Application to dynamically link to the KERNAL.
See Chapter 4: Using the KERNAL → Linking to the KERNAL for more an explanation of the structure of the KERNAL Link Table shown above.
Tutorial 1: Conclusion and Summary
In this tutorial we learned how to create an Application bundle. We learned how to format the about metadata file. We learned how to create a 3Icon for our Application. And we learned how to create and format a menu definitions file.
Next we learned how to create and organize the main Application binary, which is composed of several parts. We learned about essential headers and includes. We learned about data structures, content and data model variables. In the code section, we learned about initialization and teardown and cleanup. We learned about message handling. And we learned how to do simple drawing using the context drawing system. Lastly, we learned about where to put the KERNAL link table.
Our Application should be buildable using TurboMacroPro, natively on the C64, or using TurboMacroPro-Cross (TMPx) for cross assembly on a Mac or PC. Below is a link to a zip file containing all of the resource files in this tutorial.
ZIP file contents:
Path | File | Notes |
---|---|---|
/ | Hello World screenshot.png | What the App should look like when running. |
/Hello World/ | about.t.S00 | S00-wrapped about.t SEQ file, PETSCII. |
/Hello World/ | icon.charset.a.P00 | P00-wrapped icon.charset.a PRG file, TMP source-binary. |
/Hello World/ | icon.charset.S00 | S00-wrapped icon.charset SEQ file, 3Icon. |
/Hello World/ | main.a.P00 | P00-wrapped main.a PRG file, TMP source-binary. |
/Hello World/ | main.o.P00 | P00-wrapped main.o PRG file, C64 OS Application binary. |
/Hello World/ | menu.m.S00 | S00-wrapped menu.m SEQ file, PETSCII, menu definitions. |
/Hello World (raw)/ | about.t | about.t SEQ file, PETSCII. |
/Hello World (raw)/ | icon.charset.a | icon.charset.a PRG file, TMP source-binary. |
/Hello World (raw)/ | icon.charset.a.txt | icon.charset.a Text file, ASCII. |
/Hello World (raw)/ | main.a | main.a PRG file, TMP source-binary. |
/Hello World (raw)/ | main.a.txt | main.a Text file, ASCII. |
/Hello World (raw)/ | menu.m | menu.m SEQ file, PETSCII, menu definitions. |
The "Hello World" directory and its contents are ready to be put on an SD2IEC, (which implicitly unwraps the .S00 and .P00 wrappers,) and run as an Application on C64 OS.
The "Hello World (raw)" directory and its contents have been unwrapped to make it easier to open them in a HexEditor or PETSCII-TextEditor on a Mac or PC. The .a files (unwrapped) can be viewed using tmpview. And the two source code files have been pre-converted to ASCII using tmpview, for easily examining in an ASCII-TextEditor.
Next Section: Writing an Application: Tutorial 2: TestGround
Table of Contents
This document is subject to revision updates.
Last modified: Apr 04, 2024