diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index 7766708..d851f28 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -63,6 +63,12 @@ boot: INIA 0x01 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 CALL printString CALL newLine @@ -2349,10 +2355,14 @@ handleExit: ; 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 ; 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 ; 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 SETD.0 Finished diff --git a/Source/Emulator/io.c b/Source/Emulator/io.c index 3520b12..1520204 100644 --- a/Source/Emulator/io.c +++ b/Source/Emulator/io.c @@ -26,6 +26,20 @@ // nothing needs to look further ahead than the byte it is about to take. 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 consolePushback = -1; // A byte already taken from the host, or -1. 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 // thing to want and would otherwise be undone in the same breath as it was asked for. consoleSetMode((control & CONSOLE_MODE_KEY) != 0); + consoleCursorShown = (control & CONSOLE_CONTROL_CURSOR) != 0; + consoleCursorMoved(); int wantInterrupts = (control & CONSOLE_CONTROL_INTERRUPT) != 0; 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 // 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; + consoleAttribute = 0; + consoleCursorShown = 0; + consoleCursorMoved(); } static void consoleNewLine(void) { @@ -288,14 +304,17 @@ static void consoleDraw(uint8_t byte) { switch (byte) { case '\n': consoleNewLine(); + consoleCursorMoved(); return; case '\r': cursorColumn = 0; + consoleCursorMoved(); return; case 0x08: // Backspace, which CosmOS sends when a line is being edited. if (cursorColumn > 0) { cursorColumn--; - videoPutCell(cursorRow, cursorColumn, 0, 0); + videoPutCell(cursorRow, cursorColumn, 0, consoleAttribute); + consoleCursorMoved(); } return; default: @@ -306,10 +325,12 @@ static void consoleDraw(uint8_t byte) { if (byte < CONSOLE_FONT_FIRST) { return; } - videoPutCell(cursorRow, cursorColumn, (uint8_t)(byte - CONSOLE_FONT_FIRST), 0); + videoPutCell(cursorRow, cursorColumn, (uint8_t)(byte - CONSOLE_FONT_FIRST), + consoleAttribute); if (++cursorColumn >= videoColumns()) { consoleNewLine(); } + consoleCursorMoved(); } // ---- Waiting for a key when there is no terminal to wait on ---- @@ -538,6 +559,9 @@ static uint8_t consoleStatus(void) { if (consoleInterrupts) { status |= CONSOLE_STATUS_INTERRUPT; } + if (consoleCursorShown) { + status |= CONSOLE_STATUS_CURSOR; + } consoleFetch(); if (consoleEnded) { // 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) { 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) { uint8_t command = diskPending; 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. cursorRow = DataByte < videoRows() ? DataByte : videoRows() - 1; cursorTold = 0; + consoleCursorMoved(); break; case CONSOLE_CURSOR_COLUMN: cursorColumn = DataByte < videoColumns() ? DataByte : videoColumns() - 1; cursorTold = 0; + consoleCursorMoved(); break; case CONSOLE_COMMAND: 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 // quiet rather than fatal, the same as the screen's spare registers. break; + case CONSOLE_ATTRIBUTE: consoleAttribute = 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. @@ -988,6 +1018,7 @@ uint8_t InputHandler(uint8_t Address) { case CONSOLE_STATUS: return consoleStatus(); case CONSOLE_CURSOR_ROW: return (uint8_t)cursorRow; case CONSOLE_CURSOR_COLUMN: return (uint8_t)cursorColumn; + case CONSOLE_ATTRIBUTE: return consoleAttribute; case CONSOLE_COMMAND: // Write only. What it did is visible in the cursor and on the screen. return 0; diff --git a/Source/Emulator/io.h b/Source/Emulator/io.h index ba006bf..eb84170 100644 --- a/Source/Emulator/io.h +++ b/Source/Emulator/io.h @@ -18,7 +18,7 @@ // 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 0x05 +#define PORT_CONSOLE_TOP 0x06 #define CONSOLE_DATA 0x00 #define CONSOLE_STATUS 0x01 #define CONSOLE_CONTROL 0x02 @@ -35,6 +35,13 @@ #define CONSOLE_COMMAND 0x05 #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_REFUSE 0x11 #define PORT_MEMORY 0x12 @@ -113,6 +120,10 @@ // 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. #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 // 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 // ask the console to be, it can also ask the console what it currently is. #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 // handler for every signal that can end this process and be caught, because a machine that diff --git a/Source/Emulator/video.c b/Source/Emulator/video.c index ea558c7..936a3c5 100644 --- a/Source/Emulator/video.c +++ b/Source/Emulator/video.c @@ -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, diff --git a/Source/Emulator/video.h b/Source/Emulator/video.h index b253755..a6713dd 100644 --- a/Source/Emulator/video.h +++ b/Source/Emulator/video.h @@ -96,6 +96,21 @@ void videoReset(void); // of one-bit rows against 16 kilobytes of tiles. 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 videoRows(void); diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index 0e8f8a5..e071038 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -269,8 +269,8 @@ Port 0x00 is the oldest thing on this machine and it has not changed: writing se | Port | Register | | --- | --- | | 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. | -| 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. | +| 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, 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. @@ -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. -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. @@ -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. +### 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: 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. | | 0x04 | Cursor column. Read and write. | | 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. diff --git a/Tests/video.sh b/Tests/video.sh index 10e3280..ed1f178 100755 --- a/Tests/video.sh +++ b/Tests/video.sh @@ -93,6 +93,13 @@ emit() { 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 # SplitBit. That it now appears on a screen is the whole of this rung. say() { @@ -410,6 +417,67 @@ GOT="$(said cursorclamp)" && 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" +# ---- 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. { printf '#Program\nstart:\n' say "A"