Recently, I've been pondering how to implement filesystem operations in the emulator that meshes well with the existing event-driven interface that Mantle exposes. While file access can be expressed using asynchronous interfaces, in most cases, you just can't do anything until file access has been completed. Therefore, it is strongly preferable to expose a synchronous file interface even if the rest of the system is asynchronous in nature.
This got me to thinking about the possibility of using the Tripos kernel as a kind of standard library for the Mantle/ROSE environment. In this way, we can continue to use the existing Mantle API under the hood, while ROSE and Tripos would sit on top of this "kernel" and continue to function as each were intended.
I'm sure I can adapt the Tripos kernel into a green-threads library. Existing systems like Cintpos already essentially function in this manner. The bigger question then becomes, if I can have the kernel, can I also have the DOS interface too?
I think I can. And, if I have my understanding correct, this is how I would pull it off.
First,
Mantle boots up in either the emulator or physical hardware, and
loads the default.prg image, just like it currently does now.
It calls the default EvProc with mcInitialize to bring up the image.
The initialization handler should then initialize the Tripos kernel,
in particular the creation of the idle task (priority 0).
Once this is up,
the idle task should be the only task to ecall ecNextEvent.
The initialization handler also brings up some Tripos "device drivers" as well. One of these is the timer device (id -1). This driver is written in such a way that it makes use of the Mantle timer services behind the scenes. An additional driver might provide access to the emulator-hosted local filesystem. Basically, all devices are just fronts for Mantle. Remember, we're running in user space, so no real device access exists.
NOTE. This is true for the Kestrel-2/EX emulator. For real hardware, however, it is conceivable that Mantle can grant selective access to hardware resources so that Tripos device drivers really do work as real device drivers. This is a future-me problem, though; I'll tackle that design possibility when the situation arises.
Next, especially for the emulator users, a file handler task should be brought up to handle (at least) the DE0: drive. This pseudo-drive will represent the local emulator filesystem access API if it is configured.
NOTE. Real hardware will likely not expose a DE0: drive, since there is no emulation layer. Instead, it'll probably expose an SD0: drive, reflecting the first SD-card slot, or perhaps an IDE0: or DH0: to represent an IDE hard drive.
Regardless of which filesystem is brought up for a particular configuration, the assignment SYS: will be mapped to it. (Users of AmigaDOS or legacy Tripos will already be familiar with how this works.)
Once all the pieces are put in place, then packets will be exchanged between server tasks and client tasks to perform filesystem I/O.
Tripos tasks all have unique priorities. In other words, no two tasks have the same priority. This makes Tripos a preemptive, prioritized multitasking kernel without time-slicing. This is one area of difference from AmigaOS, which is preemptive, prioritized multitasking with time-slicing.
Here is how I see all this stuff working together.
Applications are assigned priorities between, just to hand-wave a bit, 1000-2000, usually starting high and going lower. (If we start high and go lower as we add tasks to the system, that guarantees the CLI or GUI tasks used to launch programs remain at a higher priority than the apps they launch. So, if you hit CTRL-C to cancel a program, the CLI/GUI task has a chance to kill the child task cleanly.) The priority range between 1000-2000 allows for 999 running tasks, which would likely result in a serious burden on even the largest of contemporary CPUs.
When an application issues a filesystem request,
it will construct a packet (message) in memory and send it to, say,
L:emul-handler,
responsible for the DE0: drive.
Subsequently, the app won't have anything to do,
since the DOS will immediately call TaskWait() for a response.
So, the application task falls asleep,
resulting in the next highest priority task to run.
This will likely be the file handler (L:emul-handler),
so it receives the message and handles it.
In the course of handling the file request,
the handler may want to issue a request to the L:Devs/mantle/fileio driver.
This is also done using packets.
After QPkt() places the packet onto the driver's work queue,
it invokes the driver's StartIO() function.
This should pop the packet of the driver's work queue and
ultimately should result in a Mantle system call.
Nearly all Mantle APIs resolve synchronously; so,
StartIO can just reply to the message right away and return.
The handler task, when running TaskWait(),
will not block (since StartIO replied to the request message),
and so this whole exchange is effectively synchronous in nature.
Not only that, but this whole exchange happens within a single Mantle EvProc invokation.
When the handler task is done issuing commands to the driver, it will finally reply to the application's request packet. Since the app will likely have higher priority than the handler, the act of sending the message back will cause an immediate task switch, putting the app back in control.
This cycle will continue
for as long as there are unhandled messages
circulating through the system.
However, eventually, the app and all the handlers will run out of things to do.
When this happens,
the last call to TaskWait()
will ultimately result in the idle process taking control,
and its first order of business is to issue the ecNextEvent ecall.
This will put the entire user space to sleep until the next input event,
and at long last, return control back to Mantle!
When the next input event arrives, the idle task once again assumes control. This is where the enabling magic happens. Based on the event type, the idle task uses a back channel of some sort (e.g., direct call into driver code), the end result of which is to notify a (usually a handler) task that something has happened. The task will ALWAYS have higher priority than the idle task, and so takes over right away. Handlers typically will use this to resolve pending work queue items, which in turns bubbles up the priority chain until the end-user's desired application takes control. There, the whole cycle repeats.
I'm using C as a pseudo-language here, but it could apply to any language.
So,
an application might open the MOUSE: interface and issue an asynchronous read to it.
NOTE. Don't use the DOS library's Read() call, since that behaves in a synchronous manner.
That read won't be satisfied until the next mouse motion or button event.
When it resolves,
the app processes the result and issues another read when it's ready for more events.
It might also open a KBD: device and handle raw key code input in the same way.
Remember: this code is just a sketch of what a Tripos application looks like. It is not intended to represent the finished project.
#include <tripos/tripos.h>
#include <rose/mouse.h>
scb* kbd;
scb* mouse;
void
cleanup(void)
{
// If I remember correctly, calling Close() on a stream
// control block will also cancel any pending I/O for that stream.
// Thus, there is no need to send separate packets to cancel
// pending reads.
if (kbd) {
Close(kbd);
}
if (mouse) {
Close(mouse);
}
}
int
error(char* msg)
{
fprintf(stderr, "Error: %s\n", msg);
cleanup();
return 20; // Typical Tripos FAILAT value.
}
int
main(int argc, char** argv)
{
io_pkt* pkt;
// These are OK to be synchronous I/O calls,
// because we can't do anything until they're done.
kbd = Open("KBD:raw/up", ...);
if (!kbd) {
return error("Can't open keyboard handler.");
}
mouse = Open("MOUSE:motion/buttons", ...);
if (!mouse) {
return error("Can't open mouse handler.");
}
// Capture task IDs for handlers, so we can identify
// who is replying to what message. These processes
// are guaranteed to stay resident for as long as we
// have open file handles on them.
tcb* kbd_task = DevProc("KBD:");
tcb* mouse_task = DevProc("MOUSE:");
// Let's issue an asynchronous read to the KBD: device,
// which will reply when a person strikes any key.
// We don't care which key is pressed/released; thus we
// ignore the actual buffer holding the keycode.
io_pkt kbd_pkt;
char kbd_buffer[2];
kbd_pkt.link = -1;
kbd_pkt.sender = FindTask();
kbd_pkt.type = act_read;
kbd_pkt.arg1 = (uint64_t)kbd;
kbd_pkt.arg2 = (uint64_t)(kbd_buffer);
kbd_pkt.arg3 = 1;
QPkt(&kbd_pkt, kbd_task);
// Issue an asynchronous read for a mouse event.
// In this case, we DO care about the value read.
io_pkt mouse_pkt;
mouse_report_t mouse_report;
mouse_pkt.link = -1;
mouse_pkt.sender = FindTask();
mouse_pkt.type = act_read;
mouse_pkt.arg1 = (uint64_t)mouse;
mouse_pkt.arg2 = (uint64_t)&mouse_report;
mouse_pkt.arg3 = sizeof(mouse_report);
QPkt(&mouse_pkt, mouse_task);
// Our event queues are primed; let's start handling events!
for (bool done = false; !done; ) {
pkt = TaskWait();
if (pkt->sender == kbd_task) {
// any key press will stop the program.
done++
}
if (pkt->sender == mouse_task) {
switch (mouse_report.type) {
case MRT_MOTION: {
printf("You moved the mouse to (%d, %d)\n", mouse_report.x, mouse_report.y);
break;
}
case MRT_BUTTONS: {
int down = mouse_report.down;
int up = mouse_report.up;
if (down & 1) {
printf("You pressed the left button.\n");
}
if (down & 2) {
printf("You pressed the right button.\n");
}
if (up & 1) {
printf("You released the left button.\n");
}
if (up & 2) {
printf("You released the right button.\n");
}
break;
}
} // switch
// Re-issue the read request to get the next event
mouse_pkt.sender = FindTask();
mouse_pkt.link = -1;
QPkt(&mouse_pkt, mouse_task);
}
}
cleanup();
return 0;
}
In a multi-terminal environment,
it's not quite clear to me how keyboard input would be (de)multiplexed.
The only thing that might be made to work is to use per-task assignments,
similar to how Plan 9 uses per-process filesystem mounts.
That way,
each running task in a GUI might get its own MOUSE: and KBD:
devices, etc.
But, this will require some additional thought and exploration.