Give the console colour and a cursor

COLOUR COSTS A NIBBLE AND NO HARDWARE. A glyph is drawn in palette indices 0 and 1, paper
and ink, and a cell's attribute nibble adds sixteen to both - so sixteen banks is already
sixteen ink and paper pairs, and all that was missing was a register saying which one the
console draws in. That is port 0x06, read as well as written like the rest.

The palette a machine wakes up with is arranged so that HIGHLIGHTING IS ONE BIT: banks 0 to
7 are colours on black, banks 8 to 15 are the same colours as paper with black ink. So
attribute XOR 8 turns any pair inside out. That is a convention rather than a rule of the
machine - the device only ever adds the nibble and looks the answer up - but it is the
convention that makes a highlighted line and a cursor free.

Bank 0 is still grey on black, so nothing that was written before this has changed colour.

THE CURSOR IS THE SAME BIT AGAIN. It is drawn by turning its cell inside out rather than by
putting a block over it, so the character underneath stays readable, which matters to
somebody editing a line. The device draws it rather than the window, because on a machine
with a screen a cursor is a hardware feature - one drawn by the presenter would not be in a
picture the machine saved.

It blinks on the machine's own clock, half a second each way, so the phase is a pure
function of the cycle count and a screen saved at a given cycle is the same screen every
time. A blink on the host's clock would have made every saved picture a matter of luck.

Off unless asked for, with bit 2 of the control port. That is right for a machine - a
program painting its own screen does not want something blinking in the middle of it - and
CosmOS asks for one at boot. It also asks again when it takes the console back from a
program that has stopped, because a program handing key mode back the way it was told to
writes zero, which turns the cursor off. The shell owns the prompt, so the shell is what
makes sure there is something blinking at it.

