Put CosmOS on the screen without changing a line of it
The console is now a display controller as well as a port: it owns a font, keeps a cursor, handles newline, carriage return, backspace and wrapping, and scrolls. That is an ordinary kind of chip - it is what a video terminal's character generator did - and it is the reason this rung needed no changes to CosmOS at all. CosmOS already writes bytes to port 0x00. It writes to BOTH the screen and standard output, which is deliberate. A machine with a screen and a serial line is an ordinary machine, the emulator's standard output is that serial line, and one console drives both. It is also what keeps all 165 recorded results passing under Voyager, and what makes --screen work on the plain SplitBit: there is one console and it drives everything it has. Scrolling moves the video device's origin and no memory. The row arriving at the bottom is cleared because the map is a ring and it holds what was there 128 rows ago; the rows going off the top are not, and that is a hundred rows of scrollback nothing had to keep. The test reads the register back rather than looking at the screen, because a console blitting rows instead would look identical and cost twelve percent of a frame for every line printed. The font is vendored from Hatchet-GPU with a note saying where it came from, since that repository is not part of this one. 135 glyphs in ASCII order, which is the thing that makes it worth keeping - PETSCII's whole inconvenience was that its order was not ASCII's, so a machine using it needed a translation table in front of every string. Here the machine subtracts 32. It is stored one bit a pixel and expanded into tile memory at reset: 1,088 bytes against 16 kilobytes. Voyager gets a keyboard. A window has no standard input, and a machine blocking on it inside a frame would stop drawing and stop answering, so a front end with a window installs a hook that the console calls while it has nothing: it keeps the window alive and hands back a key. The hook has to tell "nobody has typed yet", which happens sixty times a second, apart from "the window has gone", which is the end of input - one value for both would have made the first keystroke look like a closed machine. In line mode the console echoes what it is given, because there is no terminal behind a window to do it and that was always the terminal's job. Tests/video.sh grew from 14 checks to 26, half of them about the console rather than the device: those programs ask the video device for nothing and write bytes to port 0x00 like every SplitBit program always has. Verified by breaking two things - removing the scroll failed exactly the two checks about scrolling, and removing the cursor advance failed exactly the three that depend on it. Two video checks had quietly depended on palette entry 0 being black, which stopped being true the moment a machine woke up able to show text. They now set what they are about to look at, and a new check pins the waking state itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
83623a3df3
commit
773b0f8add
+101
-3
@@ -7,6 +7,7 @@
|
||||
#include "../Assembler/assembly.h" // For the fault vector numbers.
|
||||
#include "controller.h"
|
||||
#include "video.h"
|
||||
#include "font.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -199,6 +200,78 @@ static void consoleShowWhatIsWritten(void) {
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
// ---- The console draws as well as it speaks ----
|
||||
//
|
||||
// THE VOYAGER'S CONSOLE IS A DISPLAY CONTROLLER: it takes a byte stream and puts glyphs on
|
||||
// a screen, keeps a cursor, and scrolls. That is an ordinary kind of chip - it is what a
|
||||
// video terminal's character generator did - and it is why CosmOS needs no changes at all
|
||||
// to run in a window. It already writes bytes to the console.
|
||||
//
|
||||
// It also keeps writing to standard output, and that is deliberate rather than an
|
||||
// oversight. A machine with a screen AND a serial line is completely ordinary, the emulator's
|
||||
// standard output is that serial line, and having both is what lets the whole test suite
|
||||
// hold Voyager to the same recorded results as SplitBit. It is also what makes --screen
|
||||
// work on the plain machine: there is one console, and it drives everything it has.
|
||||
|
||||
static int cursorColumn = 0;
|
||||
static int cursorRow = 0;
|
||||
|
||||
void consoleHome(void) {
|
||||
cursorColumn = 0;
|
||||
cursorRow = 0;
|
||||
}
|
||||
|
||||
static void consoleNewLine(void) {
|
||||
cursorColumn = 0;
|
||||
if (cursorRow + 1 < videoRows()) {
|
||||
cursorRow++;
|
||||
} else {
|
||||
// At the bottom, the screen moves under the cursor rather than the cursor moving
|
||||
// off the screen. One port write in the device, and no memory moves at all.
|
||||
videoScrollUp();
|
||||
}
|
||||
}
|
||||
|
||||
static void consoleDraw(uint8_t byte) {
|
||||
switch (byte) {
|
||||
case '\n':
|
||||
consoleNewLine();
|
||||
return;
|
||||
case '\r':
|
||||
cursorColumn = 0;
|
||||
return;
|
||||
case 0x08: // Backspace, which CosmOS sends when a line is being edited.
|
||||
if (cursorColumn > 0) {
|
||||
cursorColumn--;
|
||||
videoPutCell(cursorRow, cursorColumn, 0, 0);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Anything below the font's first character has no glyph and no agreed meaning here.
|
||||
// Drawing a box for it would put marks on the screen that nothing asked for.
|
||||
if (byte < CONSOLE_FONT_FIRST) {
|
||||
return;
|
||||
}
|
||||
videoPutCell(cursorRow, cursorColumn, (uint8_t)(byte - CONSOLE_FONT_FIRST), 0);
|
||||
if (++cursorColumn >= videoColumns()) {
|
||||
consoleNewLine();
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Waiting for a key when there is no terminal to wait on ----
|
||||
//
|
||||
// A window has no standard input, and a machine that blocked on it inside a frame would
|
||||
// stop drawing and stop answering. So a front end with a window installs a hook: called
|
||||
// while the console has nothing, it gets to keep the window alive and hands back a byte
|
||||
// when one is typed, or -1 to say the window has gone.
|
||||
static int (*inputHook)(void) = NULL;
|
||||
|
||||
void consoleSetInputHook(int (*hook)(void)) {
|
||||
inputHook = hook;
|
||||
}
|
||||
|
||||
uint8_t consoleReadByte(void) {
|
||||
// Taking the byte answers whatever the console was asking about, so the line comes
|
||||
// down here as well as when the CPU acknowledges it. Otherwise a program that reads
|
||||
@@ -211,6 +284,31 @@ uint8_t consoleReadByte(void) {
|
||||
return byte;
|
||||
}
|
||||
consoleShowWhatIsWritten();
|
||||
if (inputHook != NULL) {
|
||||
for (;;) {
|
||||
int got = inputHook();
|
||||
if (got >= 0) {
|
||||
// ---- The screen is the terminal now ----
|
||||
//
|
||||
// In line mode a terminal echoes what is typed and rubs out a backspace,
|
||||
// and CosmOS has always relied on that. There is no terminal behind a
|
||||
// window, so the display controller does it - which is exactly the job a
|
||||
// video terminal's character generator had. In key mode nothing echoes,
|
||||
// because a program in key mode is drawing its own screen.
|
||||
if (!consoleKeyMode) {
|
||||
consoleDraw((uint8_t)got);
|
||||
}
|
||||
return (uint8_t)got;
|
||||
}
|
||||
if (got == CONSOLE_GONE) {
|
||||
// The window has closed, which is this machine's end of input the way a
|
||||
// closed pipe is the other one's.
|
||||
consoleEnded = 1;
|
||||
return 0xFF;
|
||||
}
|
||||
// Nothing typed yet. The hook kept the window alive; ask it again.
|
||||
}
|
||||
}
|
||||
unsigned char byte;
|
||||
for (;;) {
|
||||
ssize_t got = read(STDIN_FILENO, &byte, 1);
|
||||
@@ -658,9 +756,9 @@ 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 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.
|
||||
// Both, always. The screen because this machine has one, and standard output
|
||||
// because the serial line is how everything that is not a person reads it.
|
||||
consoleDraw(DataByte);
|
||||
putchar(DataByte);
|
||||
break;
|
||||
case CONSOLE_CONTROL: consoleSetControl(DataByte); break;
|
||||
|
||||
Reference in New Issue
Block a user