The C64 OS Programmer's Guide is being written
This guide is being written and released and few chapters at a time. If a chapter seems to be empty, or if you click a chapter in the table of contents but it loads up Chapter 1, that's mostly likely because the chapter you've clicked doesn't exist yet.
Discussion of development topics are on-going about what to put in this guide. The discusssions are happening in the C64 OS Community Support Discord server, available to licensed C64 OS users.
C64 OS PROGRAMMER'S GUIDE
Chapter 2: Architectural Overview
This chapter covers essential parts of how C64 OS works at a low-level. It only goes into a level of detail sufficient to understand how to work with it, how to make use of it. In some places this Guide links to weblog posts that go into more detail on the problems that had to be overcome, the decisions that had to made, and other technical aspects that are not directly relevant to programming.
Command and Control
The regular C64's operating system, (provided by the KERNAL as the low-level portion and BASIC as the user interface, command interpreter and scripting langauge,) is a single-tasking OS and in a very limited sense it is an event-driven OS.
The KERNAL has an IRQ service routine that, among other things, scans the keyboard and places in the keyboard buffer PETSCII characters that correspond to key and key-combination presses. Elsewhere in the KERNAL, (at $E5CD) the computer spends the vast majority of its time in a short loop checking for bytes in the keyboard buffer. This is an event loop, albeit a very primitive event loop. The only events are keys in the keyboard buffer, and when one of these events occurs the loop is briefly left to process that byte, and the computer returns to the loop and settles back in waiting for the next key to be pressed.
C64 OS is also a single-tasking OS and is also event-driven. The C64 OS KERNAL provides an IRQ service routine, in the service KERNAL module, and a main event loop in the screen KERNAL module. The major difference with C64 OS is that both of these are significantly more robust.
The Interupt Service Routine
The Interupt Service Routine (ISR) is running continously in the background. The interrupt is generated by the VIC-II rather than CIA1, so the ISR is synchronized to screen refreshes. The ISR prioritizes the gfx library first, if it is installed, which provides low-level support for graphics split-screen.
Next it polls devices by calling a routine in the input KERNAL module. Polling devices supports calling an optionally installed game controller driver, an optionally installed input controller driver (for the mouse cursor), and then calls an enhanced keyboard driver built into the C64 OS KERNAL. The keyboard and input controller drivers scan the hardware and put input events into three separate event queues, which we will return to later.
After polling devices, the ISR calls the sidplay library if it's installed.
Finally, the ISR performs time related functions. It updates the JiffyClock, it signals updates to the real time clock displayed in the menu bar, it updates CPU utilization history, and counts down timers.
The Main Event Loop
Just as the KERNAL ROM spends most of its time in a short loop checking for bytes in the keyboard buffer, C64 OS spends most of its idle time in the main event loop.
First it checks for the presence of queued mouse events. The event queues are first-in-first-out (FIFO). If a mouse event is present it gets dispatched and processed immediately. When the result of that event processing is complete and control returns to the main event loop, that mouse event is dequeued automatically.
Regardless of whether more mouse events are still queued, the main event loop moves on to the next FIFO queue, which holds key command events. The first one queued is dispatched and processed immediately. When control returns to the event loop that key command event is dequeued automatically.
Similarly, regardless of whether more key commands events are still queued, the main event loop moves on to the printable key events queue. The printable key events queue is identical to the KERNAL ROM's standard keyboard buffer. The first byte in this 10-byte buffer is dispatched and processed, and then dequeued automatically by the event loop.
Next, timer-based events are processed by passing control to the timer KERNAL module. Here things are handled a bit differently; Queued timers are checked for the expired condition. The first expired timer is fired, processing its associated code. Unlike with mouse and keyboard events, each queued timer is checked for the expired condition, and each is fired one after the next before returning to the event loop.
Finally, the last step of the main event loop is a redraw phase. An important principle of C64 OS architecture is that drawing (i.e., updating the screen) and event processing are separate phases. This allows for efficiencies to be introduced by coalescing closely related events. The details of this will be discussed later. But in short, if processing an event leads to visible changes on screen, the Application only flags that visible updates are necessary; It is given an opportunity to perform those redraws at a later stage.
Screen Layers (not a Window Manager)
In a multi-windowed environment, in operating systems for more powerful computers, a window manager is a low-level system that maintains the stacking order of an arbitrary number of windows. The Windows have different sizes and positions on screen, and can partially or completely overlap each other.
For various reaons, C64 OS does not implement a window manager.
- The screen of a Commodore 64 is low-resolution, 320 by 200 pixels,
- The memory capacity of a C64 is small,
- C64 OS has a single-tasking KERNAL.
C64 OS runs one Application at a time, plus one Utility can be opened concurrently with the Application. Instead of a window manager there is a fixed-order stack of screen layers. The stack supports up to three general purpose screen layers, indexes 0, 1 and 2, plus a forth layer is managed by the C64 OS menu KERNAL module. The menu layer is always at index 3, and is unaffected by pushing and pulling screen layers from the general stack.
Screen layers form the lowest level mechanism for event dispatch. They also form the lowest level dispatch for redraw messages and screen compositing. These will be covered in more detail in the following sections, Event Model and The Drawing System.
In summary, when a new Application is being loaded all three general purpose screen layers are pulled off the stack automatically. When the newly loaded Application is initialized one of its tasks is to push its screen layer. This becomes the layer at index 0. It can optionally push another screen layer that would fall at index 1. When a Utility is opened, it pushes its own screen layer, at index 1 or 2. And finally the menu layer is always at index 3. When a Utility is closed, its screen layer is pulled off the stack.
These operations are performed with the layerpush and layerpop calls found in the screen KERNAL module.
Block diagram of screen layers, ownership and event propagation.
Event Dispatch
The input KERNAL module provides a polldevices routine. This routine is not normally called manually, but is instead called internally by the interrupt service routine. By means of the input driver and a built-in keyboard driver, polldevices enqueues three different types of input events, discussed below.
Mouse Events
Although these are called mouse events, they could be generated by a joystick, a light pen, a KoalaPad, etc., by means of alternative input drivers. A mouse event consists of three bytes and the buffer allows up to three such events to be enqueued at the same time.
Every screen layer has a mouse events vector. Low-level dispatching works as follows:
- Event loop processes mouses events
- If at least one mouse event is enqueued, dispatches mouse event
- Calls mouse events vector of highest screen layer
- If call returns with carry clear, dispatch is complete
- If call returns with carry set, loops to call mouse events vector of the next highest screen layer.
- First mouse event in queue is shifted out of the queue.
Note that the call to the screen layer's mouse events vector does not pass the mouse event. It is a notification to that screen layer that a mouse event is in the queue. To retrieve the current mouse event, readmouse in the input KERNAL module is called. Calling readmouse does not dequeue the event, so you can call readmouse as many times as needed.
Because the topmost screen layer gets the event first, it has the power to stop the propagation or permit the propagation of the event to the next layer down, by returning with the carry clear or carry set, respectively.
How mouse events are structured and how to make use of them is discussed in a later part of the Programmer's Guide.
Key Command Events
A key command event is generated by the keyboard driver built into the input KERNAL module. This is a variant of Craig Bruce's three-key rollover routine. A key command event consists of two bytes and the buffer allows up to three such events to be enqueued at the same time.
Every screen layer has a key command events vector. Low-level dispatching is done in a slightly different order than mouse events, and works as follows:
- Event loop processes key command events
- If at least one key command event is enqueued, dispatches key command event
- Calls key command events vector of highest screen layer, below the menu layer
- If call returns with carry clear, dispatch is complete
- If call returns with carry set, loops to call key command events vector of the next highest screen layer.
- If event is propagated by screen layer 0, calls the key command events vector of the menu layer last.
- If the menu layer propagates the event, a system alert is generated.
- First key command event in queue is shifted out of the queue.
The way this works is that if a Utility is open, it has first dibs at handling a key command event. If the Utility panel is out of focus then the Utility framework propagates the event automatically. Next the Application gets an opportunity to handle the key command event manually. If neither the Utility nor the Application handle it, then it goes to the menu layer to search for a corresponding menu item shortcut. If no matching menu item shortcut is found, a system alert is generated. This blinks the border as feedback to the user that the key command was unhandled.
Printable Key Events
The printable key event queue is identical to the KERNAL ROM's keyboard buffer. A printable key event consists of a single PETSCII character and the buffer allows up to ten such events to be enqueued at the same time. The difference is that the events are generated from the driver built into the input KERNAL module, and this prevents non-alphanumeric PETSCII characters from entering the buffer. The reason is because any key pressed in conjunction with one or more modifier keys produces a key command event instead.
Cursor directions, return, delete, insert, home and clear are represented by non-alphanumeric PETSCII characters, however these are all queued as printable key events.
Every screen layer has a printable key events vector. Low-level dispatching is done the same as for key command events, except no system alert is generated for unhandled printnable key events.
- Event loop processes printable key events
- If at least one printable key event is enqueued, dispatches printable key event
- Calls printable key events vector of highest screen layer, below the menu layer
- If call returns with carry clear, dispatch is complete
- If call returns with carry set, loops to call printable key events vector of the next highest screen layer.
- If event is propagated by screen layer 0, calls the printable key events vector of the menu layer.
- First printnable key event in queue is shifted out of the queue.
The above is a description of the low-level event dispatching only. Events can be handled manually by the code in a screen layer. Events that are propagated to the menu layer are handled automatically. If an Application or Utility builds its interface with the Toolkit, the low-level events are forwarded to the Toolkit which has its own higher-level event model which is discussed in another part of the Programmer's Guide.
The Drawing System
Every screen layer has a redraw vector that points to a routine that performs the work of drawing the interface. You should never call the redraw routine manually, the KERNAL calls it at appropriate times.
The general flow should work something like this:
- The Application maintains structures that represent the data model.
- An event is generated, which has an effect, which modifies data in the model.
- If the change in the model invalidates what is being displayed, the Application calls markredraw in the screen KERNAL module.
- The main event loop, later, calls that screen layer's redraw routine.
- The redraw routine draws afresh from the data model.
Even if your data model has not changed, your redraw routine has to be prepared to redraw at any time. The KERNAL calls a layer's redraw routine if any part of the layer on screen gets obscured and then revealed.
Redraws
There are a few essential rules to follow.
First, you should never attempt to draw outside of the system calling the screen layer's redraw routine. Doing draws at arbitrary times is not only inefficient but it will corrupt the composited screen layering. For example, it could result in content that is meant to be on the bottom layer being drawn over top of an open menu or above a Utility panel.
Second, you should never draw directly into screen memory. This is because, at the lowest level, the gfx library dynamically combines the system screen buffer and a source of graphics data to support splitscreen. Drawing directly to screen memory could corrupt graphics data, or the changes could be completely undone by the user moving the split position.
There are several ways that C64 OS helps you perform redraws.
Buffered vs. Unbuffered
It is possible and acceptable, and sometimes even desirable to draw directly into the system screen buffer. However, it is not always a good idea to do so. Whenever a higher level layer changes in such a way that it uncovers an area of the screen that was previously obscured, the system must call the redraw routines of all lower screen layers. For example, when a menu is opened it obscures lower layers by partially covering them up. Only the menu layer draws itself when a menu opens. But when a menu closes it uncovers an area of the screen that it previously obscured, and at that point the layers beneath need to be redrawn.
If redrawing is a complex task requiring more than a fraction of a second, then redrawing every time a menu closes will make rolling over menus noticeably sluggish, which is undesirable. It would also make dragging around a Utility panel feel sluggish. In this case, you should introduce a layer buffer.
For example, in the Hello World sample app, the draw routine is very simple. It clears the screen and draws the message "Welcome to C64 OS!" somewhere in the middle. In this case there is no layer buffer. The drawing is performed directly into the screen buffer.
But if the draw routine is more complex, for example if the user interface is built on Toolkit and has several nested views, it would be much too slow to have Toolkit draw everything from scratch every time. Instead you should reserve some memory as a layer buffer (and a corresponding color buffer of the same size.) The layer buffer can be statically allocated, or it can by allocated at runtime by making calls to the memory KERNAL module to reserve some free contiguous pages of memory from the heap.
When using a layer buffer, it needs to be copied to the screen buffer when the layer's redraw routine is called. More on this below. When your data models change, invalidating what is in the layer buffer, your app should set a dirty flag and then call markredraw. When the layer's redraw routine is called it should check the dirty flag. If the layer is dirty, then a redraw to the layer buffer can be performed first, followed by copying the layer buffer to the screen buffer. If the dirty flag is clear, the heavy work of updating the contents of the layer buffer can be skipped. If drawing is expensive, this can dramatically speed up your Application.
Pre-rendered Templates
If your Application uses a layer buffer, it is usually preferable to allocate the buffer at runtime so it doesn't have to be loaded from disk as part of the Application's main binary. However, there is a case where statically allocating the layer buffer is advantageous.
You can pre-render the bulk of an interface, with labels, buttons and boxes, etc., already drawn and colored in the layer buffer (and color buffer) in the statically allocated memory in your source code. The Memory Utility in C64 OS is an example of a pre-rendered template.
The key of 6 colors at the top, the memory addresses running down the left side and along the top edge of the memory map, and the labels below the memory map are all statically drawn in the layer buffer. The memory map itself and the values of the labels below the map are updated dynamically by the redraw routine.
Context Drawing System
Pre-rendered templates are suitable for some types of user interface, but they are mostly static and therefore not flexible or responsive. To make an interface more dynamic it is necessary to draw the interface from bits and pieces using a draw routine that can take variables and data structures into account.
In order to make drawing easier, C64 OS provides a context drawing system that is built into the screen KERNAL module. The context drawing system makes some tasks that would be quite tricky much easier.
You start by defining a draw context structure. This contains pointers to the origin coordinates of a rectangle within which to draw, the width and height of that rectangle, and 16-bit top and left scroll offsets of the area within that rectangle. Along with the width of the actual layer buffer, these define a clipping window into a very large virtual canvas.
You then set draw properties on the context, such as the direction of the cursor's travel either horizontal or vertical, reverse or standard drawing, automatic ASCII to PETSCII conversion, and a color.
Next you specify the row and column coordinates, again in 16-bit, of where you want to start drawing within the virtual space defined by the context's rectangle. Then you call ctxdraw repeatedly, outputing one character at a time. ctxdraw preserves the X and Y registers so you can use those to loop over the source content. The cursor position within the virtual space is automatically propagated along the direction of travel. Draws to coordinates that fall outside the bounds of the rectangle are automatically clipped, and ctxdraw returns status about when the cursor is beyond the right or bottom bound of the rectangle such that all further draws will necessarily be clipped, allowing you to safely exit the loop.
While some of this (such as 16-bit scroll offsets) sounds complicated, much of the complexity can be ignored for simple situations by setting the scroll offsets to zero. On the other hand, when you use the context drawing system, it becomes evident how convenient it is for making tricky things (like clipping) easy.
The Toolkit
The Toolkit is a high-level system for building complex, dynamic, flexible, user interfaces in a way that is relatively easy. Toolkit is object-oriented and provides numerous built-in classes and many more premade classes that can be loaded at runtime by your Application.
It's a bit tricky to wrap your head around some of its nuances, and more involved still if you want to create custom classes. However, compared to what Toolkit classes can accomplish, it is orders of magnitude easier to learn to use Toolkit than it would be to try to implement the functionality of its classes using conventional programming and manually drawinng everything.
Under the hood, Toolkit classes backend on the context drawing system, but they perform all the drawing for you. Toolkit is a complex topic and is discussed in Chapter 6: Using the Toolkit.
The context drawing system, whether used with manual drawing routines or by Toolkit classes, can use a statically allocated layer buffer, a dynamically allocated layer buffer, or draw directly into the system's screen buffer.
The context drawing system, and by extension the Toolkit, can also be used with a static layer buffer that combines a pre-rendered template. Peek and Settings Utilities are examples in C64 OS that use a hybrid of Toolkit and pre-rendered template in a static layer buffer. The buttons and controls at the bottom of Peek are Toolkit views, but the content above starts as a pre-rendered template that gets manually redrawn.
The tabbed sections of Settings and the icon labels are made with a pre-rendered template, but the buttons are Toolkit views, laid out using a transparent container view.
The Menu System
The menu bar at the top of the screen and the status bar at the bottom of the screen are rendered on screen layer 3, as mentioned above about Screen Layers.
The menu bar and status bar and the hierarchical pull down menus are all drawn by the menu KERNAL module. The optional clock (right end) and CPU busy indicator (left end) are drawn by the services KERNAL module. A set of system draw flags controls, among other things, whether the menu bar or status bar are visible, and whether the clock or CPU busy indicator are to be drawn on the menu bar. Global keyboard shortcuts can be used to toggle these flags and thus toggle the visibility of the menu bar and status bar.
By default:
COMMODORE+SPACE toggles the menu bar CONTROL+COMMODORE+SPACE toggles the status bar
The menu system is always active, even if you've toggled the menu bar visibility off. The menu code in the KERNAL renders the menus in realtime from a tree of linked memory structures. The memory structures are created by reading a menu definitions file into memory, and then parsing the menu definitions data and converting them, in place, into linked structures. Loading menu definitions files, parsing and linking is not handled by the KERNAL, but by the loader.
The loader is special C64 OS service called loader and found in the //os/library/ directory. The loader is loaded temporarily and run to facilitate switching Applications. One of its jobs is to free the memory used by the previous menu structures, and load in, parse and link the menus for the new Application being loaded.
The Utilities menu is loaded, parsed and linked too, but this happens during boot up. The Utilities menu comes from a standard menu definitions file called utilities.m found in //os/settings/. An Application's menu definitions file is called menu.m and is found in the root of the Application's bundle directory. For example:
//os/settings/:utilities.m is the global Utilities menu definitions //os/applications/Hello World/:menu.m is the menu definitions for Hello World
Menus maybe up to 4 layers deep, the top-level menu bar, plus three levels of submenus. Menus support spacers for visual organization. Menu items have a title. Leaf menu items have an action code that consists of a single byte, and may optionally have keyboard shortcut. The keyboard shortcut is defined by two bytes, a letter, number or symbol to be pressed in conjunction with a modifier flags byte for one or more modifier keys. Modifier key combinations can be any of the following:
COMMODORE CONTROL COMMODORE+SHIFT CONTROL+SHIFT COMMODORE+CONTROL COMMODORE+CONTROL+SHIFT
SHIFT cannot be used on its own as a modifier key, because SHIFT+letter doesn't produce a Key Command Event but instead produces a Printable Key Event. Menu items can only be triggered by Key Command Events.
Action Codes
Every Application starts with a standard block of vectors that allow the Application to plug into the operating system. One of those vectors points to the Application's message handler. When a menu item is selected, whether by mouse or by its keyboard shortcut, its action code is sent to the Application as a menu command message. The Application can then do whatever it wants in response to that Action code being triggered.
Menus and the Application's support of action codes are only loosely coupled. The Application might support an action code that has no representation (no way of calling it) in the menu definitions file. The title associated with the menu item is irrelevant to the action code it sends the Application, this allows for customizing or even translating the menus to another language without needing to modify the Application's main binary. And lastly, how the menu items are ordered or nested is also unimportant to the Application.
There is one reserved action code. Hex value $01 (not PETSCII "1", which is hex value $31.) When a menu item is triggered with action code $01, the menu system intercepts this and rather than sending this as an action code to the Application, it opens a Utility whose name is an exact match for the menu item's title. This is how the menu system opens Utilities from the Utilities menu, without requiring support from the Application. The Application's own menus may also take advantage of this.
All other menu items with an action code that is not $01, must have unique action codes.
Modes and Flags
Menu items may also be in one of two states: selected and disabled. A selected menu item displays a checkmark to the left of the title. For example, App Launcher has 5 desktops which can be selected from the Go menu. The menu item for the desktop you are currently on is displayed with the checkmark.
Disabled menu items show in the disabled color, do not highlight when rolled over, and cannot be triggered by mouse or keyboard shortcut. It is possible for a menu item to be selected and disabled at the same time.
The selected and disabled modes of menu items are, for very good reasons, not stored in the menu data structures. Instead, your Application must support menu equiry messages. In realtime, whenever the menu system is about to draw a menu item, it sends the action code to the Application in a menu equiry message. At a mimimum the Application can support this message type by always responding that the item is enabled and unselected.
To support selections, the Application needs to maintain its own variables about which menu items are selected. When the menu enquiry message is received, the Application can use a routine and logic and consult its variables to determine if the menu item that corresponds to this action code should be displayed as selected or not, and returns that status in realtime. Although this sounds complicated for the simplest case, it makes things much easier in more complicated cases. For example, a single variable, such as current_desktop can be used to hold a number, say, 1 to 5. When an enquiring message arrives for the action code for desktop 1, a bit of logic returns the selected flag if current_desktop is equal to 1.
If the status info was written into the menu structures, then a change of selection would have to find the menu structure for the new item and write the status flag into it, but it would also have to find the menu structure for the previously selected item and remove the status flag from it. Responding to menu enquirying messages is much easier and much more flexible.
Memory Management
Overview
The C64 has 64 KB of main memory, plus half a KB of video color memory arranged in 1024 4-bit nybbles. In addition to the RAM the C64 also has 16 KB of code in ROM plus 4 KB of bitmap data in ROM for the two character sets. Lastly, the C64 has 4 built-in I/O chips and external expansion capabilities for more. All of these need to be addressable by the CPU.
Since the C64's 6510 processor can only address 64 KB at a time, some regions of the address space are shared. The low 3 bits of the 6510's built-in port (called the processor port) control the mapping of the contentious addressing regions.
Mapping Modes
The low 2 bits (bits 0 and 1) of the processor port specify a dependency level. Each time the numeric value of these 2 bits increases another region is mapped in that depends upon all the previously mapped in regions. At the base is RAM, level 0. At level 1, the I/O chips are all mapped in as a group. At level 2, the KERNAL is mapped in which is 8 KB of ROM and the KERNAL depends on I/O. At level 3, the BASIC interpreter is mapped in, which is an additional 8 KB ROM which depends on the KERNAL.
This can be summarized with the following table:
Level | Binary Value | I/O Region | KERNAL Region | BASIC Region |
---|---|---|---|---|
0 | 00 | RAM | RAM | RAM |
1 | 01 | I/O | RAM | RAM |
2 | 10 | I/O | KERNAL | RAM |
3 | 11 | I/O | KERNAL | BASIC |
Because the areas are progressively mapped in, the mapping mode can be stated with just one word, the latest area to be mapped in. Thus the mapping modes are: RAM, I/O, KERNAL and BASIC.
The C64 Memory Map
The uncontended addressing areas, those which are only used for RAM, are divided by the location of the BASIC ROM. Uncontended memory below the BASIC ROM we call low memory and that above the BASIC ROM we call high memory.
Region Name | Address Range | Size | 1st Function | 2nd Function | 3rd Function |
---|---|---|---|---|---|
KERNAL | $E000–$FFFF | 8K | RAM | KERNAL ROM | — |
I/O | $D000–$DFFF | 4K | RAM | I/O | CHARACTER ROM* |
High Memory | $C000–$CFFF | 4K | RAM | — | — |
BASIC | $A000–$BFFF | 8K | RAM | BASIC ROM | — |
Low Memory | $0000–$9FFF | 40K | RAM | — | — |
*The third bit of the processor port (bit 2) is a special flag that when set substitutes the Character ROM for I/O, whenever I/O would be mapped in. This mode is highly unusual, rarely ever needed and so we'll ignore it.
Standard Units of Memory
A convenient way to divide the addressing space is into memory pages. The 16-bit memory space is addressed with two bytes, the high byte and low byte. The high byte specifies a page number from $00 to $FF (0 to 255) and the low byte specifies a byte within the page from $00 to $FF (0 to 255).
Thus we can describe the C64 as having 256 pages of memory. 4 pages make a kilobyte. 16 pages make a 4KB block, which is the smallest unit size remappable by the PLA. The whole memory range consists of 16 4KB blocks. The character set occupies a single 4KB block. The BASIC ROM occupies two 4KB blocks, the KERNAL ROM occupies two 4KB blocks. High memory consists of a single 4KB block. I/O consists of a single 4KB block. And many things consist of individual pages.
For the VIC-II's use, the whole memory range consists of 4 16KB banks, only one of which can be accessed at one time.
Memory Management by Region
Low memory is divided into two chunks, workspace memory and heap memory. Workspace memory is shared by everything but is statically allocated. This is partially for hardware reasons, partially for firmware reasons, and partially for speed and practicality.
The hardware reasons. The 6510 has a single fixed address stack. It's one page of memory found at page $01. This cannot be moved, and so this memory can't be arbitrarily used by something else. The 6510 also has addressing modes, indirect indexed and indexed indirect, which cannot be used anywhere but zero page. Zero page is, as it sounds, one page of memory at page $00.
The firmware reasons. C64 OS uses the KERNAL ROM, and under certain circumstances it also uses the BASIC ROM (for example, to do floating point math.) The KERNAL and BASIC code are in Read Only Memory, therefore they require writable memory as a scratch pad for their variables. This scratch pad makes up part of what we call workspace memory.
The speed and practicality reasons. Consider the table of themed colors, for example. It needs to be stored in memory somewhere. If it's embedded within the KERNAL then its actual address becomes unstable; it moves around as the code in the KERNAL gets rearranged. The only way to access it reliably would be via some indirection such as an initial lookup or a system call. In practice this is much too slow as you have to juggle registers and make system calls repeatedly while looping through running draw code. Instead this table is statically allocated to an area of workspace memory and can be read directly.
$0000 - $08FF (9 Pages, 2.25 KB) - WorkspaceWorkspace memory runs from $0000 to $08FF (9 pages) and that includes 4 pages for the system screen buffer.
$0900 - $82FF (122 Pages, 30.5 KB) - HeapHeap memory follows workspace memory and extends all the way up to where the memory resident Toolkit classes begin. Thus, heap starts at $0900 but where it ends is dynamic and can vary slightly depending on what KERNAL modules and Toolkit classes get loaded and what hardware is being used. How heap memory gets allocated is discussed below.
$8300 - $A2FF (32 Pages, 8 KB) - ToolkitThe default mapping mode of C64 OS is KERNAL. The BASIC ROM is mapped out by default but can still be mapped in, carefully and temporarily, when needed. A core set of 9 Toolkit classes are loaded to a special area of the C64 memory map where an external ROM can be mapped. Toolkit memory impinges slightly into the bottom of where the BASIC ROM is located.
IDE64 Map Exceptions
IDE64 is a special cartridge that dynamically maps itself into this area when its DOS routines are being executed. This is the reason why DOS command strings to be sent to the IDE64 cannot be stored in RAM between $8000 and $BFFF. When an IDE64 is detected, the boundary between heap and Toolkit memory gets automatically shifted down so the last byte of heap is $7FFF.
The C64 OS KERNAL occupies all of high memory and most of the memory under the BASIC ROM. The KERNAL always butts up against the end of high memory, $CFFF and is loaded downwards, ending somewhere under the BASIC ROM, below which Toolkit memory begins.
$D000 - $DFFF (16 Pages, 4 KB) - Video MemoryThe I/O region follows high memory. There is RAM under I/O but because this region is shared the memory cannot be used for arbitrary purposes. C64 OS automatically uses this area for video memory. The advantage being that the VIC-II, when configured for bank 0, can always see this RAM even when I/O is mapped overtop of it for the CPU. Video memory is used for the custom character set and the VIC-II's screen matrix memory. It is also used as a color memory buffer, which corresponds with the screen memory buffer found in workspace memory.
$E000 - $FFFF (32 Pages, 8 KB) - Graphics/Utility MemoryLastly, above I/O comes the KERNAL ROM. The memory below the KERNAL ROM is managed by C64 OS as one big chunk. There are flags that indicate how the entire chunk is currently being used, free/unused, a Utility, bitmap graphics data, or a generic buffer managed by the Application.
When a Utility is loaded into this area, the Utility itself and the Utility Framework are in control of how this area gets sub-allocated. But the flags indicate that a Utility is currently occupying the area. This is how it knows that before replacing this memory with something else, it needs to (and can safely) ask the currently running Utility to close.
In splitscreen or fullscreen graphics mode the bitmap data are installed to this area. This is why splitscreen and a Utility cannot be used at the same time.
The VIC-II is always configured for bank 0. This allows it to access a custom character set, screen matrix memory and sprite pointers from the RAM under I/O. And also to access bitmap graphics under the KERNAL ROM. Bitmap data occupies only 8000 bytes, leaving a final 192 bytes. The mouse pointer sprites occupy the first 128 bytes of the last 192 bytes of memory.
The last 6 bytes of memory are used for CPU vectors when the KERNAL ROM is mapped out.
Heap Memory Allocation
There are two levels of heap memory allocation. The lower level is paged allocation and the higher level is done with malloc and free. Heap memory consists of approximately 122 pages, or 30.5 KB. In workspace memory there is a page allocation table consisting of 150 bytes, where each byte represents one heap memory page. A byte value in the table represents the allocation status of that entire corresponding page.
In C64 OS v1.0 there are 4 defined values:
Constant | Value | Meaning |
---|---|---|
mapfree | $00 | An unallocated page |
mapsys | $01 | A page allocated by a system process |
maputil | $02 | A page allocated by the current Utility |
mapapp | $ff | A page allocated by the current Application |
There are two calls in the memory KERNAL module, pgalloc and pgfree. When some process needs memory it calls pgalloc, passing which kind of allocation it's making (mapsys, maputil or mapapp) and the number of consecutive pages to allocate. Pgalloc looks for the first range of that many consecutive free pages and marks each of them as allocated with the requested allocation type.
Before returning it takes two more steps. First, it zeros all the memory in those pages, then it initializes them as a memory pool. The first byte of the first page is a count of how many pages long the pool is, and creates a malloc header that defines all remaining space in the pool as a single malloc'd block, but which is marked free. It then returns to the caller the page number of the first page of the memory pool.
Pgalloc searchs for the consecutive pages from the top down. For example, if you ask for 2 pages it will start by looking at the map at page $82. If that's free, it looks at the map for page $81. If that's free, then those are the firt two consecutive pages, both get marked as allocated and page $81 is returned as the first page of the memory pool. If $82 is free but $81 is not, then it moves onto $80, and so on working down through the page allocation map until it finds the highest 2 consecutive free pages.
The process can do anything it wants with this memory; It is allocated for any use; It doesn't have to be used as a malloc memory pool. In order to free this memory, the same process can call pgfree, passing the start page and the number of consecutive pages. Pgfree sets mapfree on each corresponding byte in the page allocation map.
No memory protection
There is no explicit connection between how many pages were allocated and how many pages get freed. For example, you could pgalloc 2 pages and then later pgfree one of them. How many pages your process has allocated and which pages must be kept track of manually.
If your process frees pages that it did not allocate, a crash will soon follow. So, don't do that.
One call to pgalloc for 3 pages is not the same as calling pgalloc 3 times for 1 page each, even if the three single pages end up side-by-side in the page allocation map. The single call for 3 pages will be initialized as a single memory pool of 768 bytes. The three separate calls will initialize each page as a separate memory pool, 256 bytes each.
If you allocate a multi-page memory pool but then use pgfree to deallocate just some of those pages, that memory pool becomes invalid for use with malloc. Continuing to use malloc (or free) on an invalid memory pool will eventually lead to memory corruption and some kind of crash.
When a Utility is closed, unless you are intentionally leaving some pages allocated (for some kind of terminate and stay resident purpose) you must pgfree all the pages that were allocated. Otherwise, your Utility is leaking memory.
However, when the loader loads a new Application it takes certain steps automatically. First it closes any open Utility. Then it marks free all pages that are allocated as maputil or mapapp. In other words, even the memory that has been leaked by a misbehaving Utility will get cleaned up when switching Applications.
Malloc and free
Malloc and free are available in the memory KERNAL module. Malloc can only be called in the context of a memory pool. Along with a 16-bit size to allocate, you must also pass the first page of the memory pool whence to make the allocation.
Malloc starts at the start of the memory pool and searchs for the first available block of any size. If the block is smaller than the requested size it tries to merge it with the following block and then checks again to see if it's too small. It continues to merge blocks until either the next block isn't free so it can't be merge, or the last merge operation created a block at least big enough to hold the requested size. Finally, malloc splits the current block into the exact size requested which it marks as allocated, plus a following block that is marked available.
Malloc is indifferent to the page allocation map. It only knows how to merge and split blocks from pre-existing blocks. This is why pgalloc initializes the memory pool with one large available malloc block. When malloc is called, it will split that block, carving a smaller block off the original. Malloc returns a pointer to the start of the allocated block. If malloc cannot make the requested allocation, it does not raise an exception, it uses the carry to indicate success or failure.
Free can only be called by passing a pointer that was previously returned from malloc. Free doesn't do any merging, it just marks the block as available. Merging only happens, when necessary, during calls to malloc.
When to use Malloc and when to use Pgalloc
You may wonder what the point is differentiating between these two types of allocation and when each type should be used.
On a bigger computer with more memory and a hardware memory manager, each process is given its own heap and that whole heap is initialized to accept malloc calls. If the heap is exhausted the hardware memory manager can dynamically map new pages to grow that heap. Even if the pages in real memory are not contiguous the hardware memory manager makes them appear contiguous from the perspective of the process.
The Commodore 64 does not have this ability. The CPU's address lines are hardwired to the RAM row and column selector lines. The PLA has the ability to disable RAM when the CPU is accessing the address bus, and can map ROM or I/O chips into any of the sixteen 4KB blocks. But when RAM is enabled the PLA cannot remap blocks of RAM to other addresses. This is one reason why multi-tasking on the C64 is not ideal.
The reason then why malloc cannot be called on the whole of heap memory is because the heap is shared by multiple processes. You might think that because C64 OS is not multi-tasking that talk of different processes is nonsensical. But there are different processes that can cooperate and share memory and CPU time. The Application, the Utility, the system itself, and certain libraries or drivers may all wish to allocate some memory for themselves. If everything were mallocing from the same common heap then the Application, the Utility and the system would end up having interwoven arbitrarily sized allocations. When a Utility is closed, it would be necessary to free each and every individual allocation.
With memory pools page-sized blocks of heap are marked as being owned by the Utility, or owned by the Application, etc. When push comes to shove, the system can identify which pages belong to a Utility or an Application and free them by force without depending on them to be well behaved and free all of their tiny allocations.
There is one other important consideration that makes memory pools not just nice but necessary. Some situations on the C64 require page alignment. Malloc returns pointers to arbitrarily sized blocks of memory which is convenient for some purposes, but these blocks are not page aligned. For any situation where you actually require some memory to be page aligned, use pgalloc instead.
Some examples: The directory library (dir.lib) loads directory entries 8 at a time, 32-bytes per entry, and aligned to fit perfectly in a 256-byte page. Therefore dir.lib uses pgalloc to accomplish this. File references are frequently passed with only a page byte, and numerous optimizations are made to shift pointers to elements of a file reference, all of which require file references to be page aligned. Therefore pgalloc is used to create a new file reference. Toolkit is made up of many classes, each creating an object of a different size. Many objects are required to make up a user interface, and objects can be created and destroyed at runtime. Objects need to be efficiently packed together to save memory. Therefore, tknew implicitly uses malloc to instantiate new Toolkit classes.
The File System
A C64 OS installation is contained in a single directory, called the system directory which must itself by located in the root directory of any partition of a compatible boot device.
Referring to DOSes
The DOS on CMD devices, SD2IEC and IDE64 are all very similar, for brevity I will distinguish between CBM DOS (found on legacy floppy drives) and CMD DOS (found, loosly speaking, on all the modern devices.)
Where necessary I'll indicate if something is specific to IDEDOS or SD2IEC.
The name of the system directory is, by default, "os". It can be renamed, for example, to allow you to have multiple installations of C64 OS in same same partition. (See: C64 OS User's Guide, Chapter 2: Installation → About the Installation Location.) However, for the development system the default "os" is expected. Paths throughout the documentation are referenced as //os/library/ or //os/drivers/ etc. And some contants defined in headers define //os/ in the path as well.
The system directory contains numerous subdirectories that aim to be logically named using human readable conventions. Acronyms are generally avoided, with a few exceptions. C64 filenames cannot exceed 16 characters and TurboMacroPro cannot have single lines that exceed 40 characters. Therefore, path components to assembler includes have been intentionally kept short. Here's why:
1234567890123456789012345678901234567890 ← Guide for screen width .include "//os/h/:modules.h" ← This fits. .include "//c64 os/headers/:modules.h" ← This does not fit. xXxXxXxXx ← Typical auto-indent for labels.
As you can see above, out of 40 characters, 9 are typically taken up by a standard indent depth for labels. Plus you have the overhead like the pseudo-opcode .include and the quote marks. Leaving just 20 characters for the path and the filename.
CMD DOS supports relative paths from the current path down, by omitting the leading root slash. But it does not support stepping up a directory relatively. For example:
Assuming the current directory is //os/applications/ /Hello World/resources/ ← Valid relative path from current directory. //os/settings/ ← Valid absolute path from current directory. /../settings/ ← Invalid relative path up and back down. /←/settings/ ← Invalid relative path up and back down.
Despite the fact that .. is a standard modern convention for relatively referencing the parent directory, and despite the fact that ← is used with the CD command in CMD DOS to move to the parent directory, neither of these may be used as part of a relative path.
The system directory name of "os" was intentionally chosen instead of the more obvious "c64 os". Paths to assembly include files are going to be absolute paths and therefore the system directory name will be part of every path. Typically, code files will not include files from arbitrary directories, like the applications directory or the library, they will include files from one of a few limited sources of special directories used just for programming headers.
Programming Header Directories
In fact, the programming headers directories are included for the benefit of being able to write software for C64 OS, on a C64, using the C64's own file system and the C64 OS system directory as the environment. But the programming headers are not required to run C64 OS itself. This is relevant, for example, if you try to create a custom C64 OS installation on a CMD FD-2000 disk. You can save lots of space by excluding the programming header directories.
The programming headers are found in the following directories:
Path | Notes |
---|---|
//os/s/ | Constants, structs and macros |
//os/s/ker/ | KERNAL ROM jump table and constants |
//os/h/ | C64 OS KERNAL and library jump tables |
//os/tk/s/ | Toolkit constants and structs |
//os/tk/h/ | Toolkit class method jump tables |
Conventional Filename Extensions
For the same reason that include path names are kept short and because filenames can't exceed 16 characters, a set of common filename extensions used in C64 OS are only a single character. Including the dot that leaves only 14 characters for a filename.
Extension | CBM File Type | Notes |
---|---|---|
No extension | PRG | Loadable program |
.a | PRG | TMP assembly file (binary format) |
.d | SEQ | Non-executable data (sequential binary) |
.h | SEQ | TMP assembly header file (sequential text) |
.i | SEQ | Initialization config file (sequential binary) |
.m | SEQ | Menu definitions file (sequential text) |
.o | PRG | Object file (assembled binary) |
.p | SEQ | PETSCII-graphics screencodes (sequential binary) |
.r | PRG | C64 OS relocatable object file (assembled binary) |
.s | SEQ | TMP assembly include for contants (sequential text) |
.t | SEQ | Human readable text file (PETSCII text) |
The "s" directory and the "h" directory are so named because they contain .s and .h files respectively. In practice however, both directories have .t files as well, usually in pairs of files with the same name except for one with the .s or .h and the other with the .t extension.
The .t file is the source file, containing all of the human readable notes, comments and separators for easy reading. The .t files are edited, saved, and then exported to their equivalent .s or .h file, which contain only the definitions with all comments and other human readable flourishes stripped away. Stripping everything extraneous out of the .s and .h files makes it much faster for the C64 to assemble, and they consume much less memory.
Putting this altogether then we can explain the following four files:
Path | Filename | Notes |
---|---|---|
//os/h/ | screen.t | screen KERNAL module jumptable routines with human readable notes. |
//os/h/ | screen.h | screen KERNAL module jumptable routines, labels only. |
//os/s/ | screen.t | screen KERNAL module constants and structs with human readable notes. |
//os/s/ | screen.s | screen KERNAL module constants and structs, labels only. |
Notes on some common subdirectories
Most of the subdirectories in the system directory are used to store files of a specific type. In most cases (though not all) files must be installed in their proper subdirectory. On the flip side, new items can be installed simply by copying them into their proper subdirectory so the system can find them.
//os/applications/Applications must be installed in the applications subdirectory.
When an Application is launched from App Launcher, it starts with only the name of the Application to launch. The name is looked up in the applications subdirectory.
Applications are bundles. The bundle consists of a subdirectory which contains numerous standard components which together comprise the Application. In File Manager an Application bundle that is in the applications subdirectory gets presented a single launchable item. Double clicking it opens the Application rather than navigating into its bundle directory. The moment you move an Application bundle somewhere else, it reverts to behaving like a regular subdirectory with a bunch of files in it, and the system stops recognizing it as an Application.
//os/c64tools/This directory is reserved for non-C64-OS programs. Fresh copies of the booter, the setup tool, and c64restore are kept here, along with various other small programs that help with native development and testing.
//os/c64tools/This directory is reserved for non-C64-OS programs. Fresh copies of the booter, the setup tool, and c64restore are kept here, along with various other small programs that help with native development and testing.
//os/charsets/Various character sets are stored here. The booter has a special boot.charset that includes the C64 OS logo while booting. The 2x1.digit font is stored here. Other bitmap font files will be stored here as they get developed.
//os/clipboard/The clipboard's type and data files are stored here. 0.d is the data file for the current clipboard. 0.t contains the type and size metadata for the current clipboard. The numbering of these files is in anticipation of multi-clipboard support.
//os/desktop/App Launcher's desktop directories are stored here. The aliases of each desktop are stored in their numbered subdirectories. Because aliases are each stored as individual files, other Applications can be written to allow modifying or creating aliases. An obvious addition would be a Utility that takes file selections from File Manager and allows you to create aliases to them on any desktop.
Desktop backdrops are also stored here.
//os/drivers/Drivers must be installed here. The booter, Utilities and Applications look for drivers by name and only look in this subdirectory.
Driver types are destinguished from one another by a filename extension prefix. RTC, PTR, JOY, etc. Although drivers are installed by putting them here, which driver of each type is actually used is specified in //os/settings/:drivers.t. A Drivers Utility will be written to manage all the selected drivers from one place.
//os/icons/This subdirectory contains individual icon files, named by their index value in hex. The icon indexes are defined by //os/s/:icons.t. Using this file, for example, you can discover that the trashcan icon is $28. You can thus replace //os/icons/:28.d with your own version and everything that sources the trashcan icon from the system icon library will now display the new version. No reassembly required.
Icons are loaded into memory using the loadicn KERNAL call. However, icons $00 to $14 are stored in //os/charsets/:ui.charset and are always memory resident. In order to modify these core icons, after modifying the individual icons ($00 to $14) they need to be packed together into a new version of ui.charset.
//os/kernal/All the KERNAL modules and their alternate versions are found here. The booter is responsible for loading KERNAL modules into memory. It references the file //os/settings/:modules.t which lists each module with its relative path from the system directory.
Although you could put a KERNAL module somewhere else and change //os/settings/:modules.t to reference it there, this is not recommended. Various coded bytes are used before each module to allow the booter to select between different alternatives based on available hardware.
//os/library/Libraries that are globally available to all Applications must be installed in the library subdirectory. However, libraries can also be installed locally by including them in an Application's bundle.
Libraries are loaded using a KERNAL call. The //os/library/ directory is checked first. If it's not found there then it checks in the current Application's bundle.
Boot components are also found in the library subdirectory, along with a few other core system components, such as the loader. Libraries always end with .lib.r.
//os/library/loaders/A subdirectory of the library is used for datatype loaders. Datatype loaders must be stored here in order to be available.
//os/pointers/Mouse pointers are stored here. The Mouse Utility offers a selection between four different styles. Each is stored as a separate file. You can replace one or more of these files with custom mouse pointers. An Application or Utility to draw mouse pointers, and other icons, would be a nice addition.
//os/screengrabs/The KERNAL supports a configurable keyboard shortcut to trigger a screen grab of the text-based UI. When the shortcut is activated the KERNAL loads grab.lib which auto-runs and auto-unloads itself immediately after. It writes the screen grab files here by default and names them based on the current time. This allows you to grab the screen once per second without the names conflicting or overwriting each other.
//os/services/This subdirectory stores the Application bundles for App Launcher and File Manager. These two Applications are special, they are required for C64 OS to operate correctly and must therefore never be scratched. Applications installed in the applications subdirectory, on the other hand, are not essential. There may be other service Applications in the future.
//os/settings/System-wide settings files, such as those used by the booter: drivers.t, modules.t, components.t, config.t, and others are stored in the settings subdirectory. Additionally, Utilities that save their state globally write their state files to here. Utilities that don't save their state globally, write them to the Application bundle of the current Application instead.
Other globally accessible settings are found here, such as the utilities.m file that builds the Utilities menu at boot, the favorites that are created in File Manager, the version number of C64 OS, and others.
//os/settings/assigns/When an Application or Utility is assigned to open file type, that assignment is stored here. You can manage assigns manually by deleting, copying or renaming the assign files. An Application could also programmatically create or edit assigns by operating on these files.
//os/temporary/This directory is used as a common file system scratch space. Applications may write temporary files here. Files stored in the root of the temporary directory are scratched automatically everytime C64 OS is booted. This is performed by the boot component //os/library/:temporary.o. Subdirectories of //os/temporary/ and their contents are not scratched during boot up.
//os/tk/Toolkit classes that are globally accessible are installed here. That includes the ones that are permanently memory resident and the ones that can be loaded and relocated at runtime. The memory resident classes are loaded to a fixed address and are assembled at the same time as the toolkit KERNAL module. These classes end with the .o extension. Runtime loaded, relocatable classes end with the .r extension.
Toolkit classes that are specific to an Application can be installed in the Application's bundle and loaded from there. If a Toolkit class can plausibly be uesd by more than one Application, it would be better if it were installed globally in //os/tk/ so that other Applications can make use of it too.
//os/utilities/Utilities are typically installed here. In order for a Utility to be launched from the Utilities menu, from App Launcher, or by the Opener or Utilities Utility, it must be installed here.
However, some Utilities that are highly specific to one Application can be launched by that Application from a utilities subdirectory in the Application's bundle. File Manager embeds the Places Utility, for example.
Chapter 2: Architectural Overview, Summary
This has been a brief, high level, overview of several architectural elements of C64 OS. The chapters which discuss creating an Application, a Utility, a Driver or a Library will take for granted that you are familiar with the concepts laid out in this overview.
For more detail and technical reference on how to actually make the calls discussed in this chapter, refer to, Chapter 4: Using the KERNAL, Chapter 5: Using Libraries and Chapter 6: Using the Toolkit.
Next Chapter: Development Environment
Table of Contents
This document is subject to revision updates.
Last modified: Apr 20, 2023