CosmOS pre-alpha and launchable application versions of old programs.

This commit is contained in:
Anachronaut
2026-08-17 15:31:49 -04:00
parent eff6902bcf
commit 91c9d49d1b
66 changed files with 5612 additions and 160 deletions
+17
View File
@@ -592,6 +592,23 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
*target = cpu->StackPointer;
}
break;
case 0x4D: {
// MVDS - Copy the selected Data Pointer into the Stack Pointer.
//
// This one is dangerous and is meant to be used rarely. Moving the Stack
// under a running program abandons every return address on it, so a RET
// after this goes wherever the new Stack happens to say.
//
// It exists because a system that runs other programs has no other way to
// get its Stack back. A program that gives up part way through leaves
// whatever it pushed behind, and the interrupt frame that carried the
// request to stop is on there too. Without this the Stack only ever grows
// downward, one abandoned program at a time, and a shell cannot outlive
// many of them.
uint16_t *source = selectDataPointer(cpu);
cpu->StackPointer = *source;
}
break;
//
// Dx - Output Operations:
//
+4 -1
View File
@@ -111,7 +111,10 @@ int main (int argc, char *argv[]) {
if (options.debug) {
// Wait before advancing, not after, so that a keypress is what moves the
// machine on rather than something that happens once it already has.
getchar();
// Through the console rather than getchar, so that everything reading standard
// input reads it the same way and the console's pushback stays the only place
// a byte can be sitting.
consoleReadByte();
}
int cycles;
if (options.debug) {
+179 -3
View File
@@ -7,7 +7,167 @@
#include "../Assembler/assembly.h" // For the fault vector numbers.
#include "controller.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <termios.h>
#include <unistd.h>
#include <poll.h>
// ---- The console ----
//
// The console owns its own reading rather than going through getchar. stdio keeps a
// buffer, and the status port asks the operating system what is waiting; those two
// disagree the moment stdio has read ahead, and the status port would then swear nothing
// was there while a read returned instantly. One byte of pushback here is enough, because
// nothing needs to look further ahead than the byte it is about to take.
static int consoleKeyMode = 0;
static int consoleEnded = 0;
static int consolePushback = -1; // A byte already taken from the host, or -1.
static struct termios consoleSavedTerminal;
static int consoleTerminalSaved = 0;
void consoleRestore(void) {
if (consoleTerminalSaved) {
tcsetattr(STDIN_FILENO, TCSANOW, &consoleSavedTerminal);
consoleTerminalSaved = 0;
}
consoleKeyMode = 0;
}
// Restores the terminal and then dies the way it would have died anyway, so that the
// shell sees the signal it was expecting rather than a machine that exited quietly.
static void consoleSignalHandler(int signalNumber) {
consoleRestore();
signal(signalNumber, SIG_DFL);
raise(signalNumber);
}
static void consoleSetMode(uint8_t mode) {
int wantKeys = (mode & CONSOLE_MODE_KEY) != 0;
if (wantKeys == consoleKeyMode) {
return;
}
if (!wantKeys) {
consoleRestore();
return;
}
// Nothing to configure when input is not a terminal, but the mode is still recorded:
// a program asking the status port what mode it is in should be told what it asked
// for, whether or not there was a terminal to carry it out on.
consoleKeyMode = 1;
if (!isatty(STDIN_FILENO)) {
return;
}
if (!consoleTerminalSaved) {
if (tcgetattr(STDIN_FILENO, &consoleSavedTerminal) != 0) {
return;
}
consoleTerminalSaved = 1;
// Registered on the first use rather than at startup, so a run that never asks
// for key mode installs nothing at all.
atexit(consoleRestore);
signal(SIGINT, consoleSignalHandler);
signal(SIGTERM, consoleSignalHandler);
}
struct termios raw = consoleSavedTerminal;
raw.c_lflag &= (tcflag_t)~(ICANON | ECHO);
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSANOW, &raw);
}
// Everything already written is put where it can be seen before the machine asks the host
// anything. Standard output is line buffered on a terminal, so a prompt with no newline
// after it - "> " is exactly that, and exactly why this matters - would sit in the buffer
// while the machine waited for an answer to a question nobody had been shown.
//
// getchar used to do this by accident, because reading through stdio flushes the line
// buffered streams first. Reading with read() does not, so what was a side effect of the
// old way is done deliberately here.
static void consoleShowWhatIsWritten(void) {
fflush(stdout);
}
uint8_t consoleReadByte(void) {
if (consolePushback >= 0) {
uint8_t byte = (uint8_t)consolePushback;
consolePushback = -1;
return byte;
}
consoleShowWhatIsWritten();
unsigned char byte;
for (;;) {
ssize_t got = read(STDIN_FILENO, &byte, 1);
if (got == 1) {
return byte;
}
if (got == 0) {
// End of input. Still 0xFF, which is what getchar's EOF became when this was
// the only answer available, so nothing written against the old behaviour
// changes. The ENDED bit is the new way to know it was not a real byte.
consoleEnded = 1;
return 0xFF;
}
if (errno != EINTR) {
consoleEnded = 1;
return 0xFF;
}
// Interrupted before anything arrived, so ask again.
}
}
// Asking the host whether anything is waiting, and TAKING IT IF THERE IS. The byte goes
// into the pushback and the next read of the data port hands it over, so nothing is lost
// and no program can tell that it was fetched early.
//
// Fetching it early is what makes the answer worth having. The operating system will say a
// pipe is readable when what is waiting is the end of it, so asking without reading can
// only report that SOMETHING is there. Reading settles which: a byte, or the end. Without
// this, ENDED could not go up until a program had already read the 0xFF that stands for
// it, and every program would have to swallow one imaginary byte to find out there were
// none.
static void consoleFetch(void) {
if (consolePushback >= 0 || consoleEnded) {
return;
}
// Flushed here too. A program that draws something and then polls rather than reads is
// just as entitled to have the drawing appear, and it never reaches the read that
// would otherwise have flushed for it.
consoleShowWhatIsWritten();
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
if (poll(&waiting, 1, 0) <= 0 || (waiting.revents & (POLLIN | POLLHUP)) == 0) {
return;
}
unsigned char byte;
ssize_t got = read(STDIN_FILENO, &byte, 1);
if (got == 1) {
consolePushback = byte;
} else if (got == 0) {
consoleEnded = 1;
}
// A read that failed for any other reason is left alone: the next attempt asks again,
// and an interrupted poll is not news.
}
static uint8_t consoleStatus(void) {
uint8_t status = consoleKeyMode ? CONSOLE_STATUS_KEYMODE : 0;
consoleFetch();
if (consoleEnded) {
// READY IS NOT SET HERE, although a read would answer immediately. The bit means
// "there is a byte to be had", and at the end of input there is not; what a read
// returns then is 0xFF standing in for nothing. A program looping while READY
// stops on its own at the end, which is the behaviour worth having, and one that
// wants to know why asks ENDED.
return status | CONSOLE_STATUS_ENDED;
}
if (consolePushback >= 0) {
status |= CONSOLE_STATUS_READY;
}
return status;
}
// One bit per port, so a device can ask for attention without anything having to poll
// it. Eight ports to the byte, low bit first.
@@ -233,6 +393,11 @@ static const DeviceRecord *deviceOnPort(uint8_t port) {
if (port >= CONTROLLER_PORT_BASE && port <= CONTROLLER_PORT_TOP) {
return &controllerRecord;
}
if (port > PORT_CONSOLE && port <= PORT_CONSOLE_TOP) {
// The status and control ports are the same device as the data port, which is the
// one in the table and the one that would raise a line if the console ever did.
return deviceOnPort(PORT_CONSOLE);
}
if (port > PORT_DISK && port <= PORT_DISK_TOP) {
// The base port is in the table proper, since that is the one that owns the
// memory and raises the line. The rest of the block reports the same device.
@@ -269,12 +434,17 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
}
// This function sends the DataByte to the appropriate place based on the Port Address.
switch(Address) {
case PORT_CONSOLE:
case CONSOLE_DATA:
// If data is sent here, it should be written to STDOUT.
// For now, I'll implement this so it simply writes each byte out as it comes in.
// Later, I'll want to use a buffer for this for performance, probably.
putchar(DataByte);
break;
case CONSOLE_CONTROL: consoleSetMode(DataByte); break;
case CONSOLE_STATUS:
// Read only. A device saying how it is does not take instructions through the
// same hole, so a write here is ignored rather than meaning something.
break;
case DISK_BLOCK_HIGH: diskBlock = (uint16_t)(DataByte << 8) | (diskBlock & 0x00FF); break;
case DISK_BLOCK_LOW: diskBlock = (diskBlock & 0xFF00) | DataByte; break;
case DISK_COMMAND: diskCommand(DataByte); break;
@@ -319,9 +489,15 @@ uint8_t InputHandler(uint8_t Address) {
return controllerRead(Address);
}
switch(Address) {
case PORT_CONSOLE:
case CONSOLE_DATA:
// If data is sent here, it should be read from STDIN.
return getchar();
return consoleReadByte();
break;
case CONSOLE_STATUS: return consoleStatus();
case CONSOLE_CONTROL:
// Write only. Reading it gives zero rather than the mode, because the mode is
// a bit in the status port and one fact wants one place to live.
return 0;
break;
case DISK_BLOCK_HIGH: return (uint8_t)(diskBlock >> 8);
case DISK_BLOCK_LOW: return (uint8_t)(diskBlock & 0xFF);
+57 -1
View File
@@ -14,7 +14,15 @@
// Which port a device answers on is a property of the machine rather than of any
// program, so the numbers live here and everything else refers to them by name.
#define PORT_CONSOLE 0x00
// The console answers on three ports. The data port is the machine's oldest promise and
// does not change: writing sends a byte, reading takes one and waits for it. The other two
// are additions, so a program written before they existed cannot notice them.
#define PORT_CONSOLE 0x00
#define PORT_CONSOLE_TOP 0x02
#define CONSOLE_DATA 0x00
#define CONSOLE_STATUS 0x01
#define CONSOLE_CONTROL 0x02
#define PORT_TEST 0x10
#define PORT_REFUSE 0x11
#define PORT_MEMORY 0x12
@@ -30,6 +38,54 @@
#define DISK_STATUS 0x23
#define PORT_REGISTRY 0xFF
// ---- The console ----
//
// Two modes, chosen by the program through the control port. The console starts in LINE
// mode, which is what the machine has always done: the terminal holds what is typed until
// Return, and does the echoing and the backspacing on the way. Reading the data port waits
// for a whole line to be finished somewhere else and then hands it over a byte at a time.
//
// KEY mode turns that off. Keys arrive as they are pressed, and nothing echoes them, so a
// program that wants them seen has to send them back out itself. That is not a choice this
// machine is making; it is what asking the terminal to stop holding a line means, and the
// editing goes away with it. A program that wants keys is expected to want that.
//
// READING THE DATA PORT WAITS IN BOTH MODES. The status port is how a program declines to
// wait, and keeping that in one place means the data port means one thing everywhere. A
// read that sometimes blocked and sometimes did not, depending on state set somewhere
// else, is the kind of thing that works until it does not.
//
// KEY MODE ONLY REACHES THE TERMINAL when there is one. With input coming from a pipe
// there is nothing to put into another mode, and the status port answers by asking the
// operating system whether anything is waiting, which is true of a pipe with bytes in it.
#define CONSOLE_MODE_LINE 0x00
#define CONSOLE_MODE_KEY 0x01
// Set when there is a byte to be had. NOT set at the end of input, although a read would
// answer at once there: what it answers is 0xFF standing in for nothing, and calling that
// ready would make a loop that reads while READY spin on imaginary bytes forever. A loop
// like that now stops when the input does, which is what anybody writing one intends.
#define CONSOLE_STATUS_READY 0x01
// Set once input has run out for good. The data port still answers 0xFF, which is what it
// always did and what every program written before this expects, but 0xFF is also an
// ordinary byte and this bit is the only thing that can tell the difference.
#define CONSOLE_STATUS_ENDED 0x02
// Which mode the console is in, so that a program can put it back the way it found it
// rather than assuming it knows.
#define CONSOLE_STATUS_KEYMODE 0x04
// Puts the terminal back the way it was found. Registered with atexit and called from the
// signal handlers, because a machine that stops in key mode and does not undo it leaves
// the shell that started it unusable, which is a far worse failure than anything the
// program was doing.
void consoleRestore(void);
// One byte from the console, waiting if it has to. Everything that reads standard input
// goes through here: the emulator owns one byte of pushback, and stdio holding a buffer
// of its own behind that would make the status port lie about what is waiting.
uint8_t consoleReadByte(void);
// ---- Device classes ----
//
// What kind of thing is plugged into a port. Class 0 is not a device: reading an