Nine more checks in Tests/video.sh, to 41: that the attribute colours the ink and not the
paper, that XOR 8 turns both, that it reads back, that a cursor appears where the registers
put it and only when asked for, and that it goes dark again half a million cycles later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-08-29 08:13:28 -04:00
co-authored by Claude Opus 5
parent 978aec4809
commit d6feddd1b6
7 changed files with 235 additions and 21 deletions
+12 -2
View File
@@ -63,6 +63,12 @@ boot:
INIA 0x01 INIA 0x01
OUTA 0x31 OUTA 0x31
; And a cursor, so that a person can see where the next thing they type will go. A machine
; wakes up without one, which is right: a program painting its own screen does not want one
; blinking in the middle of it. A system that reads lines from a person does.
INIA 0x04
OUTA 0x02
SETD.0 Banner SETD.0 Banner
CALL printString CALL printString
CALL newLine CALL newLine
@@ -2349,10 +2355,14 @@ handleExit:
; mode, and not interrupting. A program that wanted either is expected to put it back ; mode, and not interrupting. A program that wanted either is expected to put it back
; itself, but one that stopped early, or forgot, would otherwise hand back a shell with ; itself, but one that stopped early, or forgot, would otherwise hand back a shell with
; no echo and no backspace, or one being interrupted about keys it is reading anyway. ; no echo and no backspace, or one being interrupted about keys it is reading anyway.
; Zero is both bits, so this undoes everything the control port can be asked for, and ; This undoes everything the control port can be asked for and puts the cursor back, and
; asking for what is already the case costs a byte out of a port and does nothing. That ; asking for what is already the case costs a byte out of a port and does nothing. That
; is the right price for not having to know. ; is the right price for not having to know.
RSTA ;
; THE CURSOR IS PUT BACK RATHER THAN LEFT, because a program that borrowed key mode and
; handed it back the way it was told to writes zero, which turns the cursor off. The shell
; owns the prompt, so the shell is what makes sure there is something blinking at it.
INIA 0x04
OUTA 0x02 OUTA 0x02
SETD.0 Finished SETD.0 Finished
+36 -5
View File
@@ -26,6 +26,20 @@
// nothing needs to look further ahead than the byte it is about to take. // nothing needs to look further ahead than the byte it is about to take.
static int consoleKeyMode = 0; static int consoleKeyMode = 0;
static int cursorColumn = 0;
static int cursorRow = 0;
// What every cell the console draws is given. Zero is the pair the machine wakes up in,
// grey on black.
static uint8_t consoleAttribute = 0;
static int consoleCursorShown = 0;
// The device draws the cursor, so it has to be told where the console put it. Called
// wherever the cursor moves, which is every one of the few places that move it.
static void consoleCursorMoved(void) {
videoSetCursor(cursorRow, cursorColumn, consoleCursorShown);
}
static int consoleEnded = 0; static int consoleEnded = 0;
static int consolePushback = -1; // A byte already taken from the host, or -1. static int consolePushback = -1; // A byte already taken from the host, or -1.
static int consoleInterrupts = 0; // Whether an arriving byte puts the line up. static int consoleInterrupts = 0; // Whether an arriving byte puts the line up.
@@ -176,6 +190,8 @@ static void consoleSetControl(uint8_t control) {
// means one write can ask for line mode and interrupts together, which is an ordinary // means one write can ask for line mode and interrupts together, which is an ordinary
// thing to want and would otherwise be undone in the same breath as it was asked for. // thing to want and would otherwise be undone in the same breath as it was asked for.
consoleSetMode((control & CONSOLE_MODE_KEY) != 0); consoleSetMode((control & CONSOLE_MODE_KEY) != 0);
consoleCursorShown = (control & CONSOLE_CONTROL_CURSOR) != 0;
consoleCursorMoved();
int wantInterrupts = (control & CONSOLE_CONTROL_INTERRUPT) != 0; int wantInterrupts = (control & CONSOLE_CONTROL_INTERRUPT) != 0;
if (!wantInterrupts) { if (!wantInterrupts) {
@@ -213,12 +229,12 @@ static void consoleShowWhatIsWritten(void) {
// hold Voyager to the same recorded results as SplitBit. It is also what makes --screen // 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. // 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) { void consoleHome(void) {
cursorColumn = 0; cursorColumn = 0;
cursorRow = 0; cursorRow = 0;
consoleAttribute = 0;
consoleCursorShown = 0;
consoleCursorMoved();
} }
static void consoleNewLine(void) { static void consoleNewLine(void) {
@@ -288,14 +304,17 @@ static void consoleDraw(uint8_t byte) {
switch (byte) { switch (byte) {
case '\n': case '\n':
consoleNewLine(); consoleNewLine();
consoleCursorMoved();
return; return;
case '\r': case '\r':
cursorColumn = 0; cursorColumn = 0;
consoleCursorMoved();
return; return;
case 0x08: // Backspace, which CosmOS sends when a line is being edited. case 0x08: // Backspace, which CosmOS sends when a line is being edited.
if (cursorColumn > 0) { if (cursorColumn > 0) {
cursorColumn--; cursorColumn--;
videoPutCell(cursorRow, cursorColumn, 0, 0); videoPutCell(cursorRow, cursorColumn, 0, consoleAttribute);
consoleCursorMoved();
} }
return; return;
default: default:
@@ -306,10 +325,12 @@ static void consoleDraw(uint8_t byte) {
if (byte < CONSOLE_FONT_FIRST) { if (byte < CONSOLE_FONT_FIRST) {
return; return;
} }
videoPutCell(cursorRow, cursorColumn, (uint8_t)(byte - CONSOLE_FONT_FIRST), 0); videoPutCell(cursorRow, cursorColumn, (uint8_t)(byte - CONSOLE_FONT_FIRST),
consoleAttribute);
if (++cursorColumn >= videoColumns()) { if (++cursorColumn >= videoColumns()) {
consoleNewLine(); consoleNewLine();
} }
consoleCursorMoved();
} }
// ---- Waiting for a key when there is no terminal to wait on ---- // ---- Waiting for a key when there is no terminal to wait on ----
@@ -538,6 +559,9 @@ static uint8_t consoleStatus(void) {
if (consoleInterrupts) { if (consoleInterrupts) {
status |= CONSOLE_STATUS_INTERRUPT; status |= CONSOLE_STATUS_INTERRUPT;
} }
if (consoleCursorShown) {
status |= CONSOLE_STATUS_CURSOR;
}
consoleFetch(); consoleFetch();
if (consoleEnded) { if (consoleEnded) {
// READY IS NOT SET HERE, although a read would answer immediately. The bit means // READY IS NOT SET HERE, although a read would answer immediately. The bit means
@@ -760,6 +784,9 @@ void setDiskLatency(unsigned long cycles) {
void deviceTick(unsigned long now) { void deviceTick(unsigned long now) {
deviceNow = now; deviceNow = now;
// The screen blinks its cursor on the machine's own clock rather than the host's, so the
// picture is the same at the same cycle count however fast anything ran.
videoTick(now);
if (diskPending && now >= diskReadyAt) { if (diskPending && now >= diskReadyAt) {
uint8_t command = diskPending; uint8_t command = diskPending;
diskPending = 0; diskPending = 0;
@@ -908,10 +935,12 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
// obvious place to be, and stopping the machine over one is a poor trade. // obvious place to be, and stopping the machine over one is a poor trade.
cursorRow = DataByte < videoRows() ? DataByte : videoRows() - 1; cursorRow = DataByte < videoRows() ? DataByte : videoRows() - 1;
cursorTold = 0; cursorTold = 0;
consoleCursorMoved();
break; break;
case CONSOLE_CURSOR_COLUMN: case CONSOLE_CURSOR_COLUMN:
cursorColumn = DataByte < videoColumns() ? DataByte : videoColumns() - 1; cursorColumn = DataByte < videoColumns() ? DataByte : videoColumns() - 1;
cursorTold = 0; cursorTold = 0;
consoleCursorMoved();
break; break;
case CONSOLE_COMMAND: case CONSOLE_COMMAND:
if (DataByte == CONSOLE_COMMAND_CLEAR) { if (DataByte == CONSOLE_COMMAND_CLEAR) {
@@ -921,6 +950,7 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
// Anything else does nothing. A command block reserved for later should be // Anything else does nothing. A command block reserved for later should be
// quiet rather than fatal, the same as the screen's spare registers. // quiet rather than fatal, the same as the screen's spare registers.
break; break;
case CONSOLE_ATTRIBUTE: consoleAttribute = DataByte; break;
case CONSOLE_STATUS: case CONSOLE_STATUS:
// Read only. A device saying how it is does not take instructions through the // 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. // same hole, so a write here is ignored rather than meaning something.
@@ -988,6 +1018,7 @@ uint8_t InputHandler(uint8_t Address) {
case CONSOLE_STATUS: return consoleStatus(); case CONSOLE_STATUS: return consoleStatus();
case CONSOLE_CURSOR_ROW: return (uint8_t)cursorRow; case CONSOLE_CURSOR_ROW: return (uint8_t)cursorRow;
case CONSOLE_CURSOR_COLUMN: return (uint8_t)cursorColumn; case CONSOLE_CURSOR_COLUMN: return (uint8_t)cursorColumn;
case CONSOLE_ATTRIBUTE: return consoleAttribute;
case CONSOLE_COMMAND: case CONSOLE_COMMAND:
// Write only. What it did is visible in the cursor and on the screen. // Write only. What it did is visible in the cursor and on the screen.
return 0; return 0;
+15 -1
View File
@@ -18,7 +18,7 @@
// does not change: writing sends a byte, reading takes one and waits for it. The other two // 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. // are additions, so a program written before they existed cannot notice them.
#define PORT_CONSOLE 0x00 #define PORT_CONSOLE 0x00
#define PORT_CONSOLE_TOP 0x05 #define PORT_CONSOLE_TOP 0x06
#define CONSOLE_DATA 0x00 #define CONSOLE_DATA 0x00
#define CONSOLE_STATUS 0x01 #define CONSOLE_STATUS 0x01
#define CONSOLE_CONTROL 0x02 #define CONSOLE_CONTROL 0x02
@@ -35,6 +35,13 @@
#define CONSOLE_COMMAND 0x05 #define CONSOLE_COMMAND 0x05
#define CONSOLE_COMMAND_CLEAR 0x01 #define CONSOLE_COMMAND_CLEAR 0x01
// ---- What colour to write in ----
//
// The attribute given to every cell the console draws from now on. Its low nibble picks one
// of sixteen ink and paper pairs, and the default palette is arranged so that XOR 8 turns
// any of them inside out - which is highlighting, and is also how the cursor is drawn.
#define CONSOLE_ATTRIBUTE 0x06
#define PORT_TEST 0x10 #define PORT_TEST 0x10
#define PORT_REFUSE 0x11 #define PORT_REFUSE 0x11
#define PORT_MEMORY 0x12 #define PORT_MEMORY 0x12
@@ -113,6 +120,10 @@
// That is not especially useful, but a control bit that quietly did nothing depending on // That is not especially useful, but a control bit that quietly did nothing depending on
// another control bit would be worse than a burst of interrupts somebody asked for. // another control bit would be worse than a burst of interrupts somebody asked for.
#define CONSOLE_CONTROL_INTERRUPT 0x02 #define CONSOLE_CONTROL_INTERRUPT 0x02
// Show a cursor where the next character will go. Off when the machine starts, because a
// machine draws what it is told to and a program painting its own screen does not want one
// blinking in the middle of it. A system that reads lines from a person turns it on.
#define CONSOLE_CONTROL_CURSOR 0x04
// Set when there is a byte to be had. NOT set at the end of input, although a read would // 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 // answer at once there: what it answers is 0xFF standing in for nothing, and calling that
@@ -129,6 +140,9 @@
// Whether the console is set to interrupt, for the same reason: everything a program can // Whether the console is set to interrupt, for the same reason: everything a program can
// ask the console to be, it can also ask the console what it currently is. // ask the console to be, it can also ask the console what it currently is.
#define CONSOLE_STATUS_INTERRUPT 0x08 #define CONSOLE_STATUS_INTERRUPT 0x08
// And whether a cursor is being shown, for the same reason as the rest: everything a program
// can ask the console to be, it can also ask the console what it currently is.
#define CONSOLE_STATUS_CURSOR 0x10
// Puts the terminal back the way it was found. Registered with atexit and called from a // Puts the terminal back the way it was found. Registered with atexit and called from a
// handler for every signal that can end this process and be caught, because a machine that // handler for every signal that can end this process and be caught, because a machine that
+63 -10
View File
@@ -33,20 +33,54 @@ static int rowsFor(uint8_t m) { return m == VIDEO_MODE_80x50 ? 50 : 25; }
int videoColumns(void) { return columnsFor(mode); } int videoColumns(void) { return columnsFor(mode); }
int videoRows(void) { return rowsFor(mode); } int videoRows(void) { return rowsFor(mode); }
// ---- The two colours a machine wakes up with ---- // ---- Sixteen schemes a machine wakes up with ----
// //
// Only two, and the rest of the palette left at zero. A program that wants colour sets it, // A glyph is drawn in palette indices 0 and 1, paper and ink, and a cell's attribute nibble
// and a machine that guessed sixteen entries on its behalf would be sixteen entries it had // adds sixteen to both. So bank n colours text with entries n*16 and n*16+1, and SIXTEEN
// to overwrite. What it must not do is wake up unable to show text at all. // BANKS IS SIXTEEN INK AND PAPER PAIRS - a text attribute system that costs one nibble and
// no hardware at all.
//
// The arrangement is a convention rather than a rule of the machine, and it is chosen so
// that HIGHLIGHTING IS ONE BIT. Banks 0 to 7 are colours on black; banks 8 to 15 are the
// same colours as paper with black ink. Attribute XOR 8 therefore turns any of them inside
// out, which is what a cursor and a selected line both want, and a program that disagrees
// writes its own palette over the top.
//
// Bank 0 is grey on black, which is what the machine has always woken up as.
// //
// BLACK IS BLACK AND GREY IS GREY. These were tinted towards green to begin with, on the // BLACK IS BLACK AND GREY IS GREY. These were tinted towards green to begin with, on the
// theory that a phosphor never was neutral, and on a real screen it read as a fault rather // theory that a phosphor never was neutral, and on a real screen it read as a fault rather
// than as character - a background that is nearly black looks like a background that failed // than as character - a background that is nearly black looks like a background that failed
// to be black. A default should be the unsurprising thing; anything with a point of view // to be black.
// about colour is 254 entries away and belongs to a program. static const uint8_t defaultInks[8][3] = {
static const uint8_t defaultInk[3] = { 0xD8, 0xD8, 0xD8 }; { 0xD8, 0xD8, 0xD8 }, // grey, which is what plain text has always been
{ 0xD0, 0x40, 0x38 }, // red
{ 0x50, 0xC0, 0x50 }, // green
{ 0xD8, 0xC0, 0x48 }, // yellow
{ 0x58, 0x80, 0xE0 }, // blue
{ 0xC8, 0x60, 0xC0 }, // magenta
{ 0x50, 0xC0, 0xC8 }, // cyan
{ 0xF0, 0xF0, 0xF0 }, // white
};
static const uint8_t defaultPaper[3] = { 0x00, 0x00, 0x00 }; static const uint8_t defaultPaper[3] = { 0x00, 0x00, 0x00 };
// Where the cursor is, whether it is wanted, and what the clock says - which is what makes
// it blink without anything having to remember when it last did.
static int cursorAtRow = 0;
static int cursorAtColumn = 0;
static int cursorVisible = 0;
static unsigned long videoNow = 0;
void videoSetCursor(int row, int column, int visible) {
cursorAtRow = row;
cursorAtColumn = column;
cursorVisible = visible;
}
void videoTick(unsigned long now) {
videoNow = now;
}
void videoLoadFont(void) { void videoLoadFont(void) {
// One bit a pixel becomes one byte a pixel: index 1 where the font has a dot and 0 // One bit a pixel becomes one byte a pixel: index 1 where the font has a dot and 0
// where it does not, which is what makes the two palette entries below mean ink and // where it does not, which is what makes the two palette entries below mean ink and
@@ -63,8 +97,14 @@ void videoLoadFont(void) {
} }
} }
uint8_t *palette = videoRAM + VIDEO_PALETTE_BASE; uint8_t *palette = videoRAM + VIDEO_PALETTE_BASE;
memcpy(palette + 0 * VIDEO_PALETTE_BYTES, defaultPaper, 3); for (int bank = 0; bank < 8; bank++) {
memcpy(palette + 1 * VIDEO_PALETTE_BYTES, defaultInk, 3); // Colour on black, and then the same colour as paper with black ink, sixteen banks
// apart so that one bit turns either into the other.
memcpy(palette + (bank * 16 + 0) * VIDEO_PALETTE_BYTES, defaultPaper, 3);
memcpy(palette + (bank * 16 + 1) * VIDEO_PALETTE_BYTES, defaultInks[bank], 3);
memcpy(palette + ((bank + 8) * 16 + 0) * VIDEO_PALETTE_BYTES, defaultInks[bank], 3);
memcpy(palette + ((bank + 8) * 16 + 1) * VIDEO_PALETTE_BYTES, defaultPaper, 3);
}
} }
void videoPutCell(int screenRow, int column, uint8_t tile, uint8_t attribute) { void videoPutCell(int screenRow, int column, uint8_t tile, uint8_t attribute) {
@@ -152,7 +192,20 @@ void videoRender(void) {
const uint8_t *cells = videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE; const uint8_t *cells = videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE;
for (int column = 0; column < columns; column++) { for (int column = 0; column < columns; column++) {
const uint8_t tile = cells[column * VIDEO_CELL_BYTES]; const uint8_t tile = cells[column * VIDEO_CELL_BYTES];
const uint8_t attribute = cells[column * VIDEO_CELL_BYTES + 1]; uint8_t attribute = cells[column * VIDEO_CELL_BYTES + 1];
// ---- The cursor, turned inside out ----
//
// Not a glyph of its own, because a block drawn over a cell hides what is in it
// and a person editing a line wants to see the character they are standing on.
// XOR 8 swaps a bank for its reverse, which is what the default palette is laid
// out to make possible.
//
// The phase comes from the machine's clock, so a screen saved at a given cycle
// count is the same screen every time.
if (cursorVisible && row == cursorAtRow && column == cursorAtColumn
&& ((videoNow / VIDEO_BLINK_CYCLES) & 1) == 0) {
attribute ^= 0x08;
}
// ---- The additive nibble ---- // ---- The additive nibble ----
// //
// The low nibble of the attribute is added to every palette index in the tile, // The low nibble of the attribute is added to every palette index in the tile,
+15
View File
@@ -96,6 +96,21 @@ void videoReset(void);
// of one-bit rows against 16 kilobytes of tiles. // of one-bit rows against 16 kilobytes of tiles.
void videoLoadFont(void); void videoLoadFont(void);
// ---- The cursor ----
//
// Drawn by the device rather than by whatever is presenting, because on a machine with a
// screen the cursor IS a hardware feature - a display controller blinks it from a counter,
// and one drawn by the window would not be in a picture the machine saved.
//
// It blinks on the machine's own clock, so the phase is a pure function of the cycle count
// and a screen saved at a given cycle is the same screen every time.
#define VIDEO_BLINK_CYCLES 500000
void videoSetCursor(int row, int column, int visible);
// The machine's clock, for anything that has to know time has passed.
void videoTick(unsigned long now);
int videoColumns(void); int videoColumns(void);
int videoRows(void); int videoRows(void);
+26 -3
View File
@@ -269,8 +269,8 @@ Port 0x00 is the oldest thing on this machine and it has not changed: writing se
| Port | Register | | Port | Register |
| --- | --- | | --- | --- |
| 0x00 | Data. Writing sends a byte out, reading takes one in and waits for it. | | 0x00 | Data. Writing sends a byte out, reading takes one in and waits for it. |
| 0x01 | Status. Bit 0 a byte is waiting, bit 1 input has ended, bit 2 the console is in key mode, bit 3 the console is set to interrupt. | | 0x01 | Status. Bit 0 a byte is waiting, bit 1 input has ended, bit 2 the console is in key mode, bit 3 the console is set to interrupt, bit 4 a cursor is being shown. |
| 0x02 | Control. Bit 0 asks for key mode, bit 1 asks the console to interrupt when a byte arrives. Writing 0x00 asks for neither, which is how the console starts. | | 0x02 | Control. Bit 0 asks for key mode, bit 1 asks the console to interrupt when a byte arrives, bit 2 asks for a cursor. Writing 0x00 asks for none of them, which is how the console starts. |
The control port's two bits are independent, and one write sets both. Everything the control port can ask for, the status port reports, so a program can put the console back the way it found it instead of assuming it knows. The control port's two bits are independent, and one write sets both. Everything the control port can ask for, the status port reports, so a program can put the console back the way it found it instead of assuming it knows.
@@ -577,7 +577,7 @@ And the rows that scrolled off are still in the map, which is where a terminal o
A console on a machine with a screen sends every byte to both, because a machine with a screen and a serial line is an ordinary machine and there is one console driving both. A console on a machine with a screen sends every byte to both, because a machine with a screen and a serial line is an ordinary machine and there is one console driving both.
At reset the font is expanded into tile memory and the palette is given two entries: 0 is paper and 1 is ink. That is all a machine needs to be able to say something before any program has run, and it is deliberately not more - a program that wants colour sets it, and sixteen guessed entries would be sixteen a program had to overwrite. At reset the font is expanded into tile memory and the palette is given sixteen ink and paper pairs. See Colour below.
The font is in ASCII order, so a byte becomes a glyph by subtracting 32. Bytes below that have no glyph and are not drawn; three of them do something instead. The font is in ASCII order, so a byte becomes a glyph by subtracting 32. Bytes below that have no glyph and are not drawn; three of them do something instead.
@@ -589,6 +589,24 @@ The font is in ASCII order, so a byte becomes a glyph by subtracting 32. Bytes b
Writing past the last column wraps to the next row, the same as a newline. Writing past the last column wraps to the next row, the same as a newline.
### Colour:
A glyph is drawn in palette indices 0 and 1 - paper and ink - and a cell's attribute nibble adds sixteen to both. **So sixteen banks is sixteen ink and paper pairs**, and a text attribute system costs one nibble and no hardware at all.
Which pair the console draws in is the Attribute register, 0x06. Everything written after it is drawn that way, until it changes.
The palette a machine wakes up with is arranged so that **highlighting is one bit**:
| Attribute | Paper | Ink |
| --- | --- | --- |
| 0 | Black | Grey |
| 1 to 7 | Black | Red, green, yellow, blue, magenta, cyan, white |
| 8 to 15 | The same seven and grey | Black |
So `attribute XOR 8` turns any pair inside out, which is what a highlighted line wants and how the cursor is drawn. Bank 0 is grey on black, which is what plain text has always been.
**That arrangement is a convention rather than a rule of the machine.** A program that wants different colours writes its own palette, and one that wants thirty-two of something rather than sixteen pairs can have that too - the device only ever adds the nibble and looks the answer up.
### Moving The Cursor: ### Moving The Cursor:
Three more registers, because that is how this machine talks to everything else. Three more registers, because that is how this machine talks to everything else.
@@ -598,6 +616,11 @@ Three more registers, because that is how this machine talks to everything else.
| 0x03 | Cursor row. Read and write. | | 0x03 | Cursor row. Read and write. |
| 0x04 | Cursor column. Read and write. | | 0x04 | Cursor column. Read and write. |
| 0x05 | Command. Write 1 to clear the screen. | | 0x05 | Command. Write 1 to clear the screen. |
| 0x06 | Attribute. Read and write. |
**A cursor is shown only when it is asked for**, with bit 2 of the Control port, and status bit 4 says whether one is being shown. Off is the right default for a machine: a program painting its own screen does not want something blinking in the middle of it, and a system that reads lines from a person turns it on.
It is drawn by turning its cell inside out rather than by putting a block over it, so the character underneath stays readable - which matters to somebody editing a line. And it blinks **on the machine's own clock**, half a second on and half a second off, so the picture at a given cycle count is the same picture every time and a saved screen is not a matter of luck.
Both counted from zero, and both **readable**, which is the thing worth having: a routine that wants to put the cursor back where it found it asks where that was. Both counted from zero, and both **readable**, which is the thing worth having: a routine that wants to put the cursor back where it found it asks where that was.
+68
View File
@@ -93,6 +93,13 @@ emit() {
printf ' INIA 0d%d\n OUTA 0x00\n' "$1" printf ' INIA 0d%d\n OUTA 0x00\n' "$1"
} }
# About 262,000 cycles of nothing. A DECA is one byte and a BNA is three, so four cycles a
# turn, 256 times 256. The label suffix is so that two of these can sit in one program.
spin() {
printf ' RSTB\nspinOuter%s:\n RSTA\nspinInner%s:\n DECA\n BNA spinInner%s\n DECB\n BNB spinOuter%s\n' \
"$1" "$1" "$1" "$1"
}
# A string to the console, which is all a program has ever had to do to put text on a # A string to the console, which is all a program has ever had to do to put text on a
# SplitBit. That it now appears on a screen is the whole of this rung. # SplitBit. That it now appears on a screen is the whole of this rung.
say() { say() {
@@ -410,6 +417,67 @@ GOT="$(said cursorclamp)"
&& result ok "a cursor past the edge is clamped" "column 39, the last one" \ && result ok "a cursor past the edge is clamped" "column 39, the last one" \
|| result no "a cursor past the edge is clamped" "got $GOT" || result no "a cursor past the edge is clamped" "got $GOT"
# ---- Colour, which costs a nibble and no hardware ----
#
# A glyph is drawn in palette indices 0 and 1, paper and ink, and a cell's attribute nibble
# adds sixteen to both. Sixteen banks is therefore sixteen ink and paper pairs, and the
# default palette is arranged so that XOR 8 turns any of them inside out.
coloured() { [ "$(pixel "$1" "$2" "$3")" = "$4" ]; }
{ printf '#Program\nstart:\n'; port 0x06 0x01; say "A"; epilogue; } | run inkred || exit 1
coloured inkred 2 1 "208,64,56" \
&& result ok "the attribute register colours the ink" "bank 1 is red on black" \
|| result no "the attribute register colours the ink" "got $(pixel inkred 2 1)"
coloured inkred 0 0 "0,0,0" \
&& result ok "and leaves the paper alone" "still black behind it" \
|| result no "and leaves the paper alone" "got $(pixel inkred 0 0)"
# The same colour with one bit more, which is the whole of highlighting.
{ printf '#Program\nstart:\n'; port 0x06 0x09; say "A"; epilogue; } | run highlight || exit 1
coloured highlight 0 0 "208,64,56" \
&& result ok "XOR 8 turns a pair inside out" "red paper now" \
|| result no "XOR 8 turns a pair inside out" "got $(pixel highlight 0 0)"
coloured highlight 2 1 "0,0,0" \
&& result ok "and the ink with it" "black letters on it" \
|| result no "and the ink with it" "got $(pixel highlight 2 1)"
# Readable, like every other console register.
{ printf '#Program\nstart:\n'; port 0x06 0x05; show 0x06; epilogue; } | run attrread || exit 1
[ "$(said attrread)" = "5" ] \
&& result ok "and the attribute reads back" "bank 5" \
|| result no "and the attribute reads back" "got $(said attrread)"
# ---- The cursor ----
#
# Drawn by the device, turned inside out rather than drawn over, so that a person editing a
# line can still see the character they are standing on. Off unless asked for: a program
# painting its own screen does not want one blinking in the middle of it.
{ printf '#Program\nstart:\n'; port 0x02 0x04; epilogue; } | run cursoron || exit 1
coloured cursoron 0 0 "216,216,216" \
&& result ok "a cursor appears where the console is" "an empty cell, inside out" \
|| result no "a cursor appears where the console is" "got $(pixel cursoron 0 0)"
{ printf '#Program\nstart:\n'; epilogue; } | run cursoroff || exit 1
coloured cursoroff 0 0 "0,0,0" \
&& result ok "and there is none unless asked for" "the machine draws what it is told" \
|| result no "and there is none unless asked for" "got $(pixel cursoroff 0 0)"
{ printf '#Program\nstart:\n'; port 0x02 0x04; port 0x03 0x03; port 0x04 0x07; epilogue
} | run cursorwhere || exit 1
coloured cursorwhere 56 24 "216,216,216" \
&& result ok "and it follows the cursor registers" "row 3, column 7" \
|| result no "and it follows the cursor registers" "got $(pixel cursorwhere 56 24)"
# ---- And it blinks on the machine's own clock ----
#
# Which is what makes it deterministic: the phase is a pure function of the cycle count, so
# a screen saved at a given cycle is the same screen every time. Half a million cycles in it
# is dark, and this burns about 524,000 - a DECA and a BNA are four cycles a turn.
{ printf '#Program\nstart:\n'; port 0x02 0x04; spin a; spin b; epilogue; } | run cursorblink || exit 1
coloured cursorblink 0 0 "0,0,0" \
&& result ok "the cursor blinks off again" "half a second later, dark" \
|| result no "the cursor blinks off again" "got $(pixel cursorblink 0 0)"
# And what scrolled off the top is still in the map, which is scrollback nothing had to keep. # And what scrolled off the top is still in the map, which is scrollback nothing had to keep.
{ printf '#Program\nstart:\n' { printf '#Program\nstart:\n'
say "A" say "A"