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:
co-authored by
Claude Opus 5
parent
978aec4809
commit
d6feddd1b6
+63
-10
@@ -33,20 +33,54 @@ static int rowsFor(uint8_t m) { return m == VIDEO_MODE_80x50 ? 50 : 25; }
|
||||
int videoColumns(void) { return columnsFor(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,
|
||||
// and a machine that guessed sixteen entries on its behalf would be sixteen entries it had
|
||||
// to overwrite. What it must not do is wake up unable to show text at all.
|
||||
// A glyph is drawn in palette indices 0 and 1, paper and ink, and a cell's attribute nibble
|
||||
// adds sixteen to both. So bank n colours text with entries n*16 and n*16+1, and SIXTEEN
|
||||
// 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
|
||||
// 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
|
||||
// to be black. A default should be the unsurprising thing; anything with a point of view
|
||||
// about colour is 254 entries away and belongs to a program.
|
||||
static const uint8_t defaultInk[3] = { 0xD8, 0xD8, 0xD8 };
|
||||
// to be black.
|
||||
static const uint8_t defaultInks[8][3] = {
|
||||
{ 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 };
|
||||
|
||||
// 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) {
|
||||
// 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
|
||||
@@ -63,8 +97,14 @@ void videoLoadFont(void) {
|
||||
}
|
||||
}
|
||||
uint8_t *palette = videoRAM + VIDEO_PALETTE_BASE;
|
||||
memcpy(palette + 0 * VIDEO_PALETTE_BYTES, defaultPaper, 3);
|
||||
memcpy(palette + 1 * VIDEO_PALETTE_BYTES, defaultInk, 3);
|
||||
for (int bank = 0; bank < 8; bank++) {
|
||||
// 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) {
|
||||
@@ -152,7 +192,20 @@ void videoRender(void) {
|
||||
const uint8_t *cells = videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE;
|
||||
for (int column = 0; column < columns; column++) {
|
||||
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 low nibble of the attribute is added to every palette index in the tile,
|
||||
|
||||
Reference in New Issue
Block a user