NEWS, EDITORIALS, REFERENCE
A C64 OS App's Start Of Life
I'm pretty close to closing the circuit on being able to launch and run an app in C64 OS. It might feel like it's taken a long time to get to this point, it's been almost a year. But when I think back on my progress, I'm not disappointed. I've had to learn a lot, and I feel like the whole process has really been pulling myself up by my bootstraps. Not only have I learned, realistically, to write code in 6502 ASM, but I've learned a lot about all the various parts of a C64 along the way.
I have my C64 Programmer's Reference Guide, the large, double–sided, fold–out C64 schematics from 1982, and the Advanced Machine Language for the C64 by Abacus. I've also read extensively from the 1541-II user guide and the CMD HD manual, and the Complete Commodore Inner Space Anthology has been super useful. And of course, poring over the disassembled and fully commented BASIC and KERNAL roms and understanding how those routines work has been very important. Learning about software and hardware, and electronics theory at the same time has been unexpectedly rewarding for me intellectually. I feel as though I've learned more about technology in the past year of induldging in C64 retro computing hobby than I have in my fulltime professional software developer career over the past 5 or 6 years.
And let's not forget to add, whenever my fingers get tired of typing, or my butt starts to ache from sitting in my chair for too many hours in the day, I've thoroughly enjoyed the countless of hours of work and planning I've put into my C64 Luggable project. The hours I've sunk into coding and designing C64 OS is nearing a turning point where I can actually launch and run applications. This seems like a good time to take a look at what it's like for the system to start up and launch the default home application, in this case, App Launcher. I've already written at least one post on my ideas for launching applications, so if you're curious you might want to read that too.
As I've gone over in previous posts, the C64 OS KERNAL (for lack of a better word) is divided into approximately 10 modules. These are implemented as one assembly source code file per module, plus a header file (*.h) that declares the exported routines, and an includes file (*.s) that declares constants and macros. The booter is not part of the C64 OS kernal, and it isn't launched the way a proper application for C64 OS launches.
The booter launches more or less the way a standard C64 assembly program might launch. It's assembled to $0801 and starts off with the basic preamble that does a SYS 2061 to kick it into action. The booter must be in the system folder, AND the drive whence it is loaded must have its current partition and current path defaulted the partition and path of the system folder. For example, you cannot load the booter with a command like this:
If you have to specify the partition and path such as in the above example, because the current default partition and path are somewhere else, the booter will fail. I tested this out on the Wheels "starter", just to see if he did something more clever than me, and the Wheels starter has a similar limitation. It completely craps out if the default partition isn't where the starter is located. Although, I believe the starter must also be in the root directory of its partition. This is not the case with C64 OS.
Let's get this out of the way, C64 OS requires the system to be on a storage device that supports subdirectories. This could be an IDE64, or a CMD HD/RL/FD, or any of the wonderful SD2IEC variants out there. Please see the The Commodore 8-Bit Buyer's Guide for what purchase options you have. Other files, documents, images, whatever can be stored on other any other media, like 1541/71/81 or their emulation partitions on CMD devices, or in .DXX images on SD2IEC devices, etc.
When the booter runs, it configures the system file reference.1 It uses $BA (186) to grab the current device number, then it uses a C-P command to read the current partition number from the boot device, and writes these into the system file reference. Next, it opens the sequential file, "config.t" and starts reading it in. It finds config.t in the current partition and path, and if it doesn't find it there, it craps out. The very first line of config.t (subject to change, of course), is the path within the current partition of the system folder. This path entry is essentially just a reflection of the very path where this file is stored. So, isn't there a way to just get this path (present working directory) dynamically? Actually, reliably getting present working directory is a severe pain in the butt, given the way the various file systems on Commodore compatible drives work. If you don't believe me, feel free to read this lengthy thread about the topic. I actually contribute to that thread, in which I mention exactly what I'm talking about here. C64 OS only needs to know where its system folder is, which it gets partially dynamically and partially by the config.t's path entry. After that, all file references are handled in a standard way internal to the OS. And the concept of a PWD simply becomes a file reference held and manipulated by an application.
Before doing any config, the booter loads all of the C64 OS KERNAL modules into memory. Then it reads the config file and builds the system file reference, and a few more config variables such as default system colors. It then runs a drive detection routine and sets up a table of addresses to device types. The booter then JMP's to "runhome" via the system jump table. This JMP, of course, never returns, and nothing from the booter is ever executed again. Any memory it occupied is already marked as available by the KERNAL's memory manager.
Sometimes I give you the source code beautifully formatted in a GitHub Gist. But today you're getting an authentic view of what it looks like to see this code the way I see it, sitting in front of my flat c128.
One of the C64 OS KERNAL modules is service. It is a bit meta, but provides a few critically important, well, services. It handles environment variables. It holds onto the aforementioned system file reference. It tracks which home application it should load when an application quits. And it has the runhome routine that is actually responsible for finding and launching the home application. Service is also the module that hosts the main IRQ service routine.
When first called, runhome checks to see if the home app's file reference has been initialized, and if not it allocates memory for it, and constructs it relatively from data in the system file reference and environment data about the current home app. That is very nearly all it does. It then loads the pointer to the home app's file reference into the .X and .Y registers and JMPs to loadapp via the system jump table. After this point, the home application is treated exactly the same as any other app. The only thing special about it is that there is a system service routine that always knows how to get back to the home app.
Another C64 OS KERNAL module is file. Ideally, it handles all file system accesses. It is actually very lightweight, however, because the C64 OS KERNAL does not replace the C64's KERNAL rom, it just augments it. If you have JiffyDOS then all the actual serial routines are handled by the JiffyDOS rom, including all the KERNAL rom's standard vectors that allow the IDE64 and RamLink to work.
So you might think, well, what does the file module actually do then? Mainly it knows how to work with C64 OS file reference structs, which are constructed and manipulated by other parts of C64 OS and its applications and utilities. These abstract the much more primitive KERNAL system of logical file numbers, and the KERNAL routines SETLFS, SETNAM etc. A C64 OS file reference contains a device address which is used to look up the device type ID from the table built by the drive detection routine that was run by the booter. From that ID file knows if the device supports partitions and subdirectories. The file reference also contains a 16-character filename, a partition number and path. As well as a dynamically managed Logical File Number.
A quick aside; The path can be upto about 230 characters long, which if you have 16 character directory names, plus a one-character delimiter (/), supports nested folders a minimum of 13 levels deep. These can go deeper if the directory names use fewer than 16 characters. 1 character folder names, plus the one-character delimiter, means a maximum possible folder depth of 113. Or an average of about 25 nested folders.2
The file module manages Logical File Numbers (LFN), the ones used by the KERNAL rom, for you. You create a file reference, and open it for read or write. The file module automatically handles getting you a Logical File Number that's not in use, changing the device's default path and partition, and opening the file. Regardless of how you've navigated around the directory structure of the device. When you close a file, it doesn't destroy the file reference, it merely releases the LFN back to the pool, and sets the LFN on the file reference to indicate it's not open. Saving over a previously opened file in a text editor becomes effortless. The reference doesn't just remember the filename, but also where it came from, and will write it back to the correct place.
The first thing loadapp does is initializes the Toolkit. Toolkit has two tables, one for allocating its built–in classes, one for initializing those classes. If an application wants to subclass and extend those classes, it can do so by copying the allocation and initialization tables, which it then extends, and changing the vectors that Toolkit uses to find those tables. Reinitializing Toolkit resets the vectors back to the built–in alloc and init tables. This is necessary to recover from any subclassing efforts of the previous application.
Next it calls prepdisk, which changes the default partition and path of the device so that subsequent disk accesses are relative to the application's bundle. Then it reads the "menu.m" file which every application must have in its bundle. In order to read menu.m, it uses the blksize routine to get the number of blocks the file takes up on disk. Then it allocates the same number of blocks in memory, and reads the file into that allocated space. In C64 OS, atomic reads, that is, file reads that will be opened, read, and closed in short sequence, can use the reserved "templfn" for a temporary logical file number, rather than allocating and releasing one from the pool.
With the menu data in memory, loadapp JMPs to mnuinit in the menu module. However, I'm not going to go down that rabbit hole in this article. Suffice to say it recursively builds a tree of menu nodes which the mnudraw routine knows how to draw.
The memory manager allocates from the top of memory down, remember, so allocations for the menu data and file references and so on are allocated out of memory starting up near where the C64 OS KERNAL is. This is important, because next loadapp loads "main.o", the application's main executable file, out of the app's bundle. The application's main.o must be assembled to $0800. The KERNAL rom's load routine returns the address up to which the load ended in .X and .Y, low byte, high byte respectively. The high byte is the page number and C64 OS uses a paged memory manager. So .Y is left holding the end page number, and .X is loaded with $08, the starting page number, before doing a JSR to pgmark in the memory module. This tells the memory manager the range of pages to flag as allocated.
Next loadapp initializes the mouse, by calling initmouse in the input module. The previous app has the ability to disable the mouse, this will re–enable it. This will also load it from disk, if it hasn't already been loaded. The mouse cursor's sprite data is held in its own file in the system folder and initmouse uses the system file reference to know where to find that.
In general C64 OS uses a mouse–based UI. It provides a rich mouse–based event system that is designed hand–in–hand with the Toolkit, and it also provides a hierarchical pull down menu system. Together these make building an application with a big feature set or a complex UI reasonably simple. However, there are some kinds of apps that might wish to opt out of some of these provisions. C64 OS reserves the top row of characters for itself. There is a CPU busy animation that spins in the top left corner if an app goes into a long loop and stops responding to user input. Then there is the menu strip itself, and in the top right corner is the clock.
There is already a call to disable the mouse pointer, which short circuits the mouse driver so it stops reading the mouse and stops producing mouse events. There is also a set of system draw flags, three bits of which represent the three top row services. The CPU busy, menubar and clock can be prevented from drawing by disabling these flags via the setflags routine in the service module. loadapp calls this to re–enable these features, in case they were disabled by the previous app.
Applications also have the ability to tap into the system IRQ for timed events. loadapp resets the two vectors for custom timers back to their defaults.
Lastly, loadapp makes two more calls, the second of which it never returns from. The first is a JSR to initapp, and when this returns it does a JMP to evtloop. Both of these calls demand an entire section to describe.
Inside the Application
The reason it's so essential that an application is assembled and loaded to $0800 is because C64 OS expects the application to have its primary jump table starting at $0800.
The jump table consists of a series of export vectors, exactly the same way each C64 OS KERNAL module begins with a table of vectors. In an application, they are as follows:
- .word init ;Called with the application first launches
- .word mcmd ;Called when a menu command is triggered
- .word quit ;Called when the application is about to quit
- .word frze ;Called when the application will be frozen
- .word thaw ;Called when the application becomes active after being frozen
The init routine is therefore the first entry point of the application's actual ability to run any code. But remember, in C64 OS, like in modern OSes, the application doesn't ever just go into a hard loop polling for something like a key to be pressed. Instead, it sets up some UI and returns and lets the OS handle the main event loop. Which we'll get to shortly.
In order for an application to interact with the system at all, to be informed of mouse and keyboard events and to be told when to draw, it must push a screen layer. Typically what it would do is first create the basic UI by instantiating and assembling Toolkit views, and wiring up the event callbacks from some of those views to routines. Then it puts the pointer to the screen layer struct into .X and .Y and calls layerpush.
The screenlayer struct consists of 4 pointers to: a draw routine, mouse event, key command event and printable key event handlers. The layer is then pushed on to a stack that supports 4 layers. The topmost layer (4th is topmost) gets event priority. The menu system, cpu busy and clock are hardcoded to draw on layer 4. When the application is initialized and it pushes its first layer, it is pushing layer 1, the bottommost layer. At that time, layer 2 and 3 are unassigned.
When a mouse event is generated, layer 4 has the first shot at processing it. Layer 4 delegates this task to the menu system. If the menu system decides the mouse is not interacting with the menus then the event is allowed to be processed by the next layer. 3 and 2 are skipped because they're not assigned and the mouse routine from layer 1, is called. This is code the application implements. So technically the application can do anything it wants. With the caveat that it shouldn't go into a long loop, it should always try to return back to the main event loop as quickly as possible so as to remain responsive to the user.
While the app can do whatever, typically the app will just foward the call to the toolkit. That's what the screenshot of this app is doing. All three of the screen layer's event pointers are set to "forward" which simply does a JMP to tk_proc. Similarly the draw pointer is set to tkdraw.
You might wonder, what's the point of this layer system if it ultimately just calls routines in the toolkit? Why doesn't the main event loop just call those toolkit routines directly? The short answer is flexibility. It is important that any OS strikes a good balance between easy and flexible. The Toolkit handles a tremendous amount of redundant work. And it's super easy to snap a few of the views together to get a useful, responsive and consisten UI. But if you were forced to use the Toolkit there might be things you'd want to do that would be very difficult.
The way I've set up C64 OS, imagine you are fully onboard with using Toolkit, but you want to have some special keyboard command that isn't represented by a menu item in the menu system. No problem. Set up the first screen layer exactly as illustrated above, except instead of setting the kcmd pointer to "forward", set it instead to some other routine. That routine will be called every time a kcmd is generated (and not already handled by a higher screen layer). Your routine can check to see if it is your special command and do something if it is, and if it isn't it can then pass it on to Toolkit. Boom, the app can inject event handling outside the context of Toolkit that easily.
Here's a more dramatic example. Let's say you want to use Toolkit, but you want to have a dedicated area of the screen, say the bottom 5 rows, completely free form and unmanaged by toolkit. No problem. When the init routine configures Toolkit's root view, you set its draw bounds so that it's height is just 19 rows high (25, minus 1 for the menu bar, and minus 5 for the free form area at the bottom) and aligned to the top. Toolkit will now never draw into that bottom area. Then you set the mouse routine of the screen layer to call your custom routine. That routine checks to see if the click is above the bottom 5 rows, if it is, call the Toolkit's mouse handler. Otherwise, proceed to analyze the mouse event however you please to work with whatever you're doing in the free form area.
A third and final example. A weird example, but just to show the possibilities. You want a button in the Toolkit UI to lock the screen for 5 minutes after you've clicked it. So you create that UI with Toolkit and create the button and wire the button to call a routine on click. That routine configures a system timer for 5 minutes (18000 Jiffys). Then, in the screen layer rather than merely forwarding to toolkit, you check to see if that timer countdown variable is zero. If it's not zero then you return without forwarding any events to Toolkit. If it is zero then you forward to Toolkit as usual.
What you want is the ease of being able to rely on the Operating System for all the things that it's good for, and you want the ability to do something custom whenever you want without having to fight or hack the system.
The Event Loop
After the app has been initialized, it has presumably pushed a screen layer onto the screen layer stack, and configured whatever UI it wants to set up. The initializer returns to loadapp which promptly JMPs to eventloop. The Eventloop will therefore never return to the loadapp routine.
The IRQ service routine is still firing every 60th of a second, the mouse has been enabled, so mouse clicks are producing mouse events, and the key presses are producing keyboard events. The main event loop loops infinitely performing the same steps over and over.
When an app quits, somehow the event loop needs to exit, and not in a way which involved code at the end of a JSR, otherwise we'd have a stack leak. Instead, there is a loop break vector (loopbrkvec). When loadapp enters the event loop after loading an app, it enters at a small prelude that clears the loop break vector, before proceeding to step 1. However the loop loops back to step 1 skipping the prelude that clears that vector. Near the end of the event loop, step 5, checks to see if the loop break vector is set. If it's not set, it proceeds to step 6 and back to step 1 and continues on. If the loop break vector is set however, it does an indirect JMP through that vector. Therefore, it truly leaves the event loop never to return. This is what will be used for doing a proper application quit, however I haven't implemented that yet.
The Event Loop then performs Step 1, Step 2 and Step 3. They have a parallel structure so it's easy to explain. They handle the three primitive event types: Mouse, Key Command and Printable Key. First a JSR to the appropriate routine in the input module checks for the presense of an event on the queue. If the carry is clear, there is an event on the queue. If the carry is set it simply moves on to the next step.
If an event is on the queue it JSR's to proclayers. Proclayers is what controls the fact that the topmost layer gets a shot at the event first. If it's a mouse event, it calls the mouse callback for the topmost layer. This routine does whatever it wants. If it returns with the carry set, then the event has been handled and proclayers is finished and returns. If the carry comes back clear, proclayers loops to the next layer down, checks to see if its mouse routine is not null and calls that. This allows higher layers to always get precedence. And higher layers have the ability to deny the lower layers from ever even having a shot at the event. This can be used for model dialogs, and is also what allows, say, the menu system to prevent a click event from passing clear through the menu and into the Toolkit UI below it.
After proclayers returns, the event is immediately dequeued. This process repeats in step 2 and 3 handling keyboard events the same way. Key commands for example are routed to Screen Layer 4 first, which allows the menu system to pick them up. If the menu doesn't handle them, then they make their way to the lower screen layers, and the app can handle the key command outside the context of the menu system. The menu system, if the key command matches a menu keyboard shortcut, calls the Application's mcmd routine, as we saw configured in the Inside the Application section above.
The handling of events however should never cause the application to try to redraw itself to screen. If an event causes the visual state of the application to become out of date, the application should call the markredraw routine of the screen module. This sets redraw flags for the current layer handling the event. More on redrawing in a moment.
Step 4 of the event loop handles checking for an expired timer. The timer system is two–fold. There is a timer update vector, and a timer check vector. The timer update routine is called by the IRQ, and so it is called very reliably 60 times a second. When the timer ellapses however, that's not when the resultant code for the elapsed timer gets called. The Event Loop's execution time is less reliable, because event handlers could be poorly behaved and fail to return to the event loop in a timely fashion. Eventually though, when the event loop is executed, step 4 calls the check timer routine. If the timer is elapsed the check timer routine is free to call whatever code should execute as a result. Including possibly resetting the timer.
Step 5 of the event loop calls redraw. This redraw routine doesn't just immediately start trying to draw something. It checks to see if the mark for redraw flags are set. If they are, then it calls the draw routines on the layers starting with the lowest layer first. So the application's first screen layer push has its draw routine called. In our example this was set to forward to the Toolkit's redraw.
If the toolkit does something it shouldn't do, such as draw into the top menubar, this will be overdrawn almost immediately. Because the main redraw routine will call the draw routine on the next highest screen layer. This also allows the toolkit to trigger redraws as the result of a timer, even when the user has a menu open. The toolkit may then refresh some part of the screen that is hidden under the open menu. After it does its draw, the next highest layer gets to draw, and eventually layer 4 gets to draw, and the menu will be redrawn above the updated toolkit view.
There are some tricks I'm working on to make this fairly efficient. When the menu system is due to start drawing, for example, it will draw the lower three layers from scratch, and then buffer them to a memory area stashed under I/O. Then, unless the lower layers have their redraw bits set, it will refresh the screen by copying from the buffer, before redrawing the menus above that. This will allow the menus to be drawn above a complex Toolkit UI very quickly. There are some other tricks I'm working on as well. The Toolkit will keep track of which View triggered the screen layer to be dirty, and when it is asked to redraw, it can redraw just that one view. This will allow, for example, a user to type into a text field, and on each key stroke only that single text field will continually redraw itself, rather than the whole screen.
After the redrawing phase is complete, step 5 also handles drawing the CPU busy animation. It starts by resetting the animation character to the default state. Then it sets the jiffy time into a system variable indicating when the event loop last went through step 5. The IRQ service routine, which updates the Jiffy Clock, compares the current Jiffy Time to when the Event Loop last ran, and if more than a couple of seconds has passed, the IRQ service routine starts to replace the CPU busy character with the next frame of an animation set. Because the IRQ runs independently of the Event Loop, if some application code that handles an event decides it is going to do something for an extended time before returning to the event loop, the event loop will be unable to update the event time variable, and the CPU busy animation will automatically start ticking. This is, of course, subject to the system draw flags mentioned at the very beginning. An application is free to disable the CPU busy animation. But, when the next application is loaded, it will be automatically re–enabled.
Before step 5 finishes, it checks the loop break vector, as mentioned earlier.
Step 6 has one job. JMP to step 1! And that is pretty much all that the Event Loop does.
That's the basic start and life cycle of an application in C64 OS. It doesn't go into much detail about how drawing really works, or how to make a UI with Toolkit, or how an application will be quit and its memory deallocated. But, it does show how, starting with the Booter, the first application gets itself running and where it has the opportunity to set up its UI, and how it will respond to events.
Thanks for reading. As always, leave comments, questions and thoughts below.
- I talk a bit about file references in C64 OS here. I promise that I'll go into more detail on file references in the next post, unfortunately as of this writing, I haven't done that yet. [↩]
- 230 characters maximum length for a path string. With 16 character folder names, plus delimiter, we get 230 / 17 = ~13.5 folders deep. If the folder names were all just 1 character, plus delimiter, we get 230 / 2 = 115 folders deep. Or an average of 8 characters, plus delimiter, for 230 / 9 = ~25.5 folders deep. I think this limitation is very acceptable for a C64. [↩]