NEWS, EDITORIALS, REFERENCE
Anatomy of a Utility
A quick update, I am working on Beta 0.5 of C64 OS. Everyone is waiting for the File Manager to be ready. Or, if not ready, at least to have something to see and try.
C64 OS at its heart is a set of reusable resources that are designed to work together. The ideal is that an application is not written from the ground up, but by pulling together resources of the OS. I have designs for the File Manager's UI. I have the main application framework created. You can launch into File Manager. Its menus are created, navigable and some of them already function. But beyond that, there isn't much to see yet.
There are two high-level technologies important to implementing the File Manager:
- The Directory Library and
- The UI Toolkit
The Directory Library is done, tested, working well and in use already in a few places. I will be talking more about the Directory Library in this post, as I explain putting it to use in building a C64 OS Utility.
The UI Toolkit is one of the 10 C64 OS KERNAL modules. It consists of the KERNAL module itself, plus 12 object-oriented user interface classes. It has been a challenge for me to design this system. I had a couple of false starts because I didn't know what I was doing or how to create an extensible object-oriented framework in 6502. But once I worked out how to actually do it, it has been a long slog to get the classes implemented, testing them, working out the bugs, and the math, and figuring out how all the parts fit together. It's been fun and educational, but slower going than I'd hoped.1
I have been doing a lot of dog-fooding. This is an old programmer saying—I think it originated at Microsoft of all places—from "You should eat your own dog food." In other words, if you make something that others are going to use, you should use it too so that you find all of its shortcomings, and find out for yourself just how gross and unpalatable it is.
The Toolkit has taken a lot of dog-fooding to get it to the point where I am happy to program against it. It is supposed to make programming easier and faster, and provide more benefits than headaches. I think I've finally reached that point where the benefits are about to start pouring in. I am pleased to say that most of the Toolkit classes (the ones that will be permanently resident, more on this later in this post) are done and working. And there are just two classes left that I still need to finish before I can say that the entire core OS is complete. That's not bad, considering how much I've put into it so far!
The File Manager is coming. But it is being built by combining together many components and technologies that are part of C64 OS as a whole, and each of those parts is being tested and put to use in a simpler context first. And that's what we're going to look at today.
So let's talk about building a C64 OS Utility, from stem to stern.
- By the way, in case you don't know, I'm doing 100% of the development on a Commodore 128 with a 2MB REU. So, let us just say, when something goes wrong, figuring out how to debug the problem has been a very creative process.
A C64 OS Utility is the rough counterpart to a GEOS desk accessory. A C64 OS Utility is opened concurrently with the Application you're running, it typically presents itself in a floating movable palette. Some Utilities are standalone and can be used like mini Applications that you can access without leaving the current Application. Others are designed to exchange information with the Application to supplement or enhance its functionality.
The first Utilities I wrote were programming tools for myself. Memory, monitors memory usage and tests features of the OS, such as the dynamic memory allocation, scheduled timers, and low-level mouse events. Peek, allows you to inspect memory while C64 OS is up and running, which has been an invaluable tool for programming and debugging. Especially for debugging the Toolkit.
It has long been on my list to write a blog post about how to build a Utility. The problem is that the early Utilities were written from scratch. Only after writing several Utilities from scratch could I start to identify their similarities and worked out a process to make creating them easier and more consistent. Most recently, I have built out the Toolkit enough to be used to build Utility user interfaces. I've been taking older Utilities—the ones built as tests of OS features—and porting their UIs to Toolkit. I'm now confident that the way I'm building Utilities is how they will be released in v1.0.
Opening a Utility
C64 OS has a customizable Utilities menu, pulled down from the top left corner of the screen. Utilities that you frequently access, or that you want to have access to from within any Application, can be put in this menu.
Each menu item in the Utilities menu consumes 32 bytes—or 1/8th of a 256 byte page—of memory. If you want, you can load it up. Let's say you put 40 utilities in that menu. To do so would permanently consume 5 pages of memory. This would deprive the Application of that much free memory, which is a sacrifice you may not be inclined to make. Since the Utilities menu consumes memory in page-sized chunks, if you configure it for even one Utility you may as well configure 8 Utilities and you won't have wasted any additional space. The v1.0 C64 OS release will have a default configuration of 8 Utilities.
What about all the others? Well, that's easy, one of the 8 will be the Utilities Utility, which I have recently written. Launch it and it will present you with a list of all the other Utilities. However, that full list of all Utilities only gets loaded into memory temporarily. You get all that memory back as soon as the Utilities Utility is closed.
To keep this discussion grounded in something real, we'll use the Utilities Utility for the sample code. Here is a short video I uploaded to Vimeo showing an early build of it. Some improvements have since been made, but this will give a general idea of what is being created by the description given in this post.
Parts of a Utility
Here are the parts that go into the Utilities Utility:
- Includes for macros, constants and system calls
- Assemble-time options
- Main vector table
- State variables
- View Store
- Screen layer
- Mouse events
- Key command events
- Printable key events
- Layer index
- Draw context
- Toolkit environment
- Persistent config variables
- Content resources (strings, constants, icon-table, etc.)
- Utilities framework include
- Init routine (allocate memory, instantiate UI, etc.)
- Dynamically linked TKList Class
- Dynamically linked Directory Library
- Quit routine (deallocate memory, close files, free resources, etc.)
- Draw routine
- Custom logic routines...
- Mouse event handling routine
- Key event handling routines
- Message event handling routine
- Dynamically linked KERNAL jump table
- Draw context buffer
That looks like a lot, doesn't it? Just for a Utility which looks rather simple, one little list of filenames and a couple of buttons at the bottom. But of course, it isn't as simple as it looks. It can be opened concurrently with any Application that is running. It presents itself in a floating palette, that can be dragged about the screen without interfering with the Application's own UI. That little list can be quite long, where does it get the memory from? How does it read in the files from a directory? How does it sort the directory entries? How does it know which device number, partition and directory path to read from? It's a mouse driven UI, how does the hittesting work, given that it can be moved about the screen? How does it know when to redraw itself? How does it talk to the Application, or get a command from it?
Let's walk through the list of parts and talk about what they do.
Includes for Macros, Constants and System Calls
You need to access system resources. Some of these are static memory address down in workspace memory, and some are constants and macros, and more. The first thing included is the KERNAL modules header, like this:1
On the C64, a TMP line cannot overflow a 40 column screen line, and while labels sit flush left, everything else (such as ".include") gets indented. Thus, the reason my path names are so short, "os" rather than "C64 OS" and "h" rather than "headers" is to ensure that this actually fits on one line. Some of the limitations that will be discussed wouldn't be limitations if the development was not being done on a C64/128, but that's half the fun! So we'll stick to keeping those considerations in mind.
All references to files are delivered directly to the storage device's DOS. So, while originally an assembler command like .include probably assumed it would only take a filename, it works just fine to use a fully qualified path. The above path syntax originated on the CMD storage devices but became a defacto standard and has been carried through to the IDE64 and SD2IEC. You can read all about this in the SD2IEC User's Manual.
Typing out full paths is a pain, deeper path references can bump against the line limit, and you also want to minimize the repetition of an absolute pathname. What if you were to install your instance of C64 OS in a directory named "cOS1" or something? You need only include modules.h with an absolute path reference, and it defines a set of macros:
#inc_s "ctxcolors" ;resolves to: .include "//os/s/:ctxcolors.s" #inc_h "file" ;resolves to: .include "//os/h/:file.h" #inc_k "mem" ;resolves to: .include "//os/s/ker/:mem.s" #inc_tks "tkobj" ;resolves to: .include "//os/tk/s/:tkobj.s" #inc_tkh "tkview" ;resolves to: .include "//os/tk/h/:tkview.h"
These macros make including resources short and easy. The purpose of each of these paths is being documented in more detail in the C64 OS developer documentation.
C64 OS provides numerous includable resources. Pick and choose the ones you need for the functionality of your Utility. Some common ones being included by Utilities are:
#inc_s "app" #inc_s "ctxcolors" #inc_s "ctxdraw" #inc_s "dir" #inc_s "file" #inc_s "memory" #inc_s "service" ... etc.
There are only three of these, at the time of this writing. These switches control some useful functionality that will be assembled into the Utility.
swapicon=1 istkt=1 confglob=1swapicon
C64 OS's main user interface is built out of a customizable character set. Of 256 characters, 18 are left undefined, to be defined by the Application and/or the Utility. One problem is if the Application uses a bunch of those 18 characters for its own custom purposes, but then you open a Utility that also wants to use some of those for its own icons. If you turn on swapicon, the utility framework (we'll get to that soon) will automatically backup all 18 custom characters and restore them from backup when the utility is quit. This allows you to overwrite these at your leisure, without worrying that you may be clobbering something the Application is using.
It gets better than that. C64 OS provides a library (a collection) of useful, runtime-loadable, 8x8 pixel icons and a KERNAL call to install them. You make a table that holds entries with the character set index, an XOR bitmask, and a constant from the Icon Library. The KERNAL call installs the icons for you, and will also send a message to the Application with a pointer to your icon table. The Application can do whatever it wants. It can ignore the message, it can do a character substitution, or it can inspect the table to see if the characters the Utility is using collide with any of the characters it uses. The Application will be messaged again when the Utility quits.istkt
The istkt option allows you to specify if you are using a Toolkit-based UI. If so, the Utility framework will handle updating the Toolkit environment automatically during the Utility's draw cycle.confglob
The option confglob allows you to specify whether you want your Utility's persistent configuration variables to be stored once, globally, for the whole operating system or whether a unique set of config variables should be saved for each Application the Utility is opened with.
Global config files are written to //os/settings/:filename.i and Application-specific config files are written into the application's directory bundle. For example, //os/applications/Gallery/:filename.i
Main Vector Table
Like Applications, Utilities begin with a main vector table to critical features necessary to integrate the Utility into the rest of the system. The vectors are:
identity init quit message
Init, quit and message all point to routines. Identity is the only one that points not to a routine but to a null-terminated string.identity
Let's start with identity, since it's the most straightforward. A Utility is loaded and run using a KERNAL call, by passing a pointer to a string containing the name of the Utility to run. The OS tracks how high memory is being used. (In C64 OS parlance that's the 8KB block below the KERNAL ROM). Utilities always install themselves into high memory, at $E000 and up. If a Utility is currently running, the loadutil KERNAL call compares the string passed to it to the Identity string of the running Utility. If they match, nothing happens as that Utility is already loaded. Otherwise, steps are taken to quit any previously loaded Utility and load and run the new one.init
After a Utility is loaded, it is run by calling the routine pointed to by init. You could set this directly to your own routine and do whatever you want during init. But after I created several Utilities by hand, I identified many commonalities and put them into the Utility framework so we don't have to write this stuff from scratch each time.
Since we'll be using the Utility framework, we'll point the main vector table entry to initutil. A Utility has some standard customization options and usually some truly custom requirements too. The assemble-time options discussed earlier handle the standard customization. At the end of initutil, the framework calls extinit which stands for "Extended Initialize". So you put "initutil" in the main vector table, and then implement your own routine called "extinit". The only thing you have to do in your extinit routine is the truly custom stuff, which we'll discuss below.quit
The quit vector is handled exactly the same way as init. If you're using the Utility framework, you plug "quitutil" into the vector table, and implement your own routine called extquit to handle just custom stuff. Typically during extquit you will free resources that you manually allocated.
We'll return to talk about what else the framework does upon quit.message
This is the routine through which the system and the Application send standard communications to the Utility. Application's also have a message entry in their main vector table, which allows the system and Utilities to send messages to the Application.
A message consists of a message type code passed in the Accumulator. The meaing of the X and Y registers depend on the message type. Message codes are defined in //os/s/:app.s. Some example message codes are:
mc_mnu (A menu item has been triggered) mc_menq (An enquiry about a menu item) mc_col (A color selection) mc_fopn (A file reference for open) mc_fsav (A file reference that was saved) mc_stptr (A request for a status string pointer) mc_hmem (A notice that high memory usage has changed) mc_theme (A notice that the UI colors have changed)
I usually use a routine called procmsg (process message) for handling these message events. We'll come back to discuss messaging passing in more detail, below.
Next come what I refer to as state variables. These are not the only variables used by the Utility, these are the major global variables that will be used by the whole Utility. Other variables can be defined local to their routines.2View Store
We'll come back to this later when we talk about building the Toolkit UI. However, in brief, there are usually some Toolkit objects that once configured don't need to be directly referenced again. For example, a button that is initialized, anchored, inset, assigned a title string, given a click action callback and then appended to a parent view, can often just be forgotten about. If the user clicks around in the UI, the Toolkit internally handles the hittesting and the event handling, and if the button gets clicked the callback routine will be called, which will give you access to the button again at that time.
There are other Toolkit objects, though, that you will want to have a handle on. For example, perhaps you'll want to disable a button depending on what is selected in a list. While the user interacts with the list, your custom logic needs to have a quick reference to that button object. You can create a separate label and a have a separate variable for every object you need access to, but this can also lead to a proliferation of labels, and you need to come up with unique names for them all. In my experience, this is not very convenient. To help with this I've created standard macros, #storeset and #storeget. A "Store" is effectively just a statically allocated array of 16-bit items, with just a single label for the whole store. I use this for holding convenient references to Toolkit objects. Like this:
views .word 0,0,0,0 ;Enough for 4 object pointers.
The macros read and write RegWrds (16-bit word, stored in the X and Y registers.) These RegWrds are ready to be used by other routines, or are produced by other routines. Like this:
#storeget views,2 ;Grabs pointer to view 2 as a RegWrd jsr ptrthis ;Makes the RegWrd pointer the "this" in object-oriented land.
We'll come back to talk about this more later.
Important State Structures
There are three important structures that should be found in the state variables section of the Utility.
- Screen Layer
- Draw Context
- Toolkit Environment
The Toolkit environment is only necessary if the Utility uses a Toolkit-based UI. Our Utilities Utility uses Toolkit so we'll discuss it.Screen Layer
C64 OS does not support multiple "windows", it doesn't have a "window manager" proper, because, frankly, I've discussed this many times before. It's the same reason why an iPhone or an iPad doesn't have multiple overlapping resizable windows the way a desktop computer has. What works from a UI point of view on a screen that is huge isn't necessarily suitable on a screen that is small. The C64's screen resolution is significantly smaller than the resolution of an Apple Watch!
Instead, C64 OS's screen is built up (composited) from a stack of layers in a fixed order. There are 4 layers. The application's main UI occupies layer 0. The menu and status bars occupy layer 3, leaving two layers in the middle. One of those two layers is for a Utility, and the other is reserved for the future, for advanced applications that want to push and manage an extra screen layer.
A screen layer consists of a set of pointers to manage drawing (onto that layer), and receiving mouse and keyboard events. You define the screen layer and give it a label of scrlayer. The Utility framework pushes and pulls it from the system's screen layers stack for you. If you have advanced or custom drawing to perform, you can point the draw vector to your own drawing routine. But if your UI doesn't need anything advanced you can simply point it at drawmain. Drawmain is defined by the Utility framework and will update the Toolkit UI for you. We don't need any custom drawing in the Utilities Utility.
If a Utility does not use the Toolkit, you would have to implement your own hittesting in the screen layer's handler for mouse events. This is what I have done for most of the other Utilities up until this point. In this Utility though, we're using the Toolkit. We still need a routine to handle mouse events. However, as we'll see later, its primary role will be to forward the event notice on to the Toolkit.
The screen layer's vectors for key events can similarly be used to manually implement support for the keyboard. But again, the main role of these routines will be to forward the event notices to the Toolkit. You can also combine manual keyboard support with the Toolkit's built-in functionality. For this Utility though, we will use only the Toolkit.Draw Context
On a C64, everything in memory is open, so you can technically write data to screen memory at any time. However, this is not a suitable way for multiple processes to share the screen. What's worse is that direct writes are not even a convenient way to draw to the screen. For example, the screen has a fixed width of 40 characters, but a Utility doesn't take up the full screen, it usually sits in a small rectangular box. You want to be able to draw relative to the proportions of that box, and then you want the box to be mapped to anywhere on the screen, including being clipped at the edges of the screen.
If you put all this together, you will realize that drawing directly to screen memory is actually a huge pain and would result in very complex code. The drawing context solves all of this. Your Utility needs to have a drawing buffer. This could be dynamically allocated, but it doesn't need to be. I typically use the free space that immediately follows the Utility's code, which will put it somewhere underneath the KERNAL ROM. This is memory that can't be dynamically allocated by C64 OS anyway, so this is the perfect spot. Due to how the VIC-II works, you need 2 buffers: one for screencodes and one color data. Decide how big you want your Utility to be. Let's say this one is 19 columns by 14 rows. That's 19x14 or 266 bytes for screencodes plus an additional 266 bytes for color codes.
The draw context is a structure that records these details. A pointer to the start of the each buffer, the width and height. You can read more about the draw context in the the post Context Drawing System.
There are system calls in the Screen KERNAL module to configure the draw context to draw within a clipped, offset and scrolled region of those buffers. You can use these KERNAL calls to do manual drawing. However, even if you don't do any manual drawing, you still need a draw context, because the Toolkit UI classes backend all of their drawing on the context drawing system.Toolkit Environment
The Toolkit consists of a set of classes that are always available in memory, plus the ability to dynamically load custom classes that are not permanently resident. Additionally, the Toolkit KERNAL module pulls these all together, supports their object-oriented nature, and connects the world of the Toolkit object node tree (the nested UI you assemble) with the lower level parts of C64 OS.
There can be more than one Toolkit environment that is active on screen at one time. For example, the Application's main UI (on screen layer 0) could be made up of Toolkit objects, and the UI of a Utility on screen layer 1 could be made up of a different hierarchy of Toolkit objects. There are a number of properties that a Toolkit environment manages. The first is its draw context, as we just discussed. You put a pointer to the Utility's draw context into the Toolkit environment structure. The TKEnvironment also has a pointer to its root view. Each Toolkit UI begins with a single root view, into which other views can be nested. We'll come back to this.
The Toolkit also uses the TKEnvironment struct to store temporary state information, such as the view that is currently in keyboard focus, or the view that was hit and took the mouse down event so that it knows how to route subsequent tracking and mouse up events.
Persistent Config Variables
This section should immediately follow the State Variables, and actually intersects with the bottom of the Toolkit environment struct. The bottom portion of the Toolkit environment plus whatever other variables you want to persist go below the confstart label. When the Utility is closed, the Utility framework automatically saves all of the data from the confstart label upto the the number of bytes specified by confsize. Where it saves this depends on the assemble-time option for global or local configuration.
When the Utility is opened, its config file will be looked for. If found, it gets read in automatically. If it is not found, the default values you have in the Utility's binary will be used. This is entirely transparent to you. So you hardly have to think at all about saving and restoring your Utility's config variables. The only thing you need to be aware of is that once saved, the saved config file will be restored. So you don't want to just willy-nilly rearrange the order of the config variables in your source code. Otherwise, the restore from a saved config will put the values into the wrong places.
You are always safe to add new config variables to the bottom of the config section. An old config file may load in and populate the upper config variables, leaving the new ones with their default values on the first load. The very next save will update the config file with the new complete set of config variables.
If you are going to undergo a major version change and introduce a completely new set of config variables, there is a way to deal with this. You must specify in your Utility the name of your config file. It should be based on the name of your utility, in order to minimize conflicts with other Utilities, and it should have the extension .i, which is the C64 OS standard for non-program binary data, that doesn't match any other known file format. (I chose "i" for initialization data.) You could include a version number in the config filename. So our Utilities Utility could save its config to "utilities1.i" for its first version. Down the road, if we made a major and backwards-incompatible change to its config variable structure, we could just change the name of the config file to "utilities2.i"
These sections are not all strictly necessary. But I like to keep my code organized, and I like there to be some consistency in the organizational structure across my different Applications and Utilities, so I know where to find things and I don't need to reinvent the wheel when deciding where to put things.
I use this section for content constants. These generally come in the form of strings that will be used in the labels, buttons and tabs of the UI, as well as custom icon data or the icon table for the system's Icon Library.Strings
Similarly to the #storeget and #storeset macros, I have macros for string lists. You could give each string a label but if you have a lot of strings, you get a lot of labels. Since TurboMacroPro for C64 limits the number of labels per source code file, you don't want to waste them on a bunch of strings. Here's how my strings definition looks:
#inc_s "string" .null "OK" .null "Open" .null "Cancel" .null "Go Home"
The .null pseudo-op puts a 0-byte at the end of the string data. This terminates each string and also delimits the strings. The content between quotes is encoded in PETSCII. The "string" include must come immediately before the block of strings and it imports two macros, getstr and getstrxy. The first returns the string in A/X (lo byte/hi byte) which is sometimes preferable to X/Y, especially when using Y in a loop or as a pointer index. So you could do the following to set the title of a button that is currently this.
ldy #settitle jsr getmethod #getstr 2 ;Cancel jsr sysjmp
Preparing a method to be called, and actually calling it are two steps, because you want the space between them to setup the input parameters to the method.Icon Library
As mentioned earlier, your Utility might want to define a custom icon, or load in some icons from C64 OS's Icon Library. Define a table of icons, like this:
icns .byte $78,$ff,ci_trash .byte $79,$00,ci_calendar .byte 0
That table defines two custom icons, a trash can and a calendar. There are currently around 65 standard icons to choose from. And more will come as I think about them. The first byte of each row is the character set index the icon will be loaded into. The available character set indexes for custom icons are from $78-$7F and $F8-$FF. These are also the characters that get automatically backed up and restored if you use the assemble-time option, swapicon. The second byte is an XOR mask. Typically you'd set this either to $00 to get the normal version, or $ff to get an inverted version. Inverted versions are useful if you'll display the icon on a button, for example. And the last byte is the icon you want to load. The icons are defined in //os/s/:icons.s
Load the icon table like this:
#ldxy icns jsr loadicons
This simultaneously messages the Application with a pointer to your icon table. The original icons will be automatically restored when the Utility is quit, and the Application will be messaged again at that time too. You can now use the characters you've defined in your icon table in your strings. Like this:
#inc_s "string" .byte $78 .null " Delete"
String #0 is now a trash can followed by a space and the word "Delete". Beauty.
If you use the icons from the Icon Library, your Application or Utility will have a consistent look with other Apps and Utilities. It will automatically acquire any new appearence if the system icons get updated in a future version. Plus, you don't have to design and common icons yourself.
The Utility framework is a chunk of source code that you include, and it gives you free functionality and makes your Utility behave like all the others. You don't have to use it, but... doing everything manually is a big pain, and the result will be something non-standard. Here's a list of some of the features provided by the Utility framework:
- Dynamically link KERNAL jump table
- Panel and titlebar colors taken from Theme
- Drag panel around by its titlebar
- Click the leftmost cell of the titlebar to quit
- Config variables get loaded and saved automatically
- Control-click the titlebar to toggle window shade
- Performs mouse events bounds checking, with support for transparent regions
- Click off the panel to prevent it from intercepting key events
- Backs up and restores custom icon set
- Calls toolkit update on the draw cycle
There is still work for me to do here to make it easier to put together a Utility, but the Utility framework is very handy. You can include it anywhere after the main vector table. I usually include it just before the start of code, following the Content Resourses section. Include it as follows:
Since we're using the Utility framework, we point the screen layer's init vector at initutil. That performs many useful common initialization tasks, then calls extinit. You must implement extinit. Even if you have nothing extra to initialize, (unlikely) you still need the extinit label, but it can just have an RTS.
There are several things we need to do during initialization for the Utilities Utility. This is highly custom work, what gets done here is entirely dependent on what the Utility is meant to do.
- Load/relocate/link the TKList class
- Load/relocate/link the Directory Library
- Allocate memory for Toolkit objects
- Instantiate the user interface classes
C64 OS includes several built-in classes that remain resident in memory all the time. We have limited memory, however, and there is an infinite variation of UI classes that we might want to use in our user interfaces. Therefore, some less frequently used classes are available as relocatable binaries.
Here are a couple of cool traits of relocatable classes: They dynamically link to the KERNAL modules (as do libraries, drivers, utilities and applications.) They dynamically link to their superclass at relocation time.
Other than those internal behaviors, a custom class is loaded just like any other relocatable binary. Into dynamically allocated memory, page-aligned, but with the link routine coming first. Before calling loadreloc, you must put a pointer to its superclass into the Toolkit's class pointer. After relocation and dynamic linking, the class pointer has been updated to point to the newly loaded class. In order to instantiate this class later, or to deallocate the memory into which the class was loaded, you should copy this pointer to a local variable.
I'll describe some of what's going on in this code, below, when loading in the Directory Library, as the setup for the relocation is done the same way for both of these.Load/Relocate the Directory Library
The Directory Library is located at: //os/library/:dir.lib.r And it is loaded the same way any other relocatable is loaded. Just like the custom TKList class, above. Let's see how that's done.
You have to know how big the relocatable is. This can sometimes be determined by its block size on disk, but not always. The Directory Library is 5 blocks on disk, but requires 7 contiguous pages of memory. It uses the final two pages as working space for sorting pointers to the directory entries. The additional space required for the directory entries themselves is managed by the Directory Library, so you don't need to worry about that.
All relocatables load essentially the same way.
- Request a number of contiguous pages from the memory allocator
- Initialize the block as a system file reference
- Append a relative pathname to the path component
- Copy the filename into the file reference
- Setup any required input parameters (superclass pointer into class, for example)
- Call loadreloc, passing a RegPtr to the file reference
- Save the address it was loaded to
Here's how this is done for the Directory Library.
Why are we doing this? Why do we need a system file reference? Because the user is allowed to change the name of the system directory. You must never depend on it being its default (//os). And it could be on any device or partition. So we will get a system file reference, append it with the relative path /library/ and copy in the filename of the library. The file reference now correctly refers to the library we want to load. And the relocatable will be loaded overtop of the file reference and into the pages that follow it.
The macros are extremely handy. They shorten the code, make errors less likely, and make the code much more readable. The routines have been designed, as well as I can, to make them flow into one another. For instance, getsfref has two memory allocation options. If you pass 0 in the Y register, it will automatically allocate 1 page of system-owned memory, configure that page as a file reference and return the page number in X.
But you can also allocate memory manually, and pass the page number to use in Y. getsfref will configure that page as file reference, and still return the page number in X. Because we want our relocatable Directory Library to load into a 7-page block, we use pgalloc to allocate 7 pages. pgalloc returns the first allocated page number in Y. Well, that's perfect, because Y is the input parameter for getsfref.
The next little trick is that, getsfref, in addition to returning the page number in X, also returns Y as the index of the null byte of the file reference's path component. Write X to the ZP pointer, and Y is already primed to append to the system path. The macro #stradd takes a ZP pointer and a pointer to a null-terminated string. #stradd uses Y as the index into the ZP pointer and X as the index into the null-terminated string. This makes it very easy to append "/library/" to the directory path configured by getsfref.Dynamically Link the Directory Library
There is one other step before the Directory Library can be used. The Directory Library begins with a jump table. This is similar to the KERNAL modules which each begin with a jump table. In order to dynamically link to the KERNAL modules your Utility needs to have its own jump table, usually I put this at the very end. And we'll come to that, later.
Just before the KERNAL jump table, I'll specify the jump table entries from the Directory Library. like this:
freedir jmp freedir_ readdir jmp readdir_ confcmp jmp confcmp_ presort jmp presort_ sortdir jmp sortdir_ drecord jmp drecord_
The JMP absolute instruction can only take a 16-bit address. Therefore the assembler will produce a 16-bit argument, even though the constants (the ones with the trailing underscores) are 8-bit numbers, like 3, 6, 9, 12, etc. The assembler will set the JMP address's high byte to $00. Once we know where the Directory Library has been relocated to, we must link to it by overwriting these $00 high bytes with the real page number. This should be done immediately after the call to loadreloc. Like this:
jsr loadreloc ;Dynamically link the jump table ;loadreloc returns the page number in A sta freedir+2 sta readdir+2 sta confcmp+2 sta presort+2 sta sortdir+2 sta drecord+2 rts
Now, our calls throughout our Utility to, say, freedir will go to our jump table, which will go to the relocated page address plus the offset into the page for the freedir jump table entry in the Directory Library.Allocate Memory for Toolkit Objects
We have our Toolkit environment structure, and it has been configured with a pointer to the Draw Context for the Utility. Additionally, it needs a memory pool. To use the Toolkit, you will instantiate classes to create user interface objects, and some of those objects will instantiate other classes automatically. All such instantiations are made by calling tknew. In turn, tknew reads the size of an object from its class definition and allocates precisely that number of bytes with a call to malloc.
Malloc is a much more fine-grained way of allocating memory than the page allocator that we have mostly seen and used up to this point. Malloc will search out a free chunk of memory that is precisely the right size, and mark that arbitrarily sized chunk as occupied, carving it off from a larger chunk called a memory pool. Before you can use malloc, you have to specify the memory pool within which it will search for free space, and out of which it will make its allocation.
A memory pool is very simple. In fact, we've already worked with them. Every time you use the page allocator to allocate one or more contiguous pages, the first couple of bytes of the first page have a signature written into them allowing them to be used as a memory pool for malloc. The question is, how big should you make the memory pool? If you make it too big you'll just waste space. But if you make it too small to fit all of your Toolkit objects, it will throw an exception if the pool runs out of space. This will crash your Application, unless you are anticipating the possibility, catch the exception and deal with the lack of memory gracefully.
I usually draw out the user interface I want on graph paper. I do a little napkin math to add up the sizes of all the objects I'm likely to need, and round up to the nearest page. The Utilities Utility only needs around 350 bytes, or 2 pages leaving a bit left over.
lda #memutil ;Owned by Utility ldx #2 ;2 Pages jsr pgalloc sty tkenv+te_mpool #ldxy tkenv jsr settkenv
The call to pgalloc returns the allocated page number in Y. Write this to the memory pool property of the Utility's Toolkit environment. Before we are able to create or work with Toolkit objects we have to set the current Toolkit environment, which is accomplished with the last two lines above.Instantiate the User Interface Classes
It is now time to build our object-oriented Toolkit-based user interface. Before we actually start instantiating classes, we should have some basic gist for how this is going to work.
The Toolkit environment has a reference to the Draw Context for this Utility. It also holds a reference to the single root view that we will instantiate. The root view's anchoring and insets are relative to the bounds of the whole Draw Context. So, let's say our Utilities Utility has a panel that is 19 columns wide and 14 rows high. The Draw Context will look like this:
drawctx .word charmap ;pointer to start of screencode data (statically allocated, at end of code) .word colmap ;pointer to start of color code data (statically allocated, after charmap) .byte 19 ;current clip box width .byte 19 ;buffer width .byte 14 ;current clip box height .word 0 ;Scroll Offset Top .word 0 ;Scroll Offset Left
Naturally, the current clip box width starts the same as the buffer width, and the top and left scroll offsets start at 0. These get adjusted by Toolkit as its nested objects draw themselves relative to each other.
We don't want our root view to start drawing in the zeroth row of the Draw Context buffer, because the buffer contains the whole Utility panel including its titlebar. We might also want a nice border on both panel sides and below the titlebar. I like to remove the bottom border so the buttons sit flush with the bottom of the panel. I think it looks cool and modern, and it saves screen real estate. It'll be like this:
Red indicators show positioning result of the root view's anchoring and insets.
In order to get a nice border around our UI elements, it would be ideal to have the root view only draw within the area indicated by those red markers. How do we accomplish this? It's very easy. We set the anchoring to TOP|BOTTOM|LEFT|RIGHT, so that the view dynamically resizes to stay connected to its parent on all four sides. But then, we set the offtop property to 2, and the offleft and offrght properties both to 1. We can leave the offbot property at its default value of 0. TKView defaults its anchoring to TOP|BOTTOM|LEFT|RIGHT and all the offsets to 0, so we only need to override the properties that will not be their defaults.
Now that we know how that will work, let's instantiate the root view.
That's it! Isn't that clean? That is really clean. We are writing object-oriented code in 6502 assembly. Very cool stuff. Now let's figure out how this works.
The root view has to be a TKView, but due to the magic of object-oriented inheritance we can assign any TKView subclass as the root view. TKTabs or TKScroll or TKSplit, for example, are all good candidates for a root view. In this case though, we have three main elements which are siblings of each other: a scroll view and two buttons. In order to get them to hang together, they will be made the children of one containing view. An actual TKView (as opposed to a TKView subclass) is ideal for this purpose.
TKView is one of the built-in classes. To instantiate it we fetch a pointer to its class definition with a call to classptr passing its class ID in Y. That's what happens at lines 1 and 2. This returns a RegPtr (X/Y) to the class definition. Note how dynamic this is. classptr is a dynamically linked KERNAL call, because at assemble time our Utility does not know where the Toolkit KERNAL module will reside. And classptr returns to us a pointer to the TKView class, because our Utility doesn't know where that class resides at assemble time either.
Instantiate the TKView class by calling tknew. TKNew does several things. It looks up the size of a TKView object, and malloc's that size from the current memory pool. It writes a pointer to the new object into this, which is a ZP pointer. It also writes a pointer to the class definition into class, which is another ZP pointer. The ZP pointers, this and class are at the very heart of how the object-orientation works. TKNew also writes the class pointer into the first two bytes of this new object. The first two bytes of every TKObj (the superclass of TKView) is the ISA pointer. The ISA pointer allows the system, at a later time, to identify that this object "is a TKView".
The pointer to the new this is also returned in a RegPtr, which we save into the Toolkit environment as our root view. The root view becomes the starting point for all the other recursive processes.
We have a new object but besides the ISA pointer it is little more than an empty chunk of malloc'd memory. To become a usable instance of its class it has to be initialized. The init method takes no parameters, and that is done on lines 7, 8 and 9. The call to getmethod is contextual, it looks up the method pointer by way of the current class pointer, and installs it in jmpvec. Next you can prepare any arguments needed by the method (there are none for this method), and then call the method by doing a JSR through sysjmp.
The init method for TKView is then run and it is entirely contextual based upon the current this pointer. Init, as already mentioned, is what is responsible for setting up the default properties of the object, such as its TOP|BOTTOM|LEFT|RIGHT anchoring and zeroed offsets. The only thing we need to do is overwrite some of those default properties. This job is made easier by a series of macros. On lines 11, 12 and 13, I'm using #setobj8 to set an 8-bit property.
These and many of the other useful macros aren't limited to objects. Why? Because in C64 OS the only difference between a struct and an object is that an object is a struct whose first two bytes (the ISA pointer) point to a class definition that is a subclass of TKObj. Thus, the macros can be used on structs just as easily as on objects proper.
The first parameter is a ZP pointer to the object (or struct). The second parameter is an indirect index from that pointer. The third parameter, for #setobj8, is an int8 constant. The other macros are similar.
Note that—because the 6502 is little endian—#setobj8 can also be used to set the low byte of a 16-bit property. The offsets of TKView are all 16-bit properties, but they are initialized to zero. Thus, we can use #setobj8 to set just the low byte and leave the high byte zero.
Now that we have our root view in place, we want to nest some subviews inside it. Here's what our main layout will look like.
Layout of nested subviews.
The yellow block will be a TKScroll view, and the two blue blocks will be TKButtons. The "Open" button will be on the right, and a refresh button on the left. Instantiating the TKScroll view is very similar to how we did the TKView. Let's take a look:
It's very similar. Instead of getting the class pointer for TKView, we get the pointer to TKScroll. Instead of saving the new object as the Toolkit environment's root view, we stash a reference to it using the aforementioned #storeset macro. Initalizing the TKScroll also takes no parameters. It internally calls its superclass's init method, so TKView's init method initializes its anchoring, offsets and other properties.
We are going to nest the TKScroll inside the TKView, therefore we only need to think about how it will be anchored and offset from the bounds of its parent. As we can see in the diagram above, it sits flush to the top, left and right of the root view, and these are all of its default values. We only need to pull its bottom edge off the bottom of its parent to make room for the buttons. Do that with #setobj8 this,offbot,2
Lastly, we need to attach our new object into the existing UI hierarchy. To do that, we grab a RegPtr (#rdxy) to the root view, (remember that this and class are still set for our new TKScroll view,) and call appendto.Creating the TKList
What do we actually want to scroll? A TKScroll view can be used to scroll anything whose content is larger than itself. You could slap one button in the TKScroll, make the button wider than the TKScroll, turn on the horizontal scrollbar and bingo bango, the TKScroll would let you scroll back and forth across the width of that over wide button. But, that seems kind of useless.
More practically you could layout a whole whack of buttons, one above the next, and use the TKScroll to move up and down through that tall stack. What we're going to do is create an instance of the custom TKList class, which is more or less a very tall stack of dynamically generated buttons. A single instance of TKList, however, is much more efficient than a stack of hundreds of button objects.
Let's see the code to do this, and then walk through what it's doing.
Because TKList is not a built-in class, we don't use classptr to get the pointer to its class definition. We got and stored our own reference to the class definition back when we dynamically loaded, relocated and linked the custom TKList class. Now, we just grab that pointer we saved and call tknew. It's fully integrated into the Toolkit just as though it were one of the built-in classes. That is, so very cool.
We'll stash a reference to this new object with #storeset views,1 and call its init method.
The TKList class does a lot of work for us, but it must be extensible and useful for listing pretty much anything. This is not specifically a "file directory" list class, it's just a list class. It has 4 callback properties. Setting them is optional. It won't crash if we don't set them, but it also won't do much without them either.
The first two, lines 10 and 11, are the most important. tklctsz forwards the TKView's contsz method to the outside world. It's structured the same way. You can expect that your callback will be called twice, once with and once without the carry set, to get horizontal and vertical content sizes. The TKScroll view that the list is embedded in is what makes the contsz call to its content view, and the answers to those requests is how the TKScroll view coordinates with its scrollbars. We'll return to our implementation of listctsz later.
ctntaidx is how the TKList view obtains its content during its draw cycle. The TKList knows how much of itself is visible (how many rows), and its first visible row. When its draw routine is called it only attempts to draw its visible rows. This makes it very efficient. Even if it has 200 rows of data, if it's only 10 rows high and its s_top (scroll distance from top) is 25, it will only attempt to draw rows 25 to 34. Each time it is ready to draw a new row it calls its ctntaidx (content at index) callback, passing an 8-bit index in the X register. The callback uses the index to lookup a record and returns a pointer to a string. The TKList draws that string, clipping it at its right edge if it's too long or padding it if it's too short.
tklclik and tklkeyp are both similar. They get called whenever someone clicks the list, or whenever a key press is routed to the list. The TKList class initializes itself with the Accepts First Key flag. This integrates with the Toolkit such that, a click on the list will cause the list to become the first responder for key events, which automatically deprives the previous first key responder of that status. This is how, for example, keyboard focus shifts back and forth between different views that support keyboard input. If you didn't want a specific list to take keyboard focus, it's easy to prevent. Just unset the accepts first key flag after the list has been initialized. Like this:
Remember, while your callbacks are running, this and class are still set. You can use the same callback to support more than one list and use the this object to decide how to respond. In our case, things are easy because we only have one list. We maintain a selected index state variable. When the user clicks, we can read the hitrow property off the this object, and save that as our selection index. Then mark the list dirty so it will redraw. When it does redraw, we use our selection index variable to report back to it what to draw as selected.
You should be able to see that it is entirely up to the callback whether to support multiple selections. The callback can just as easily inspect the raw event, check to see if a modifier key was held down, and use that to make more sophisticated selections. This makes the TKList's support for selections infinitely flexible. For an example of flexible complexity, we could recognize a clicked index as something that shouldn't be selectable (a separator, say, or a group header), and just not change the selection when that index is clicked.
For keypresses it decodes the cursor up and down key presses for us, and passes a suggested selection delta to the callback. This makes it easier to support the most common case of just a single selection. But you can also completely ignore that suggestion, read the key event manually, and do whatever you want with it.
That's the general idea. We'll return later to look at our specific callback implementations for the Utilities Utility.
It's now time to insert our TKList object into the UI hierarchy. This is done with lines 16 to 23. Rather than simply appending as a child to a parent view that can take many children, we are going to assign the list as the one and only content view of the TKScroll object. To do this, we have to switch the this context to be the TKScroll object. We retrieve a pointer to the TKScroll object with #storeget views,0 and call ptrthis. Now the this and class pointers are configured for the scroll view.
Prepare to call the scroll view's setctnt method with getmethod. This method takes a pointer to the object you want to assign as its content object. So we retrieve a pointer to the list with #storeget views,1 and then JSR sysjmp to call the method.
We need to activate the TKScroll object's scrollbars, and the TKScroll is still the this object after calling its setctnt method. Prepare the setbar method, and pass 0 or 1 for On or Off, via X and Y for Horizontal and Vertical. Our list will not scroll horizontally, so we just turn on the vertical bar. This is done at lines 25 to 29.Creating the Buttons
The only thing left for this user interface are the two buttons. TKButton inherits from TKCtrl, and although TKCtrl inherits from TKView, a button is not a container for child views. Strange things would happen if you tried to append a child to a button.
When a TKButton is initialized, it changes its default anchoring to be TOP|LEFT only. It sets its height to 1, and its width to 8, which is enough to show the word " Button " with space on both sides, which is set as its default title. You can see in the diagram, (repeated below, so you don't need to scroll back up to find it,) that our buttons hug the bottom edge of their parent view. There are two ways you could do this. You could simply observe that this Utility panel is not resizable, and thus they sit a fixed distance from the top of their parent. They are already anchored top, so you just need to adjust their offtop to put them in the right place.
This would work, but it's not ideal. It doesn't follow the spirit of the anchoring mechanics. The TKScroll is anchored to all four sides of its parent, but is offset in by 2 rows from the bottom. This means, in theory, if the parent got taller, the scroll view would get taller, but would remain 2 rows off from the bottom. What you should do is change the achoring of the buttons from TOP to BOTTOM. Now if the parent were to resize, they would remain at the bottom and would remain below the TKScroll where they should be.
Layout of nested subviews.
Let's instantiate the refresh button.
This should start to look familiar. TKButton is a built-in class. Get its class definition pointer and initialize it. Change its rsmask property so that it is anchored BOTTOM|LEFT instead of its default TOP|LEFT. Change its width property to 3.
TKButtons have a title, which can be set with its settitle method. We get its title string pointer using #getstr. If you recall, this was defined earlier in the "content" code section, used for things like string constants. In this case, the string consists of just the Amiga-style cycle UI character which is always available in the character set so it doesn't have to be loaded as an icon.
TKButtons inherit from the TKCtrl class. TKCtrl provides an action-target mechanism to be called when the button is clicked. It can be used to call a method on another object, or to call a non-object-oriented routine. We'll use it here to call our refresh routine. (Lines 17 to 21).
And finally, we append the refresh button to the root view. Here's something to notice. There is no explicit reference retained to the refresh button. We just assigned it an action target, appended it to the root view, and now we're moving on. We don't actually need a reference to it. Once it's part of the UI hierarchy, its anchoring and offsets put it where it should be and draw it when necessary. Hittesting will detect when it's hit, its TKCtrl behavior will highlight it on mouse down and call refresh on mouse click. Bingo bango, set it and forget it.
Instantiating the Open button is a breeze. It's virtually identical to the refresh button. I don't even have to show you the code to do it. We'll just change its anchoring to be BOTTOM|RIGHT, we'll set its width to be 6, we'll assign its title string to string #1 (instead of string #0), and its target will be the routine openutil. No explicit reference to the refresh button is retained. Append it to the root view and done.
We're over 10,000 words into this post, and we've just finished the "init" routine. But, that is how it should be. The majority of creating an Application or a Utility is setting up existing framework code. Plus a tiny amount of custom logic to tie them all together.
The quit routine. This is pointed to by the Utility's main vector table, right at the beginning. We're using the utility framework, so the jump table entry is set to quitutil. That will do a bunch of stuff for us, including saving any changed config variables, deallocating some of the memory that initutil allocated, popping the screen layer, restoring any backed up custom characters, etc. Then it calls extquit for us to implement any extra work to be done at quit time.
The only thing that needs to be done is to deallocate memory that was explicitly allocated. We allocated memory for the TKList class, the Directory Library, and space for the user interface objects. Also, the Directory Library will have allocated additional space for the directory that it loads in. The Directory Library knows how to deallocate this space, all we need to do is tell it to do so by calling, freedir.
Custom Logic Routines
There is no custom drawing in this Utility, so the screen layer struct's draw vector can be set to drawmain which is part of the Utility framework. And because we use the Toolkit and have set istkt=1 in the assemble-time options, it will tell the Toolkit to update which will handle any drawing it needs to do.
We need to implement several custom logic routines, however. If you've been following along, here they are:
- refresh (called when the refresh button is clicked)
- openutil (called when the open button is clicked)
- listctsz (callback for list size)
- listcnt (callback for list content by index)
- listclik (callback for list click)
- listkeyp (callback for list key press)
Now we're into the meat and potatoes of what the Utilities Utility is actually about. It has to load in and present a list of utilities.
In C64 OS, all installed Utilities are found in the system's "utilities" directory. Ergo, we want to load the contents of that directory (//os/utilities/) to present to the user.
We have already discussed various segments of code, but in order to keep things streamlined I didn't mention everything that should be done in those segments. Here's an example of that.
The Directory Library needs a file reference to the utilities directory. Therefore we'll add a state variable to hold the file reference. We have to allocate and configure that file reference, which we'll add to the end of the extinit routine. And we need to deallocate it in the extquit routine.
We've already seen how to make and configure a file reference, but let's see it again for this one.
This code needs only to be done once, during init. And the page number that was allocated automatically for the file reference gets saved so we can refer to it later to pass into the Directory Library and eventually to deallocate.
In the refresh routine then, we are mainly working with the Directory Library. Here's what we want it to do:
- Free any previously loaded directory
- Load the directory specified by the utilities file reference
- Sort the directory alphabetically by filename
- Scroll the list back to the top
- Reset the selected index to zero
Here's the code to do this, then we'll talk about:
That looks like a lot! It's not really that much, but there are some gotchas and some head scratchers that need to be explained.Free Previous Directory Load
First we free any previously read directory. To do that, we have to check if the root directory page is actually set. We haven't talk about this yet. When the Directory Library starts reading in a directory, it allocates one page and fills into it up to 8 entries. If there are more entries, it allocates a new page, and links the first page to the new page. The new page may not follow in memory directly after the first, because, the page allocator is searching for the next available free page. This continues, one new page gets allocated and added to the chain for every 8 entries that get read in. When finished, it returns to us the page number of the first or root page of the chain. We save this for later to a variable I've called rtdirpg.
In the code, probably in the state variables section, rtdirpg is defined like:
rtdirpg .byte 0
In other words, it gets statically allocated and initalized to 0. A value of 0 in some sense means that it points to page 0, but, that's the 6502's Zero Page, and the memory allocator never hands out Zero Page as a page of free memory for you to use! So, if the value is 0, we take that to mean nothing has been allocated yet. On the first run of refresh, therefore, freedir is skipped.Directory Meta Data
In addition to the file reference containing a device, partition and directory path to load, the Directory Library also needs a reference to a directory meta data struct. What is this? It will contain all of the meta data about the directory. Here are the fields: (by the way, this is all stuff that I am and will be documenting more concisely at c64os.com/c64os).
td_head = 0 ;16 bytes td_did = 16 ;2 bytes td_free = 18 ;2 bytes td_pfree = 20 ;5 bytes td_part = 25 ;2 bytes td_ppart = 27 ;3 bytes td_fc = 30 ;2 bytes td_pfc = 32 ;5 bytes td_patt = 37 ;17 bytes td_type = 54 ;1 byte td_sortf = 55 ;1 byte td_sortd = 56 ;1 byte
Here's what these mean in English: The directory header, that's the title that appears in inverse text at the top of a typical C64 directory. The directory ID is the two byte code that follows the header.
Free is a 16-bit integer of the blocks free. This is useful for programmatic reference, but it's not convenient for rendering to the screen. "PFree" is for PETSCII Free, a convenient PETSCII conversion of the free blocks. 5 bytes to hold up to "65535". We're not displaying these values in our Utility, but it would be easy. Just make a label and point the label's title text at this property of the meta data struct.
Part is the partition number, this is pulled from the value that appears before the header text in a typical directory listing. PPart is the PETSCII conversion of the partition number, 3 bytes for up to "255".
FC is a 16-bit integer of the file count, the number of files in this directory. This is not provided automatically by a typical READY. prompt directory listing. The C64 OS directory library counts the files and provides the value for convenience. PFC, obviously, is the PETSCII conversion of the file count. Again, it would be easy to put this in the UI via a TKLabel. This could also be useful, to tell the user how many Utilities are installed in their instance of C64 OS. That would be cool.
Patt is not read from the directory listing. Rather, it is the CBM/CMD/IDE64/SD2IEC DOS file matching pattern sent to the drive, to load this directory. The default for this is just "*" to match all filenames. Type, similarly, is used to limit the directory load to a specific CBM File Type. By default it loads all file types. You could set it to "P" to load only PRG type files, "S" for SEQ type files, etc. Both, td_patt and td_type are features of the Directory Library that the Utilities Utility doesn't expose to the user. We'll just set their defaults and leave them like that.
Sortf and sortd are for Sort Field and Sort Direction. We'll return to these shortly, as we are indeed going to sort the directory.Meta Data Gotchas
We have a small problem. I only discovered this when I first sat down to implement this Utility. Typically you would statically allocate the meta data struct, and initialize its default values, such as the pattern and file type matching. The problem is that Utilities are run from beneath the KERNAL ROM. The Directory Library needs to have the KERNAL ROM patched in because it uses it to access the IEC bus. Any writes back to the statically allocated meta data structure would work, (because of the magic of the PLA, writes to the KERNAL ROM get written to the underlying RAM), however reads will fail. Trying to read the pattern, for example, would read some bytes out of the KERNAL ROM.
Here's what we do instead. Start by statically allocating your meta data structure. I do this in the state variables section of the code. Then in the extinit routine, copy the struct to the main memory page that gets automatically allocated by the Utility framework, and we'll then reference it from there.
Why does the Utility framework allocate a page in main memory?
For precisely the same reason that our meta data structure needs a main memory page. Because the statically allocated screen layer structure and those config variables, they need to be in main memory too. Fortunately, the Utility framework handles moving that stuff out to main memory for us. If we're using the iconswap feature, by the way, the framework will back up the 18 custom characters (18 * 8 = 144 bytes) to the last 144 bytes of that main memory page. The screen layer which is 9 bytes gets written to the first 9 bytes of that page. The config variables are written following the screen layer. So, 256 bytes minus 144 (if using custom icons) minus 9 leaves 103 bytes for the config variables. By default, every Utility only uses 2 bytes, for X and Y screen position of the Utility panel. That leaves 101 bytes for your other custom config variables.
In our Utilities Utility, we're not using any additional config variables. So there are 101 free bytes in that memory page. Why not use them for the meta data struct? In the extinit routine then, I've included a loop that copies the 57 byte meta data structure to mempage+slsize+confsize. Mempage is provided by the Utility framework, it's already allocated and defined for you. slsize is a constant, and confsize is a constant you have to set anyway so the framework knows how many bytes of config variables to save and restore.
The Directory Library defines mdptr and you have to configure that pointer to point at your meta data structure. So that's what's going on at lines 7, 8 and 9. mempage (the allocated memory page) is the pointer's high byte, and the low byte is slsize+confsize.Read By Redirect
There is another gotcha, but it's easily overcome. The Directory Library needs to make calls to the KERNAL ROM. Therefore, we cannot call readdir directly. Actually, of all of the Directory Library's routines, readdir is the only one that requires the KERNAL ROM. C64 OS provides a mechanism to allow code from beneath the KERNAL ROM (namely, Utilities) to call the KERNAL ROM. You put the routine you want to call into redirectvec and then you JSR to redirect. Jumping through redirect doesn't change any processor flags or registers, it patches in I/O and the KERNAL with: INC $01 INC $01. How the hell does that work??! Read my post The 6510 Processor Port. Figuring this out was an absolute godsend, and in fact this very mechanism (redirect/redirectvec) was the motivation for figuring it out. INC/DEC allow you to patch in and patch out the major memory areas of the C64, controlled by the Processor Port and the PLA, without modifying the A, X or Y registers or the carry flag!
That's what's going on at lines 17, 18, and 19. It looks like a pain in the butt, but think about it. We're running this code from 8 precious kilobytes of RAM that are inconveniently in a contentious addressing range with the KERNAL ROM, and yet this code is still able to make use of the KERNAL ROM! At the beginning of C64 OS, I didn't think this would be feasible, and yet here we are. So, it's a small pain in the butt for a huge win.
Readdir returns the root page of the chain of pages allocated to hold the directory. Here is where we save that at rtdirpg for later use and for freedir.Configuring Sort Comparator
I have gone into this in much more detail in the post, Directory Library, Natural Sorting, including getting into the code itself. There is no need to recapitulate all that in this post. Here, we are simply using the Directory Library as it is meant to be used by a Utility that needs to read in a directory.
Before sorting, the sort comparator must be configured. ts_name is the constant for sorting the directory entries by filename. There are other options, of course, type, size, disk (the order found originally on the disk). But for the Utilities Utility, we want them in alphabetical order by name. ts_case is the constant for case insensitive. There is also an option for ASCII 2 PETSCII conversion, if the filenames were encoded in ASCII. But, they're not, so we don't need that flag.
With the sort comparator configured, we're ready to sort: lines 30 and 31. First we call presort, then we call sortdir. What's up with that? Remember, the Directory Library is designed to work with multiple directories in memory. That's why you set the meta data pointer, and save the root directory page, etc., because you can have multiple meta data structures one for each directory. Only one of those directories, though, is ever "sorted" at a time. presort reads through the chain of directory pages and populates a buffer within the Directory Library with pointers to each directory entry. sortdir then sorts those pointers.
In a more advanced context, such as the File Manager, one directory is maintained for each of its four tabs. When a tab is switched, the tab's meta data pointer and root directory page are swapped in, and presort is called once. All subsequent sorts do not require calling presort first. Change a tab: swap the pointers, call presort. In our Utility we only have one directory to worry about. But that is the long answer for why presort has to be called before sortdir after loading in a new directory.Updating the User Interface
After doing a refresh, we're going to scroll the box to the top and reset the selected index to 0. This is done from lines 34 to the end.
Use #storeget views,0 to grab a reference to the TKScroll object. ptrthis to make it the current this. Prepare its setoff method to be called. This method is used for setting either the vertical or the horizontal offset. We only have the vertical scrollbar turned on.
The selection index, if you remember the discussion about the TKList class, is maintained by us not by the list class. So that's one's easy, just set it to zero.
There is only one other thing to say about refresh. Since it loads in the directory—and we want the directory to be loaded in as soon as the Utility is opened—we should add a first call to refresh at the end of the extinit routine. Done.
Open the Selected Utility
If you thought reading in the directory and sorting it was complicated, it all pays off with how dead simple it is to open a utility.
We have a selection index (selidx), and we have a Directory Library which has loaded in a directory. When the user clicks the open button, here's everything we need to do:
- Call drecord with selidx
- Point RegPtr (X/Y) at the filename of that record
- Call loadutil
That's it! Holy cow that's simply. drecord is a Directory Library method that takes an index in X, and configures deptr (Directory Entiry Pointer) which it also defines for you. We read the index straight out of our TKList's selection index.
The macro #rdxy reads the pointer value into X and Y. Then I'm using a little tiny hack, the filename starts at offset 3 within the directory entry struct. And also, I know that all directory entry structs are 32 bytes and page aligned, therefore I know with 100% certainty that incrementing X will not cause it to wrap around to the start of the page. It now points at the null terminated filename string. That's exactly what the KERNAL call loadutil needs to load that utility. Actually loading the selected utility is the easiest part.
TKList Callback Routines
We need our TKList callback routines to customize its behavior. So let's dive into those.List Content Size
We are rendering a list of C64 filenames. On our trusty C64, filenames are a maximum of 16 characters long. That's why we made the Utility panel 19 columns wide. Two columns for panel border, 1 column for the vertical scrollbar, and 16 columns for the longest filename with no clipping and no horizontal scrolling.
The callback listctsz will be called twice, once for its horizontal size, once for its vertical. The horizontal size is easy, we just return the static value 16. Remember, the Toolkit uses 16-bit values for most things. A TKScroll can scroll content up to 65,535 lines long and 65,535 columns wide. So, we need to return 0 in Y as the high byte, and 16 in X as the low byte.
When the routine is called again for vertical content size, we will refer to the directory's file count. Let's take a look at the code first.
The carry is used to indicate whether the call is for the horizontal or vertical content size. When it is clear, we skip down to get the vertical size. Now, our meta data structure is out in main memory, in mempage. At lines 10, 11, 12, we configure mdptr to mempage as the high byte, and the low byte to slsize+confsize. That's exactly where the metadata structure starts, so we can now index it using the defined constants provided by the Directory Library.
But first, why do we have to configure the meta data pointer again? Isn't it already configured from the call to refresh? Welcome to the wonderful world of 6502. Indexable pointers must be in zero page. And there is very little zero page all of which is visible to the entire operating system. Some ZP address are permanently reserved, while others are shared. mdptr is shared. It's used by a dynamically loaded library, so under many circumstances there will be no Directory Library anywhere in memory. It makes no sense for an ephemeral library like this to have its own dedicated ZP addresses. They are too precious and few for that. Every time we are going to use mdptr we have to set it afresh. And other parts of the OS that use these shared ZP addresses do the same, knowing that something like the Directory Library may have stolen them.3
Next we pull the file count. Ah, we should recognize this from earlier. td_fc is the struct offset for the integer file count. The PETSCII version is useful to output to the UI, but this is the perfect example of why it's useful to have it as an integer too. We just copy it to X for the low byte, and set Y to 0 for the high byte and return.
Note that, I'm also saving it to maxfc. I'm saving to maxfc because I want quicker easier access to it than having to configure and dereference mdptr every time. We'll see why in a second. But that's it. Now the TKScroll knows how long the TKList shall be, and it can set its scrollbars to allow the user to scroll through the length of data.List Content by Index
If you recall how the TKList works, we're now going to get into the details of how to implement its callback for getting list content by index.
The TKList knows its own scroll offset. It knows its own size. It knows how to be efficient about what to draw and what not to draw, and when to draw, and when to stop drawing. All it needs to know is what to draw for a given index, and also it needs a clue about one of two possible ways to draw it: selected or not selected. And that is all this callback needs to provide. This routine can ignore everything else about TKList that it doesn't need to know. Let's jump straight into the code:
I love 6502. I don't know if I'm doing it right. It's so limited in some ways, and so liberatingly flexible in others. This routine does things that high-level languages don't dream about doing.
The index whence to fetch the list data is passed in X. Remember when we stashed the file count for quick access? Well, here it is. It's just faster and more convenient to CPX maxfc than it would be to configure the mdptr, load the index into Y, pull the value into the Accumulator... and then what? Somehow you have to compare the Accumulator to the X register. Believe it or not, the 6502 has no instruction for doing that! Nor is there an zero page indexed CPX(),y instruction. You'd have to write either X or A to memory first before doing the compare. So, that's why we wrote the filecount to maxfc first. This routine is also going to be called many times over during the draw cycle. We want it to be as fast as possible.
If X is greater than or equal to maxfc then the index we're being asked about is past the end of the directory entries we have. This will happen if you have fewer Utilities installed than there are rows in the TKList to draw. In this case, we load X and Y with zero, that's a null string pointer, clear the carry so it doesn't get drawn selected and return.
More magic occurs from lines 10 to 13. We need to return with the carry clear if this row should not be selected, or set if this row should be selected. The problem is we have to do this relatively early, before the X register gets blown away. But, when we call drecord, who knows how that's going to leave the carry. So here's how this works:
CPX selidx compares the X index we're retrieving to the selected index. If X is equal to selidx the carry will be set. That's good because we're using a set carry to mean selected. But if X is not equal to selidx the carry is in an inconsistent state. If it's not equal because it's less-than, then the carry is clear. But if it's not equal because it's greater-than, then the carry is set, and that's wrong. So, if they are equal the carry is right and we can proceed. But if they are not equal we must explicitly clear the carry.
Next, we are going to call into the routine of some library whose source we aren't privy to. We don't know how it might change the state of the carry. Push the processor flags to the stack to back them up.
After all this, X still holds the index the TKList is after. We call drecord which sets deptr with a pointer to that directory entry. Just as we saw in openutil we can read that pointer into RegPtr (X/Y) and increment X three times so it points to the filename of the directory entry. X and Y are now fully occupied with the string pointer to return. We really don't want to be doing any selection comparisons after this point. No problem the only thing left is to pull the process flags from the stack, thus restoring the state of the carry.
It's pretty clean, short and fast. Maybe the PHP/PLP could be omitted to make it faster. But what's really nice is how cleanly it integrates with the Directory Library. It gives us X, drecord takes X. That's one way that 6502 is able to be so fast. There is zero overhead between TKList calling our routine with a parameter, and us calling a library routine with a parameter. It's the same parameter!List Click
I love how short and tight catidx was. But listclik is even shorter. Here it is:
TKList passes to us its clicked row in X. We could do fancy logic here, inspecting the item at X and deciding whether to allow it to be selected. But, we allow everything in the list to be selectable. How do we do that? Just write X to selidx. Boom. It's selected.
The next part probably needs some more dog-fooding. I'm opting to raise the TKList object's dirty redraw flag, and setting the Toolkit environment's dirty update flags manually, in the callback. That works. It causes the TKList object to be redrawn, which, when it redraws, will reflect the change in selection. An alternative (which I may yet implement) is to use the carry as a return status to indicate whether the click resulted in a change of selection or not. (Or even a change of content.) Then move the code that raises the dirty flags into the TKList class itself.List Key Press
A good user interface should support the keyboard not just the mouse (or joystick). The TKList class inherits from TKView, and TKView has a flag accepts first key. If you click on an accepts first key view, that view will take the place of the previous first key view. As keys are pressed, the Toolkit routes them to the first key view first. TKList then forwards that to its key press callback. But it provides a helpful hint first. If you push the down cursor it calls the callback with positive 1 in the accumulator. If you push the up cursor, it calls with negative 1 in the accumulator. For all other keys, it calls with 0 in the accumulator. Let's see why.
First, if the accumulator is 0, then we know neither cursor up nor down was pressed. And for our purposes that will mean no change. Branch to nochange and we return with the current selected index. Now, you might wonder why we are sending back the selected index, when I have said that the TKList doesn't have an internal representation of selected index. It doesn't. This index, whatever you pass back, is used by the TKList to decide if it should scroll or not. If the returned index is somewhere within its visible rows, no scrolling will take place. But if it is outside its visible range, the minimum distance will be scrolled, necessary to make that index be in the visible range.
Next, we compare to #$ff (which is the unsigned equivalent of signed -1). If it is -1, we'll branch to adjustup, because it suggests that we move the selection up the list. Let's jump there right away, because it's simpler than moving the selection down. The only reason why we couldn't move the selection up, is if the selection index is currently zero. Load the selection index into X to check. If it is zero, branch back to nochange. Otherwise, the accumulator still holds -1, and we fall through to apply.
Apply clears the carry and adds the current selected index to the accumulator. When adjusting up the accumulator is -1, so adding a -1 decrements the selected index. Write the new selected index and return it.
I love Two's Complement. It's brilliant. I've talked about it before in my post Floating Point Math from BASIC (2/2). But even without a comprehensive understanding of how Two's Complement works, it's fun and easy to see why $ff functions like -1.
Imagine that you have only one byte. The byte is like a giant lottery wheel with 256 possible positions around the outside rim. Going clockwise around the wheel, starting at $00, then $01, $02, $03 all the way around the wheel, you end up at $ff at the very end, right back where you started, right next to $00.
Now let's say the only thing we have is an add instruction. To add X, you spin the wheel counter clockwise X number of positions. Let's say you start at, say $05, and you want to add, say $02. You spin the wheel 2 positions from $05 and you end up at $07. It worked. How far would you have to spin the wheel for it to land on exactly the same number? The answer, 256, or $100 in hex, which is one more than $ff. So, from any value on this wheel, if you add $100, you end up exactly where you started, no change. But, what if, instead of adding 256 you add 255 ($ff)? You end... almost... where you started. In fact you end up just one less than where you started. And that's why adding $ff is the same as subtracting 1.
Returning to the code, assuming now the value passed in was 1, the hint to move the selection down. We don't want the selection to be moved off the bottom end of the list. The last selection index is one less than the number of files in the directory. Therefore, we load the current selection index into X, add one (INX), and compare to maxfc. If it is still less than maxfc, then we're safe to apply the hint value.
And that's it for keyboard support. The cursor keys move the selection up and down the list.Built-In Keyboard Support
Is that all there is to keyboard support? What about pressing return to open the selected item? Here's the trick, and I've just got it supported properly by working on this Utility. TKButton is a subclass of TKCtrl. TKCtrl has a flag for default control. We can mark the Open button as default, like this:
The status of a control as a default control is dynamic. You could turn this flag on and off at any time, as the result of some other action. But, in the Utilities Utility we'll make it always the default control. We'll set its default flag while creating the UI objects, right after initializing the Open button. TKButton knows to check if it is a default control, and draws itself using the color for default controls from the Toolkit theme. (i.e., blue instead of red.)
The key event is sent to the first key object, first. But if the object doesn't handle this key on its own, it calls its superclass implementation of that method. This behavior recursively trickles up the class hierarchy, either until it is handled or until it gets back to TKView's implementation. TKView's behavior is to propagate the unhandled key event to the next responder object. If there is no explicit next responder object, it passes it to the object's parent object. The process repeats by moving up the class hierarchy of that object, until TKView's implementation passes it to its parent, and so on.
Finally, lastly, if TKView doesn't have a parent (because it's the root view), then it begins a process of walking the node tree. Walk is an extensible recursive routine provided by the Toolkit KERNAL module. It starts at this, so it can be used to walk only a branch of the whole tree, if this isn't the root view. It can be optionally configured to skip invisible views. It then walks the node tree and calls a configured callback on every node.
In this case, TKView implements the callback, configures walk to skip invisible nodes, and starts it walking from the root view. Each time callback is called, this is set as another node it must check. It uses another Toolkit KERNAL routine called instanceof. This allows you to test if the object inherits from a given class. If the node is an instance of TKCtrl then it can check properties that it knows exist only on TKCtrl objects. If it finds that the control is marked as default and not marked as disabled, it calls sendact (Send Action) on the control. Then it returns with the carry clear, which tells walk that it should stop walking and unwind all its recursion.
- Always to first key object first
- Then up the class hierarchy of the current object
- Then to the next responder object
- If no next responder object, to the object's parent
- Repeat steps 2 to 4 up the view hierarchy, to the root view
- If the key is <RETURN>, walks the visible view hierarchy
- Calls send action (sendact) on the first enabled, default, control
This is cool on so many levels.
There is a lot of object-oriented goodness going on here. The Toolkit environment keeps track of which view is in focus. Programmatically speaking the view in focus is the view that takes the key events first. Then you have the class hierarchy, which allows a subclass to inherit the keyboard support of its superclass(es). But better yet, it is the TKView superclass that everything inherits from which is responsible for propagating the event through the view hiearchy. (This comes straight from AppKit.)
Lastly, if it manages to make it all the way to the TKView implementation of the root view, then there is more object-oriented goodness. How does it find the control to trigger? TKView recursively walks the tree of view objects. At each node, how does it know what kind of an object this is? It uses instanceof to see if it is or descends from a TKCtrl. That is so cool. Additionally, it only walks into branches of the view hierarchy that are visible. This means if you have a list and a default button inside a TKTabs view, the key event will get routed up to root view, and then back down and into the tab that's selected, and find the button inside that tab. Meanwhile, you could have another form inside a different tab, but it would not be triggered by mistake, because one if its containing views is hidden. Wham! So dynamic.
Event Processing: Mouse, Keyboard, Messages
We've built the UI, and we've talked about how key events get routed to the views. Presumably mouse events will be routed to view objects when the user clicks on them. The question is, how do low-level events, system-generated from the drivers, get captured by our utility and routed into its Toolkit environment?
The short answer is: The main vector table and the screen layer. Near the very beginning of this we talked about how every Utility begins with its main vector table. That table contains a pointer to its identity string, pointers to its init and quit routines, and lastly a pointer to its message handling routine. All system-level messages are delivered via the messages routine. The init routine (with the help of the Utility framework) pushes a screen layer structure onto the stack of screen layers. The screen layer has pointers to routines for drawing, mouse events, keyboard command events and printable keyboard events.
For an overview of the mouse events and the difference between the two kinds of keyboard events you can read this post, The Event Model. This post is now almost 4 years old. It stretches back to the very beginning of the C64 OS project. But, it still holds up, as it is about the very lowest levels of event processing. This was done early, but it hasn't changed and is very unlikely to ever change.Mouse Events
This area is still a little bit rough. The integration between low-level mouse events, the toolkit and the Utility framework requires a little bit more boilerplate code than I would prefer.
The screenlayer has a pointer to the mouse processing event for low-level mouse events. This routine will be called only when a mouse event is available and ready to be processed by the Utility. I usually label this routine procmous. The mouse event is not passed to this routine, instead, this routine is called as a notification that a mouse event is available. To fetch the mouse event, if you actually need it, call the Input KERNAL module's readmouse routine.
In procmous the first thing to be done is to forward notification of the event to the Toolkit environment. Obviously, if your Utility doesn't use the Toolkit you'll have to process the low-level mouse events manually. We won't discuss that here as it is out of scope for this post, and because our Utilities Utility does use the Toolkit.
Because the mouse event is not actually passed around, we don't receive the event and pass it to the Toolkit. We call the Toolkit KERNAL module's tkmouse routine and the only thing passed in is a RegPtr (X/Y) to our Utility's Toolkit environment. Like this:
#ldxy tkenv jsr tkmouse
Here's one thing I'm trying to improve. If the Toolkit environment is dirtied and needs an update, that means the low-level system needs to queue up this screen layer for an update. But the Toolkit environment doesn't have direct knowledge of the screen layer. Therefore we need to add this little piece to bridge the gap:
lda tkenv+te_dirty beq *=8 ldx scrlayer+slindx jsr markredraw
We don't need to know or care what the mouse event was, nor how the Toolkit UI handled it. Only that if the Toolkit environment ends up with its dirty flag set, we need to inform the system that this screen layer needs to be queued up for a redraw.
Next, we need to forward certain key mouse events into the Utility framework. Again, I should figure out a less verbose way to do this. At the moment, the guts are somewhat exposed because you need them to be if you want to integrate a custom UI and custom mouse event handling.
You don't really need to know how this works. And so I really should be hiding it, by absorbing into the Utility framework, and using it with a simple assembly-time option. For now, let's just say quickly what it does.
The KERNAL call readmouse gets you the current mouse event that includes the event type, keyboard modifier flags, and a pixel coordinate from 0-319 across and from 0-199 down. getmusrc leaves the flags and mouse type, but converts the pixel coords into text-cell row/column coords. We need to know what the mouse event type is, which is contained in the lower nybble, so we mask away the upper nybble.
Writing the equivalent of a C switch statement in 6502 is actually kind of a pain. C64 OS provides a convenient switch macro. With this macro, we just write #switch followed by an int8 constant for how many cases there are. That many cases must immediately follow as a list of matching bytes. A corresponding list of routine pointers follows that, in the return address format (pushed to the stack, followed by an RTS). The switch is performed on the contents of the accumulator. If a match is found, execution passes to the specified routine. And if no match is found, execution falls through to the code following the switch.
There are several behaviors provided by the Utility framework:
- Normalizing the event coordinates to the offset of the Utility panel on the screen
- Bounds checking the event to determine if it inside the Utility panel
- Setting a flag for whether the panel is in focus or not
- Dragging the Utility panel around the screen by its titlebar
- Window shading the panel by Control-Clicking its titlebar
- Quitting the Utility by clicking its upper-left close button
To accomplish all of these behaviors, we need to pay attention to 4 low-level mouse event types:
- ldown: Left mouse button down*
- ltrack: Moving the mouse while the left mouse button is held down
- lup: Left mouse button up
- lclick: Left click
The switch macro switches on each of those 4 event types, calling dragstart, dragdo and dragend for each of: down, track and up. And if a click is performed we call another macro called #doclose. #doclose is due to have its name changed. It handles closing the Utility, but over time has been extended to support WindowShade and panel focus. Lastly, for another other event type, chkbnds is called. If the event is within the bounds of the panel it will not be allowed to propagate to lower screen layers. If it is outside the bounds of the panel it will be propagated to lower screen layers.
WindowShade extension for classic Mac OS
The screen layer's key event handling routines will be called whenever a key event is present that has been allowed to propagate down to the Utility's screen layer. At this low level the events know nothing about a Utility panel or whether that panel is in focus, etc. It is easy to create a modal panel, therefore, by always capturing the events and never allowing them to propagate to the screen layers below. But more typically we want to propagate the events if the panel is not in focus, or handle them if the panel is in focus. If the user interface of the Utility does not use the Toolkit you can call the Input KERNAL module's routines to fetch the current key event and handle it directly. But since our Utilities Utility uses the Toolkit it is sufficient to forward notification of the events to the Toolkit.
There are two routines here, prockprnt for processing printable keyboard events, and prockcmd for processing keyboard command events. They are almost identical and you could optimize them to share some code. I present them here in their entirety for clarity of what they do.
If panelfoc is zero, immediately return with the carry set. This allows the keyboard event to be propagated to the next screen layer and no further action will be taken by the Utility. Otherwise, load a RegPtr (X/Y) with a reference to the Toolkit environment structure. And call the appropriate Toolkit KERNAL routine: tkkprnt or tkkcmd. The Toolkit and your UI objects take over from there.
Upon return, Toolkit may have set the dirty flag in the Toolkit environment. If so, you need to mark this screen layer as needing a redraw in the next update cycle. And lastly, return with the carry clear to prevent the key event from propagating to the next screen layer.System Messages
Messages can be passed to a Utility either by the system or by the current Application. Messages can also be sent to the Application either by the system or by an open Utility. Messages are not delivered to the Utility's screen layer, they are delivered to the Utility directly, via its main vector table.
C64 OS defines a handful of standard message codes. They are designed to allow loose coupling. That means the Application will send messages to your Utility that your Utility may not be able to handle. You need to gracefully reply that the message was not handled by returning with the carry set.
Additionally, when the Application sends a message to the Utility it is designed to not know or care how the Utility will handle it. The idea is to open up a vast and creative workflow by allowing Applications and Utilities to pair together in ways that might not have been originally intended by the developers. In some cases an Application may explicitly open a Utility by name and then send it a message. But the user can also substitute these standard Utilities with alternatives, simply by naming the alternative Utility with the original's name and supporting the same set of messages.4
The Utilities Utility doesn't support—at least not at this time—any system messages. But I wanted to say what the idea is. In order to painlessly not support any messages you can point the main vector table's message handler to a standard C64 OS response routine. There are three of these defined:
Just like the C64's KERNAL ROM, C64 OS typically uses the carry to signal one of a pair of response states. CLC/SEC for Handled/Not-Handled, Stop Propagation/Allow Propagation, Opened/Not-Opened, Success/Failure, etc. If you don't handle any messages and therefore you always want to signal Not-Handled, just point the main vector table at sec_rts. As you can imagine this routine does nothing but:
sec_rts sec rts
Dynamically Linked KERNAL Jump Table
We are very much at the end now. Our Utility will make use of many KERNAL calls, and the Utility framework will make use of a bunch that we don't explicitly make use of. For example, the Utility framework uses fopen, fread, fwrite and fclose to automatically read in and write out the persistent config variables.
I have written about this before, in the post, Distributed Jumptable. Even though this post is 2 years old, it is still exactly how it works, with one exception. I've added a macro to make the distributed jump table easier to create and read.
To make a dynamic links to routines provided by KERNAL modules, you can use the #syscall macro. Like this:
Using #syscall abstracts how the table is structured and allows each jump table entry to be presented on a single line. The #inc_h macro includes the KERNAL module header which defines both the lookup table entry (i.e. lfil for "Lookup File", and linp for "Lookup Input") as well as the routine offsets, the ones that end with underscores (i.e. fopen_, fread_, fwrite_, fclose_, readmouse_, etc.)
This KERNAL jump table has to be dynamically linked. That is, it needs to be converted at load time to point to the locations in memory where those actual KERNAL calls reside. To do this, you need to put the extern label at the start of the table and the end of the table must be terminated with a single $ff byte. The Utility framework will automatically link the table for you. You must include in the table every KERNAL routine that you will use, including all of those that will be used by the Utility framework.
Earlier, when we talked about bringing in the Directory Library, I said that its jump table typically comes near the end. I've put the Directory Library's jump table immediately before the extern label. That way, all of this Utility's external calls are together.
Draw Context Buffer
Utilities are always assembled to $E000, and have full control over memory from there up. The last 192 bytes from $FF40 to $FFFF are reserved for system use. This includes three 64-byte blocks. Two of those blocks are used for the mouse pointer sprites, and the final block is used for the processor vectors and some other flags and settings. If your Utility incautiously overwrites the memory from $FF40 to $FFFF, first the mouse pointers will get corrupted and then C64 OS will likely crash.
However, the area from $E000 to $FF3F is considered by C64 OS to be high memory, and its contents are only tracked as a single lump. For example, if you use the C64 OS Graphics Library to install bitmap graphics which can be accessed via split-screen mode and the keyboard shortcut to toggle into fullscreen graphics mode, the bitmap data will occupy high memory. When a Utility is launched, it will collapse the split-screen and automatically install itself into high memory. That is why you cannot have a Utility open at the same time as you have split-screen mode engaged. They occupy the same memory area.
While a Utility is using high memory, though, it has complete manual control over the entire area. The C64 OS memory allocator never allocates memory from high memory because the use of that area is contended for by the KERNAL ROM. Utilities, on the other hand, must be written carefully to work with the KERNAL ROM, as discussed earlier.
There are almost 8 kilobytes of memory in high memory to play with. It is unlikely that your Utility's codebase will take up the whole space. Especially considering that dynamically loaded resources, such as libraries and custom classes, are loaded into space allocated from main memory. If your Utility binary takes up, say, 4K, that leaves 4K more from $F000 up that can't be used by anything else. This is the ideal place to put your Utility's draw context buffers.
Our Utility panel will be 19 columns by 14 rows, that's 266 bytes for the screen codes, plus an additional 266 bytes for the color codes. I have been hardcoding parts of the Utility's UI directly into the character and color buffers, although this may change in the future.
For example, for the Utility panel's titlebar I simply put the characters directly into the character buffer, like this:
charmap ;This label is put in the draw context struct character map ;First line, Utility panel titlebar .byte $6e,$6f,$6f,$a0,$d5,$94,$89,$8c,$89,$94,$89,$85,$93,$a0,$6f,$6f,$60,$60,$60
$6e is the custom character for the Utility panel close button. $6f is the custom character for the horizontal strips that surround the title of the Utility in the title bar. I typically put two of these, then a space (Thanks KrocCamen, it does look better with the spaces.) The space is $a0, that's a reverse space. From $d5 to $93 are the reverse screencodes for "Utilities", and then another $a0 reverse space. Followed by two more $6f's to make the end of the titlebar.
The final three $60's are identical to a space, they are screencodes in which no pixels are set. Because $20, space, is already suitable for this, $60 is used to indicate transparent regions. The chkbnds routine of the Utility framework doesn't just check to see if the mouse event occurs outside the rectangular bounds of the Utility panel, it also checks for these transparency characters in the character map. Mouse clicks on transparent areas pass right through and unfocus the panel just as though you'd clicked outside the panel. Additionally, the draw context's buffers get copied to the screen buffer by a low-level KERNAL routine called ctx2scr. It too treats $60 as transparent and skips copying those characters when compositing to the screen buffer. The combination of these two allows you to make arbitrary transparent regions in your Utility panel. That is super cool.
The extra $60's at the end of the titlebar, then, is what gives the titlebar that right-side notch that makes it look a bit like BeOS. I like it, I think it looks cool. It makes use of the transparency feature and lets you see more of the underlying UI. This is useful on a screen with a very low resolution.
The remainining 12 rows I simply fill up with $a0's, reverse spaces. The Toolkit UI will overwrite these when it draws itself.
What about the color map? That comes next. There are two ways to specify the colors. You can hardcode the colors, which you might want to do for a very custom UI. Two do this, you just put in the actual VIC-II color codes, $00 for black, $01 for white, up through $0F for light grey. However, typically you want your Utility to use the Toolkit's customizable colors. The user can choose one of the built-in themes, or they can customize the color theme. To support the color theme use the ctxcolor codes (as found in //os/s/:ctxcolors.s) but add $40 to them. (In other words, set bit 6.)
For example, the floating panel titlebar color code is $03. You can therefore put $43 into the colormap to represent the titlebar color, which will be dynamically substituted with the user's preferred titlebar color when the Utility is launched. The main panel background color code is $02, so you can put $42 into the colormap to represent that. Like this:
colmap ;First line, Utility panel titlebar color .byte $43,$43,$43,$43,$43,$43,$43,$43,$43,$43,$43,$43,$43,$43,$43,$43,$60,$60,$60 ;Subsequent lines of the Utility panel's main color .byte $42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42 .byte $42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42 .byte $42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42 .byte $42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42,$42 ;... and so on.
The final $60,$60,$60 on the end of the titlebar are set the same as they are in the character map, and will be skipped to produce the same transparency in the color map.
It is the Utility framework that performs the color code mapping, substituting in colors taken from the TKColors table at runtime. It is done automatically, but also provides a routine, mapcolrs, which you can call at any time to do the substitution again. This can be useful in advanced situations when you are doing custom drawing.
It's been a long post. I've struggled through busy December and the holiday season to get this out before the end of 2020. I'm not sure how interesting this post will be for anyone who doesn't have a keen interest in programming for C64 OS. However, I've always said, I write the kinds of posts and articles that I would want to find on the internet. And since I'm a C64 OS developer, the post is also for me. It helps me to organize my thoughts and document the process, the steps, the parts involved.
While I will be summarizing more concisely, in a reference format, the programming APIs, memory locations, constants, macros and more in the C64 OS programming documentation of this site, that alone is rarely enough for a developer to figure out how to set about actually creating an App, a Utility, a Driver, a Library or other resource. It is these sorts of posts, that can be used in conjunction with the reference documentation and sample code, that helps a developer figure out not just the APIs but the big picture of how things are meant to fit together.
The post is long and there are many tangents to go on to describe what all is going on in the code. But in the end, for this Utility, there isn't actually very much code, especially compared to what you get. Here are some things that this Utility offers that the programmer doesn't have to write because the operating system provides them:
- Point and click user experience
- Drivers for multiple input devices
- Moveable floating panel
- Flexible, reusable, object-oriented UI
- Hit testing and event processing
- Scrolling list, with rich proportional scroll bar
- User customizable color theme
- Multi-device directory parsing
- Directory entry sorting
- Dynamic memory allocation
- Automatic persistent configuration
Plus, the overarching architecture allowing for an Application and a Utility to coexist and use the computer's resources at the same time. And this Utility gets all of this from only ~800 lines of its own source code (in 6502 ASM), resulting in a 2.2KB binary.
There is a lot more to do, a lot more for me to write, to really bring C64 OS to life and to make it useful to a wider audience of C64 enthusiasts. But for me, at least, it has already crossed the point of fulfilling many of my goals. I wanted C64 OS to make my C64 feel fast and useful. Using a mouse to dragging a floating utility panel around the screen, Control-clicking its titlebar to shade/unshade the panel, dragging that proportional scrollbar up and down. It feels really great.
I go to bed each night dreaming of the possibilities that C64 OS is opening up for applications for my C64 that I am itching to write. And hopefully this post and others like it will inspire other developers to want to write Apps and Utilities for C64 OS too.
- The source code is in TurboMacroPro format. It is compatible with the native C64 version of TMP, and it is also compatible with TMPx, an open source TurboMacroPro cross compiler. If you don't use TMP, it's not that tricky to rework the source code for the format of your preferred assembler.
- TIP: There is a limited number of labels that TurboMacroPro native can support in a single source code file. However, if you enclose your routines in blocks (.block to open, .bend to close the block), you can reuse common labels, such as: next, loop, done, exit, ptr, etc. A label like loop only counts once, even if you use it 10 times in 10 different code blocks.
- Shared ZP address are never used by anything in or called by the IRQ handler. For example, input drivers must never use shared ZP addresses. Otherwise, something like the Directory Library could have its mdptr changed out from under it in the middle of a routine's execution!
C64 OS provides a "Color Picker" Utility called Colors. It sends and receives
mc_col messages to and from the application.
A third party can easily implement a new color picker. Simply rename the old one and name the new one "Colors". Apps open the color picker by name. If the new one supports the mc_col message, the app will integrate with it all the same.
Do you like what you see?
You've just read one of my high-quality, long-form, weblog posts, for free! First, thank you for your interest, it makes producing this content feel worthwhile. I love to hear your input and feedback in the forums below. And I do my best to answer every question.
I'm creating C64 OS and documenting my progress along the way, to give something to you and contribute to the Commodore community. Please consider purchasing one of the items I am currently offering or making a small donation, to help me continue to bring you updates, in-depth technical discussions and programming reference. Your generous support is greatly appreciated.
Greg Naçu — C64OS.com