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
@@ -50,7 +50,9 @@ every recorded result byte for byte.
|
|||||||
**The screen belongs to the machine, not to the window.** The video device is a tile engine
|
**The screen belongs to the machine, not to the window.** The video device is a tile engine
|
||||||
on ports 0x30 to 0x3F that brings its own bank of video memory, and it renders into a buffer
|
on ports 0x30 to 0x3F that brings its own bank of video memory, and it renders into a buffer
|
||||||
that is a pure function of that memory - so the same program draws the same picture whether
|
that is a pure function of that memory - so the same program draws the same picture whether
|
||||||
or not anybody is watching. Voyager puts that buffer on the glass and decides nothing about
|
or not anybody is watching. The console draws on it: it is a display controller as well as a
|
||||||
|
port, with a font, a cursor and scrollback, which is why CosmOS runs in a window without a
|
||||||
|
line of it being changed. Voyager puts that buffer on the glass and decides nothing about
|
||||||
it. Either binary will save a picture of the screen with `--screen`, which is how a test
|
it. Either binary will save a picture of the screen with `--screen`, which is how a test
|
||||||
suite on a host with no display checks what was drawn. See **The Screen** in the Programming
|
suite on a host with no display checks what was drawn. See **The Screen** in the Programming
|
||||||
Manual.
|
Manual.
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
// font.c
|
||||||
|
// GENERATED ONCE from the Hatchet-GPU sprite sheet, and vendored here on purpose.
|
||||||
|
//
|
||||||
|
// Hatchet was an earlier attempt at a graphics system for this machine and is not part of
|
||||||
|
// this repository. What survives of it is this font: an 8x8 sheet 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 and is done.
|
||||||
|
//
|
||||||
|
// One bit a pixel, most significant bit leftmost, eight bytes a glyph. The screen wants
|
||||||
|
// eight bits a pixel, so the machine expands this into tile memory at reset rather than
|
||||||
|
// storing it expanded: 1,088 bytes here against 16 kilobytes there.
|
||||||
|
//
|
||||||
|
// Index 0 is ASCII 32, the space, and is blank. Everything through ASCII 126 is where
|
||||||
|
// ASCII says it is; after that come the drawn extras - box corners and junctions, arrows,
|
||||||
|
// the card suits - which have no ASCII to be in order with.
|
||||||
|
|
||||||
|
#include "font.h"
|
||||||
|
|
||||||
|
const unsigned char consoleFont[CONSOLE_FONT_GLYPHS * CONSOLE_FONT_BYTES] = {
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 32 space
|
||||||
|
0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0x00, 0x30, // 33 '!'
|
||||||
|
0x00, 0x36, 0x36, 0x12, 0x00, 0x00, 0x00, 0x00, // 34 '"'
|
||||||
|
0x00, 0x24, 0x7E, 0x24, 0x24, 0x7E, 0x24, 0x00, // 35 '#'
|
||||||
|
0x00, 0x18, 0x3E, 0x58, 0x3C, 0x1A, 0x7C, 0x18, // 36 '$'
|
||||||
|
0x00, 0x62, 0x66, 0x0C, 0x18, 0x30, 0x66, 0x06, // 37 '%'
|
||||||
|
0x00, 0x38, 0x44, 0x48, 0x30, 0x4A, 0x44, 0x3A, // 38 '&'
|
||||||
|
0x00, 0x18, 0x18, 0x08, 0x00, 0x00, 0x00, 0x00, // 39 '''
|
||||||
|
0x00, 0x1E, 0x38, 0x70, 0x70, 0x70, 0x38, 0x1E, // 40 '('
|
||||||
|
0x00, 0x78, 0x1C, 0x0E, 0x0E, 0x0E, 0x1C, 0x78, // 41 ')'
|
||||||
|
0x00, 0x00, 0x5A, 0x3C, 0x7E, 0x3C, 0x5A, 0x00, // 42 '*'
|
||||||
|
0x00, 0x00, 0x18, 0x18, 0x7E, 0x7E, 0x18, 0x18, // 43 '+'
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x10, // 44 ','
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x7F, 0x7F, 0x00, 0x00, // 45 '-'
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x30, 0x00, // 46 '.'
|
||||||
|
0x00, 0x03, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0x60, // 47 '/'
|
||||||
|
0x00, 0x3C, 0x66, 0x4E, 0x5A, 0x72, 0x66, 0x3C, // 48 '0'
|
||||||
|
0x00, 0x18, 0x38, 0x18, 0x18, 0x18, 0x18, 0x7E, // 49 '1'
|
||||||
|
0x00, 0x3C, 0x66, 0x46, 0x0C, 0x18, 0x30, 0x7E, // 50 '2'
|
||||||
|
0x00, 0x3C, 0x66, 0x06, 0x0C, 0x06, 0x66, 0x3C, // 51 '3'
|
||||||
|
0x00, 0x3C, 0x6C, 0x6C, 0x7E, 0x7E, 0x0C, 0x0C, // 52 '4'
|
||||||
|
0x00, 0x7E, 0x60, 0x7C, 0x0E, 0x66, 0x6E, 0x3C, // 53 '5'
|
||||||
|
0x00, 0x3C, 0x66, 0x60, 0x7C, 0x66, 0x66, 0x3C, // 54 '6'
|
||||||
|
0x00, 0x7E, 0x66, 0x66, 0x0E, 0x1C, 0x38, 0x30, // 55 '7'
|
||||||
|
0x00, 0x3C, 0x66, 0x66, 0x3C, 0x66, 0x66, 0x3C, // 56 '8'
|
||||||
|
0x00, 0x3E, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x06, // 57 '9'
|
||||||
|
0x00, 0x00, 0x30, 0x30, 0x00, 0x30, 0x30, 0x00, // 58 ':'
|
||||||
|
0x00, 0x00, 0x30, 0x30, 0x00, 0x30, 0x30, 0x10, // 59 ';'
|
||||||
|
0x00, 0x0E, 0x1C, 0x38, 0x70, 0x38, 0x1C, 0x0E, // 60 '<'
|
||||||
|
0x00, 0x00, 0x3E, 0x3E, 0x00, 0x3E, 0x3E, 0x00, // 61 '='
|
||||||
|
0x00, 0x70, 0x38, 0x1C, 0x0E, 0x1C, 0x38, 0x70, // 62 '>'
|
||||||
|
0x00, 0x3C, 0x66, 0x06, 0x1C, 0x18, 0x00, 0x18, // 63 '?'
|
||||||
|
0x00, 0x3C, 0x66, 0x4E, 0x4E, 0x40, 0x60, 0x3C, // 64 '@'
|
||||||
|
0x00, 0x3E, 0x36, 0x63, 0x63, 0x7F, 0x63, 0x63, // 65 'A'
|
||||||
|
0x00, 0x7C, 0x66, 0x66, 0x7C, 0x66, 0x66, 0x7C, // 66 'B'
|
||||||
|
0x00, 0x3E, 0x73, 0x60, 0x60, 0x60, 0x73, 0x3E, // 67 'C'
|
||||||
|
0x00, 0x7C, 0x66, 0x63, 0x63, 0x63, 0x66, 0x7C, // 68 'D'
|
||||||
|
0x00, 0x7E, 0x7E, 0x60, 0x78, 0x60, 0x7E, 0x7E, // 69 'E'
|
||||||
|
0x00, 0x7E, 0x7E, 0x60, 0x7C, 0x7C, 0x60, 0x60, // 70 'F'
|
||||||
|
0x00, 0x3E, 0x73, 0x60, 0x67, 0x63, 0x73, 0x3E, // 71 'G'
|
||||||
|
0x00, 0x63, 0x63, 0x63, 0x7F, 0x63, 0x63, 0x63, // 72 'H'
|
||||||
|
0x00, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, // 73 'I'
|
||||||
|
0x00, 0x7F, 0x6C, 0x0C, 0x0C, 0x6C, 0x6C, 0x38, // 74 'J'
|
||||||
|
0x00, 0x67, 0x6E, 0x7C, 0x78, 0x7E, 0x66, 0x67, // 75 'K'
|
||||||
|
0x00, 0x60, 0x60, 0x60, 0x60, 0x60, 0x7E, 0x7E, // 76 'L'
|
||||||
|
0x00, 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, // 77 'M'
|
||||||
|
0x00, 0x63, 0x73, 0x7B, 0x7B, 0x6F, 0x67, 0x67, // 78 'N'
|
||||||
|
0x00, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x3C, // 79 'O'
|
||||||
|
0x00, 0x7E, 0x67, 0x63, 0x67, 0x7E, 0x60, 0x60, // 80 'P'
|
||||||
|
0x00, 0x3E, 0x77, 0x63, 0x63, 0x77, 0x3E, 0x07, // 81 'Q'
|
||||||
|
0x00, 0x7E, 0x67, 0x63, 0x66, 0x7C, 0x6E, 0x67, // 82 'R'
|
||||||
|
0x00, 0x3E, 0x77, 0x70, 0x3E, 0x07, 0x77, 0x3E, // 83 'S'
|
||||||
|
0x00, 0x7F, 0x6C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, // 84 'T'
|
||||||
|
0x00, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x3E, // 85 'U'
|
||||||
|
0x00, 0x63, 0x63, 0x63, 0x77, 0x3E, 0x1C, 0x08, // 86 'V'
|
||||||
|
0x00, 0x63, 0x63, 0x63, 0x6B, 0x6B, 0x7F, 0x36, // 87 'W'
|
||||||
|
0x00, 0x66, 0x66, 0x3C, 0x18, 0x3C, 0x66, 0x66, // 88 'X'
|
||||||
|
0x00, 0x63, 0x77, 0x3E, 0x1C, 0x38, 0x70, 0x60, // 89 'Y'
|
||||||
|
0x00, 0x7F, 0x7F, 0x0E, 0x1C, 0x38, 0x7F, 0x7F, // 90 'Z'
|
||||||
|
0x00, 0x3E, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3E, // 91 '['
|
||||||
|
0x00, 0x60, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, // 92 '\'
|
||||||
|
0x00, 0x7C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x7C, // 93 ']'
|
||||||
|
0x00, 0x08, 0x1C, 0x3E, 0x77, 0x63, 0x00, 0x00, // 94 '^'
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x7F, // 95 '_'
|
||||||
|
0x00, 0x30, 0x38, 0x18, 0x00, 0x00, 0x00, 0x00, // 96 '`'
|
||||||
|
0x00, 0x00, 0x38, 0x06, 0x3E, 0x66, 0x66, 0x3B, // 97 'a'
|
||||||
|
0x00, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x7C, // 98 'b'
|
||||||
|
0x00, 0x00, 0x3C, 0x66, 0x60, 0x60, 0x66, 0x3C, // 99 'c'
|
||||||
|
0x00, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x66, 0x3E, // 100 'd'
|
||||||
|
0x00, 0x00, 0x3C, 0x66, 0x7C, 0x60, 0x62, 0x3C, // 101 'e'
|
||||||
|
0x00, 0x1E, 0x30, 0x60, 0x7C, 0x60, 0x60, 0x60, // 102 'f'
|
||||||
|
0x00, 0x00, 0x3C, 0x66, 0x66, 0x3E, 0x06, 0x7C, // 103 'g'
|
||||||
|
0x00, 0x60, 0x60, 0x7C, 0x66, 0x66, 0x66, 0x66, // 104 'h'
|
||||||
|
0x00, 0x18, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, // 105 'i'
|
||||||
|
0x00, 0x06, 0x00, 0x06, 0x06, 0x66, 0x66, 0x3C, // 106 'j'
|
||||||
|
0x00, 0x60, 0x66, 0x6C, 0x78, 0x7C, 0x64, 0x66, // 107 'k'
|
||||||
|
0x00, 0x38, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, // 108 'l'
|
||||||
|
0x00, 0x00, 0xC2, 0x66, 0x7E, 0x7E, 0x66, 0x66, // 109 'm'
|
||||||
|
0x00, 0x00, 0xEC, 0x76, 0x66, 0x66, 0x66, 0x66, // 110 'n'
|
||||||
|
0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x66, 0x3C, // 111 'o'
|
||||||
|
0x00, 0x00, 0x7C, 0x66, 0x66, 0x7C, 0x60, 0x60, // 112 'p'
|
||||||
|
0x00, 0x00, 0x3C, 0x66, 0x3C, 0x04, 0x19, 0x3E, // 113 'q'
|
||||||
|
0x00, 0x00, 0x60, 0x7C, 0x6C, 0x60, 0x60, 0x60, // 114 'r'
|
||||||
|
0x00, 0x00, 0x3C, 0x66, 0x30, 0x0C, 0x66, 0x3C, // 115 's'
|
||||||
|
0x00, 0x18, 0x18, 0x7E, 0x58, 0x18, 0x18, 0x18, // 116 't'
|
||||||
|
0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x7E, 0x3D, // 117 'u'
|
||||||
|
0x00, 0x00, 0x42, 0x66, 0x66, 0x76, 0x3C, 0x18, // 118 'v'
|
||||||
|
0x00, 0x00, 0x42, 0x66, 0x66, 0x7E, 0x7E, 0x24, // 119 'w'
|
||||||
|
0x00, 0x00, 0x66, 0x76, 0x38, 0x1C, 0x6E, 0x66, // 120 'x'
|
||||||
|
0x00, 0x00, 0x66, 0x66, 0x76, 0x3E, 0x06, 0x7C, // 121 'y'
|
||||||
|
0x00, 0x00, 0x7E, 0x66, 0x0C, 0x18, 0x32, 0x7E, // 122 'z'
|
||||||
|
0x00, 0x0E, 0x38, 0x30, 0x18, 0x30, 0x38, 0x0E, // 123 '{'
|
||||||
|
0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, // 124 '|'
|
||||||
|
0x00, 0x70, 0x1C, 0x0C, 0x18, 0x0C, 0x1C, 0x78, // 125 '}'
|
||||||
|
0x00, 0x00, 0x03, 0x3B, 0x6E, 0x60, 0x00, 0x00, // 126 '~'
|
||||||
|
0x00, 0x40, 0x60, 0x70, 0x78, 0x7C, 0x10, 0x00, // 127
|
||||||
|
0x00, 0x00, 0x00, 0x0F, 0x1F, 0x1C, 0x18, 0x18, // 128
|
||||||
|
0x00, 0x00, 0x00, 0xF0, 0xF8, 0x38, 0x18, 0x18, // 129
|
||||||
|
0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, // 130
|
||||||
|
0x00, 0x00, 0x00, 0xFF, 0xFF, 0x3C, 0x18, 0x18, // 131
|
||||||
|
0x18, 0x18, 0x38, 0xF8, 0xF8, 0x38, 0x18, 0x18, // 132
|
||||||
|
0xFF, 0xFF, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, // 133
|
||||||
|
0x03, 0x03, 0x07, 0xFF, 0xFF, 0x07, 0x03, 0x03, // 134
|
||||||
|
0x01, 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, // 135
|
||||||
|
0x80, 0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, // 136
|
||||||
|
0xFF, 0xFF, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, // 137
|
||||||
|
0xFF, 0xFF, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, // 138
|
||||||
|
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 139
|
||||||
|
0x18, 0x18, 0x1C, 0x1F, 0x0F, 0x00, 0x00, 0x00, // 140
|
||||||
|
0x18, 0x18, 0x38, 0xF8, 0xF0, 0x00, 0x00, 0x00, // 141
|
||||||
|
0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, // 142
|
||||||
|
0x18, 0x18, 0x1C, 0x1F, 0x1F, 0x1C, 0x18, 0x18, // 143
|
||||||
|
0x18, 0x18, 0x3C, 0xFF, 0xFF, 0x00, 0x00, 0x00, // 144
|
||||||
|
0xC0, 0xC0, 0xE0, 0xFF, 0xFF, 0xE0, 0xC0, 0xC0, // 145
|
||||||
|
0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0xFF, 0xFF, // 146
|
||||||
|
0xC0, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, // 147
|
||||||
|
0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0x80, // 148
|
||||||
|
0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xFF, 0xFF, // 149
|
||||||
|
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xFF, 0xFF, // 150
|
||||||
|
0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, // 151
|
||||||
|
0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, // 152
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, // 153
|
||||||
|
0x00, 0x36, 0x7F, 0x7F, 0x7F, 0x3E, 0x1C, 0x08, // 154
|
||||||
|
0x00, 0x08, 0x1C, 0x3E, 0x7F, 0x3E, 0x08, 0x3E, // 155
|
||||||
|
0x00, 0x08, 0x1C, 0x3E, 0x7F, 0x3E, 0x1C, 0x08, // 156
|
||||||
|
0x00, 0x1C, 0x1C, 0x7F, 0x7F, 0x7F, 0x08, 0x3E, // 157
|
||||||
|
0x00, 0x18, 0x24, 0x24, 0x42, 0x42, 0x7E, 0x00, // 158
|
||||||
|
0x00, 0x3C, 0x66, 0x42, 0x42, 0x66, 0x3C, 0x00, // 159
|
||||||
|
0x00, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x00, // 160
|
||||||
|
0x00, 0x42, 0x24, 0x18, 0x18, 0x24, 0x42, 0x00, // 161
|
||||||
|
0x00, 0x02, 0x04, 0x44, 0x28, 0x28, 0x10, 0x00, // 162
|
||||||
|
0x08, 0x0C, 0x0E, 0xFF, 0xFF, 0x0E, 0x0C, 0x08, // 163
|
||||||
|
0x18, 0x18, 0x18, 0x18, 0xFF, 0x7E, 0x3C, 0x18, // 164
|
||||||
|
0x10, 0x30, 0x70, 0xFF, 0xFF, 0x70, 0x30, 0x10, // 165
|
||||||
|
0x18, 0x3C, 0x7E, 0xFF, 0x18, 0x18, 0x18, 0x18, // 166
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// font.h
|
||||||
|
// The console's character generator.
|
||||||
|
// Written by Anachronaut
|
||||||
|
|
||||||
|
#ifndef FONT_H
|
||||||
|
#define FONT_H
|
||||||
|
|
||||||
|
// Eight bytes a glyph, one bit a pixel. See font.c for where it came from.
|
||||||
|
#define CONSOLE_FONT_BYTES 8
|
||||||
|
#define CONSOLE_FONT_GLYPHS 135
|
||||||
|
|
||||||
|
// The first character the font has, so a byte maps to a glyph by subtracting this.
|
||||||
|
#define CONSOLE_FONT_FIRST 32
|
||||||
|
|
||||||
|
extern const unsigned char consoleFont[CONSOLE_FONT_GLYPHS * CONSOLE_FONT_BYTES];
|
||||||
|
|
||||||
|
#endif // FONT_H
|
||||||
+101
-3
@@ -7,6 +7,7 @@
|
|||||||
#include "../Assembler/assembly.h" // For the fault vector numbers.
|
#include "../Assembler/assembly.h" // For the fault vector numbers.
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "video.h"
|
#include "video.h"
|
||||||
|
#include "font.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -199,6 +200,78 @@ static void consoleShowWhatIsWritten(void) {
|
|||||||
fflush(stdout);
|
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) {
|
uint8_t consoleReadByte(void) {
|
||||||
// Taking the byte answers whatever the console was asking about, so the line comes
|
// 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
|
// 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;
|
return byte;
|
||||||
}
|
}
|
||||||
consoleShowWhatIsWritten();
|
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;
|
unsigned char byte;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
ssize_t got = read(STDIN_FILENO, &byte, 1);
|
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.
|
// This function sends the DataByte to the appropriate place based on the Port Address.
|
||||||
switch(Address) {
|
switch(Address) {
|
||||||
case CONSOLE_DATA:
|
case CONSOLE_DATA:
|
||||||
// If data is sent here, it should be written to STDOUT.
|
// Both, always. The screen because this machine has one, and standard output
|
||||||
// For now, I'll implement this so it simply writes each byte out as it comes in.
|
// because the serial line is how everything that is not a person reads it.
|
||||||
// Later, I'll want to use a buffer for this for performance, probably.
|
consoleDraw(DataByte);
|
||||||
putchar(DataByte);
|
putchar(DataByte);
|
||||||
break;
|
break;
|
||||||
case CONSOLE_CONTROL: consoleSetControl(DataByte); break;
|
case CONSOLE_CONTROL: consoleSetControl(DataByte); break;
|
||||||
|
|||||||
@@ -132,6 +132,23 @@ void consoleRestore(void);
|
|||||||
// of its own behind that would make the status port lie about what is waiting.
|
// of its own behind that would make the status port lie about what is waiting.
|
||||||
uint8_t consoleReadByte(void);
|
uint8_t consoleReadByte(void);
|
||||||
|
|
||||||
|
// Puts the cursor back in the corner. Called when the machine starts, since the screen is
|
||||||
|
// cleared then too and a cursor left where the last program stopped would be a cursor
|
||||||
|
// pointing into something that is gone.
|
||||||
|
void consoleHome(void);
|
||||||
|
|
||||||
|
// ---- How a front end with a window feeds the console ----
|
||||||
|
//
|
||||||
|
// Called while the console has nothing to give. It returns a byte, or one of the two
|
||||||
|
// answers below. They have to be told apart: a window with nobody typing yet is the normal
|
||||||
|
// case and happens sixty times a second, while a window that has gone is the end of input.
|
||||||
|
// One value for both would have made the first keystroke look like a closed machine.
|
||||||
|
//
|
||||||
|
// Without a hook the console reads standard input, which is what it has always done.
|
||||||
|
#define CONSOLE_NOTHING_YET (-1)
|
||||||
|
#define CONSOLE_GONE (-2)
|
||||||
|
void consoleSetInputHook(int (*hook)(void));
|
||||||
|
|
||||||
// ---- Device classes ----
|
// ---- Device classes ----
|
||||||
//
|
//
|
||||||
// What kind of thing is plugged into a port. Class 0 is not a device: reading an
|
// What kind of thing is plugged into a port. Class 0 is not a device: reading an
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ uint8_t machineStart(Machine *m, const EmulatorOptions *options, const char *pro
|
|||||||
// the device's, and a reset that left last program's screen up would be a reset that
|
// the device's, and a reset that left last program's screen up would be a reset that
|
||||||
// did not happen.
|
// did not happen.
|
||||||
videoReset();
|
videoReset();
|
||||||
|
consoleHome();
|
||||||
// The controller has to know where the memories are before anything can reach
|
// The controller has to know where the memories are before anything can reach
|
||||||
// them through it. Banks 0 and 1 are those two arrays.
|
// them through it. Banks 0 and 1 are those two arrays.
|
||||||
initializeController(Program, Data);
|
initializeController(Program, Data);
|
||||||
@@ -190,6 +191,7 @@ void machineRunSlice(Machine *m) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
videoReset();
|
videoReset();
|
||||||
|
consoleHome();
|
||||||
initializeCPU(&m->cpu, Program, Data);
|
initializeCPU(&m->cpu, Program, Data);
|
||||||
break; // Out of this batch; the loop above carries on with a new CPU.
|
break; // Out of this batch; the loop above carries on with a new CPU.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
// Written by Anachronaut
|
// Written by Anachronaut
|
||||||
|
|
||||||
#include "video.h"
|
#include "video.h"
|
||||||
|
#include "font.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
@@ -29,12 +30,66 @@ static int renderedHeight = 0;
|
|||||||
static int columnsFor(uint8_t m) { return m == VIDEO_MODE_80x50 ? 80 : 40; }
|
static int columnsFor(uint8_t m) { return m == VIDEO_MODE_80x50 ? 80 : 40; }
|
||||||
static int rowsFor(uint8_t m) { return m == VIDEO_MODE_80x50 ? 50 : 25; }
|
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 ----
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
static const uint8_t defaultInk[3] = { 0xDC, 0xE6, 0xDC };
|
||||||
|
static const uint8_t defaultPaper[3] = { 0x10, 0x14, 0x12 };
|
||||||
|
|
||||||
|
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
|
||||||
|
// paper. Glyphs the font does not have are left blank rather than left as whatever was
|
||||||
|
// in tile memory.
|
||||||
|
memset(videoRAM + VIDEO_TILE_BASE, 0, (size_t)VIDEO_TILE_COUNT * VIDEO_TILE_BYTES);
|
||||||
|
for (int glyph = 0; glyph < CONSOLE_FONT_GLYPHS && glyph < VIDEO_TILE_COUNT; glyph++) {
|
||||||
|
uint8_t *tile = videoRAM + VIDEO_TILE_BASE + glyph * VIDEO_TILE_BYTES;
|
||||||
|
for (int y = 0; y < CONSOLE_FONT_BYTES; y++) {
|
||||||
|
const unsigned char row = consoleFont[glyph * CONSOLE_FONT_BYTES + y];
|
||||||
|
for (int x = 0; x < VIDEO_CELL_PIXELS; x++) {
|
||||||
|
tile[y * VIDEO_CELL_PIXELS + x] = (row & (0x80u >> x)) ? 1 : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint8_t *palette = videoRAM + VIDEO_PALETTE_BASE;
|
||||||
|
memcpy(palette + 0 * VIDEO_PALETTE_BYTES, defaultPaper, 3);
|
||||||
|
memcpy(palette + 1 * VIDEO_PALETTE_BYTES, defaultInk, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void videoPutCell(int screenRow, int column, uint8_t tile, uint8_t attribute) {
|
||||||
|
if (screenRow < 0 || screenRow >= rowsFor(mode)) return;
|
||||||
|
if (column < 0 || column >= columnsFor(mode)) return;
|
||||||
|
const int mapRow = (scroll + screenRow) % VIDEO_MAP_ROWS;
|
||||||
|
uint8_t *cell = videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE
|
||||||
|
+ column * VIDEO_CELL_BYTES;
|
||||||
|
cell[0] = tile;
|
||||||
|
cell[1] = attribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
void videoScrollUp(void) {
|
||||||
|
scroll = (uint8_t)((scroll + 1) % VIDEO_MAP_ROWS);
|
||||||
|
// The row now at the bottom held whatever was there a ring ago, so it is cleared. The
|
||||||
|
// rows that went off the top are NOT cleared, which is the whole of the scrollback: a
|
||||||
|
// hundred rows of what has already been said, still sitting in the map.
|
||||||
|
const int bottom = rowsFor(mode) - 1;
|
||||||
|
const int mapRow = (scroll + bottom) % VIDEO_MAP_ROWS;
|
||||||
|
memset(videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE, 0, VIDEO_MAP_STRIDE);
|
||||||
|
}
|
||||||
|
|
||||||
void videoReset(void) {
|
void videoReset(void) {
|
||||||
memset(videoRAM, 0, sizeof(videoRAM));
|
memset(videoRAM, 0, sizeof(videoRAM));
|
||||||
mode = VIDEO_MODE_40x25;
|
mode = VIDEO_MODE_40x25;
|
||||||
scroll = 0;
|
scroll = 0;
|
||||||
renderedWidth = 0;
|
renderedWidth = 0;
|
||||||
renderedHeight = 0;
|
renderedHeight = 0;
|
||||||
|
// A machine wakes up able to show text. Everything here is ordinary video memory that a
|
||||||
|
// program may overwrite the moment it wants the screen for something else.
|
||||||
|
videoLoadFont();
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t *videoMemory(uint32_t *capacity) {
|
uint8_t *videoMemory(uint32_t *capacity) {
|
||||||
|
|||||||
@@ -85,6 +85,28 @@
|
|||||||
|
|
||||||
void videoReset(void);
|
void videoReset(void);
|
||||||
|
|
||||||
|
// ---- What the console needs to draw with ----
|
||||||
|
//
|
||||||
|
// The Voyager's console is a display controller: it takes a byte stream and puts glyphs on
|
||||||
|
// the screen, the way a video terminal's character generator does. That is a real kind of
|
||||||
|
// chip rather than an emulator convenience - but it does mean the console and a program
|
||||||
|
// drawing graphics are writing one screen, because a machine has one screen.
|
||||||
|
//
|
||||||
|
// The font is expanded into tile memory at reset rather than stored expanded: 1,088 bytes
|
||||||
|
// of one-bit rows against 16 kilobytes of tiles.
|
||||||
|
void videoLoadFont(void);
|
||||||
|
|
||||||
|
int videoColumns(void);
|
||||||
|
int videoRows(void);
|
||||||
|
|
||||||
|
// Screen coordinates, not map coordinates. The ring is the device's business, and a caller
|
||||||
|
// that had to know where the origin was would have to be told every time it moved.
|
||||||
|
void videoPutCell(int screenRow, int column, uint8_t tile, uint8_t attribute);
|
||||||
|
|
||||||
|
// Moves the origin on by a row and clears the one that has just come into view at the
|
||||||
|
// bottom - which is holding whatever was there 128 rows ago, since the map is a ring.
|
||||||
|
void videoScrollUp(void);
|
||||||
|
|
||||||
uint8_t *videoMemory(uint32_t *capacity);
|
uint8_t *videoMemory(uint32_t *capacity);
|
||||||
|
|
||||||
uint8_t videoWrite(uint8_t value, uint8_t port);
|
uint8_t videoWrite(uint8_t value, uint8_t port);
|
||||||
|
|||||||
+74
-30
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
#include "machine.h"
|
#include "machine.h"
|
||||||
#include "video.h"
|
#include "video.h"
|
||||||
|
#include "io.h"
|
||||||
#include "utility.h"
|
#include "utility.h"
|
||||||
#include "raylib.h"
|
#include "raylib.h"
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -58,6 +59,65 @@ static int takeHeadless(int *argc, char *argv[]) {
|
|||||||
return headless;
|
return headless;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- The window, kept in one place ----
|
||||||
|
//
|
||||||
|
// Both the frame loop and the input hook have to be able to present, because a machine
|
||||||
|
// waiting for a key is still a machine somebody is looking at. A window that froze while a
|
||||||
|
// program asked a question would look broken every time it asked one.
|
||||||
|
static Texture2D screenTexture;
|
||||||
|
static int windowOpen = 0;
|
||||||
|
|
||||||
|
static void presentFrame(void) {
|
||||||
|
// The device turns video memory into pixels; this puts them on the glass. Everything
|
||||||
|
// that decides what the screen looks like is in the machine, where the suite can
|
||||||
|
// reach it.
|
||||||
|
videoRender();
|
||||||
|
int width, height;
|
||||||
|
const uint8_t *frame = videoPixels(&width, &height);
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
UpdateTextureRec(screenTexture, (Rectangle){ 0, 0, (float)width, (float)height },
|
||||||
|
frame);
|
||||||
|
}
|
||||||
|
BeginDrawing();
|
||||||
|
// Not black. The border around a smaller mode should look like a machine with a screen
|
||||||
|
// on, rather than like a window that failed to open.
|
||||||
|
ClearBackground((Color){ 18, 22, 20, 255 });
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
// Centred, so changing mode moves the picture rather than the window.
|
||||||
|
Rectangle from = { 0, 0, (float)width, (float)height };
|
||||||
|
Rectangle to = {
|
||||||
|
(float)((VIDEO_MAX_WIDTH * SCREEN_SCALE - width * SCREEN_SCALE) / 2),
|
||||||
|
(float)((VIDEO_MAX_HEIGHT * SCREEN_SCALE - height * SCREEN_SCALE) / 2),
|
||||||
|
(float)(width * SCREEN_SCALE), (float)(height * SCREEN_SCALE)
|
||||||
|
};
|
||||||
|
DrawTexturePro(screenTexture, from, to, (Vector2){ 0, 0 }, 0.0f, WHITE);
|
||||||
|
}
|
||||||
|
EndDrawing();
|
||||||
|
}
|
||||||
|
|
||||||
|
// What the console asks while it is waiting. Presenting from in here is what keeps the
|
||||||
|
// window answering, and EndDrawing paces it, so waiting for a key costs a frame rather
|
||||||
|
// than a spin.
|
||||||
|
static int voyagerKey(void) {
|
||||||
|
if (!windowOpen || WindowShouldClose()) {
|
||||||
|
windowOpen = 0;
|
||||||
|
return CONSOLE_GONE;
|
||||||
|
}
|
||||||
|
presentFrame();
|
||||||
|
int character = GetCharPressed();
|
||||||
|
if (character > 0 && character < 128) {
|
||||||
|
return character;
|
||||||
|
}
|
||||||
|
switch (GetKeyPressed()) {
|
||||||
|
// The keys a character queue does not carry, because they are not characters.
|
||||||
|
case KEY_ENTER: case KEY_KP_ENTER: return '\n';
|
||||||
|
case KEY_BACKSPACE: return 0x08;
|
||||||
|
case KEY_TAB: return '\t';
|
||||||
|
case KEY_ESCAPE: return 0x1B;
|
||||||
|
default: return CONSOLE_NOTHING_YET;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
int headless = takeHeadless(&argc, argv);
|
int headless = takeHeadless(&argc, argv);
|
||||||
|
|
||||||
@@ -103,47 +163,31 @@ int main(int argc, char *argv[]) {
|
|||||||
// allocation sixty times a second for a picture that is the same size every time.
|
// allocation sixty times a second for a picture that is the same size every time.
|
||||||
Image blank = GenImageColor(VIDEO_MAX_WIDTH, VIDEO_MAX_HEIGHT, BLACK);
|
Image blank = GenImageColor(VIDEO_MAX_WIDTH, VIDEO_MAX_HEIGHT, BLACK);
|
||||||
ImageFormat(&blank, PIXELFORMAT_UNCOMPRESSED_R8G8B8);
|
ImageFormat(&blank, PIXELFORMAT_UNCOMPRESSED_R8G8B8);
|
||||||
Texture2D screen = LoadTextureFromImage(blank);
|
screenTexture = LoadTextureFromImage(blank);
|
||||||
UnloadImage(blank);
|
UnloadImage(blank);
|
||||||
|
windowOpen = 1;
|
||||||
|
// The keyboard becomes the console's input, in place of a standard input the window
|
||||||
|
// does not have.
|
||||||
|
consoleSetInputHook(voyagerKey);
|
||||||
// ---- A slice a frame ----
|
// ---- A slice a frame ----
|
||||||
//
|
//
|
||||||
// The machine gets its turn, then the window gets its turn. Closing the window
|
// The machine gets its turn, then the window gets its turn. Closing the window
|
||||||
// stops the machine, and the machine halting leaves the window up so that whatever
|
// stops the machine, and the machine halting leaves the window up so that whatever
|
||||||
// it drew is still there to look at - a program that ends should not take its
|
// it drew is still there to look at - a program that ends should not take its
|
||||||
// output off the screen with it.
|
// output off the screen with it.
|
||||||
while (!WindowShouldClose()) {
|
// A slice, then a frame. The machine halting leaves the window up so that whatever
|
||||||
|
// it drew is still there to look at - a program that ends should not take its output
|
||||||
|
// off the screen with it.
|
||||||
|
while (windowOpen && !WindowShouldClose()) {
|
||||||
if (machineRunning(&machine)) {
|
if (machineRunning(&machine)) {
|
||||||
machineRunSlice(&machine);
|
machineRunSlice(&machine);
|
||||||
}
|
}
|
||||||
// ---- Presenting, not deciding ----
|
presentFrame();
|
||||||
//
|
|
||||||
// The device turns video memory into pixels; this puts them on the glass. The
|
|
||||||
// split is the whole design: everything that decides what the screen looks like
|
|
||||||
// is in the machine, where the suite can reach it.
|
|
||||||
videoRender();
|
|
||||||
int width, height;
|
|
||||||
const uint8_t *frame = videoPixels(&width, &height);
|
|
||||||
if (width > 0 && height > 0) {
|
|
||||||
UpdateTextureRec(screen, (Rectangle){ 0, 0, (float)width, (float)height },
|
|
||||||
frame);
|
|
||||||
}
|
}
|
||||||
BeginDrawing();
|
windowOpen = 0;
|
||||||
// Not black. The border around a smaller mode should look like a machine with
|
// Taken back before the machine stops, so nothing can ask a window that has gone.
|
||||||
// a screen on, rather than like a window that failed to open.
|
consoleSetInputHook(NULL);
|
||||||
ClearBackground((Color){ 18, 22, 20, 255 });
|
UnloadTexture(screenTexture);
|
||||||
if (width > 0 && height > 0) {
|
|
||||||
// Centred, so changing mode moves the picture rather than the window.
|
|
||||||
Rectangle from = { 0, 0, (float)width, (float)height };
|
|
||||||
Rectangle to = {
|
|
||||||
(float)((VIDEO_MAX_WIDTH * SCREEN_SCALE - width * SCREEN_SCALE) / 2),
|
|
||||||
(float)((VIDEO_MAX_HEIGHT * SCREEN_SCALE - height * SCREEN_SCALE) / 2),
|
|
||||||
(float)(width * SCREEN_SCALE), (float)(height * SCREEN_SCALE)
|
|
||||||
};
|
|
||||||
DrawTexturePro(screen, from, to, (Vector2){ 0, 0 }, 0.0f, WHITE);
|
|
||||||
}
|
|
||||||
EndDrawing();
|
|
||||||
}
|
|
||||||
UnloadTexture(screen);
|
|
||||||
CloseWindow();
|
CloseWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -261,7 +261,10 @@ Nothing in that program names a Data Pointer, so all of it runs through Data Poi
|
|||||||
|
|
||||||
## The Console:
|
## The Console:
|
||||||
|
|
||||||
Port 0x00 is the oldest thing on this machine and it has not changed: writing sends a byte out, reading takes one in and waits until there is one. Every program ever written for SplitBit uses it that way and still does. What is new is that a program can say what it wants a keypress to mean, can ask whether a read would have to wait, and can arrange to be told when a byte arrives instead of having to ask at all.
|
Port 0x00 is the oldest thing on this machine and it has not changed: writing sends a byte out, reading takes one in and waits until there is one.
|
||||||
|
|
||||||
|
**On a machine with a screen, the console draws.** It is a display controller as well as a port: it owns a font, keeps a cursor, and scrolls - which is what a video terminal's character generator did, and is why a program written before there was a screen puts text on one without being changed. See Writing On The Screen below.
|
||||||
|
Every program ever written for SplitBit uses it that way and still does. What is new is that a program can say what it wants a keypress to mean, can ask whether a read would have to wait, and can arrange to be told when a byte arrives instead of having to ask at all.
|
||||||
|
|
||||||
| Port | Register |
|
| Port | Register |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -570,6 +573,26 @@ Scrolling therefore moves a register and no memory at all. That is not a small s
|
|||||||
|
|
||||||
And the rows that scrolled off are still in the map, which is where a terminal on this machine gets scrollback without having to keep any.
|
And the rows that scrolled off are still in the map, which is where a terminal on this machine gets scrollback without having to keep any.
|
||||||
|
|
||||||
|
### Writing On The Screen:
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
| Byte | Does |
|
||||||
|
| --- | --- |
|
||||||
|
| 0x0A | Newline. The cursor goes to the start of the next row, and at the last row the screen scrolls instead. |
|
||||||
|
| 0x0D | Carriage return. The cursor goes to the start of the row it is on. |
|
||||||
|
| 0x08 | Backspace. The cursor steps back and rubs out what was there. |
|
||||||
|
|
||||||
|
Writing past the last column wraps to the next row, the same as a newline.
|
||||||
|
|
||||||
|
**Scrolling moves the video device's Scroll register and no memory at all.** The row that comes into view at the bottom is cleared, because the map is a ring and it is holding whatever was there 128 rows ago. The rows that go off the top are *not* cleared, and that is the point: a hundred rows of what has already been said are still in the map, so a machine has scrollback without anything having to keep it.
|
||||||
|
|
||||||
|
**It is one screen.** A program that writes its own tiles and its own map has taken the screen, and a console still writing characters into it will scribble on what that program drew. This is not an oversight to be worked around - it is what one screen means, and it is why a program that wants the screen takes it.
|
||||||
|
|
||||||
## Asking What Is There:
|
## Asking What Is There:
|
||||||
|
|
||||||
A program that only ever runs on one machine can be told where everything is. A program meant to run on more than one has to ask, and the bus registry on port 0xFF is what it asks.
|
A program that only ever runs on one machine can be told where everything is. A program meant to run on more than one has to ask, and the bus registry on port 0xFF is what it asks.
|
||||||
|
|||||||
@@ -158,6 +158,14 @@ the palette is what colours it, that the attribute nibble adds sixteen, that scr
|
|||||||
which row is on top, that the map wraps, and that an impossible mode is refused without
|
which row is on top, that the map wraps, and that an impossible mode is refused without
|
||||||
stopping the machine.
|
stopping the machine.
|
||||||
|
|
||||||
|
**Half of it is about the console rather than the device.** Those programs ask the video
|
||||||
|
device for nothing at all: they write bytes to port 0x00, the way every SplitBit program
|
||||||
|
always has, and the picture is what is checked. A character lands at the cursor and the
|
||||||
|
cursor moves along, a newline starts the next row, backspace rubs out, the line wraps at the
|
||||||
|
last column, and the screen scrolls by moving the origin rather than by moving 1,920 bytes -
|
||||||
|
which is checked by reading the register back, because a console that blitted rows instead
|
||||||
|
would look identical on the screen and cost twelve percent of a frame a line.
|
||||||
|
|
||||||
Breaking the additive nibble fails exactly one check. Breaking the scroll origin fails
|
Breaking the additive nibble fails exactly one check. Breaking the scroll origin fails
|
||||||
exactly two. Moving every cell one pixel sideways fails the four about placement. That is
|
exactly two. Moving every cell one pixel sideways fails the four about placement. That is
|
||||||
what a screen test is supposed to do.
|
what a screen test is supposed to do.
|
||||||
|
|||||||
+119
@@ -58,6 +58,13 @@ start:
|
|||||||
INIA 0x03
|
INIA 0x03
|
||||||
OUTA 0xE8 ; Video memory becomes bank 3
|
OUTA 0xE8 ; Video memory becomes bank 3
|
||||||
ASM
|
ASM
|
||||||
|
# ---- Said rather than assumed ----
|
||||||
|
#
|
||||||
|
# The machine wakes up with a palette so that it can show text before any program has
|
||||||
|
# run, so palette entry 0 is the console's paper rather than black. A check that wanted
|
||||||
|
# black and got paper would be a check that had quietly depended on a default. These
|
||||||
|
# tests are about the device, so they set what they are about to look at.
|
||||||
|
poke 0xC000 0x00; poke 0xC001 0x00; poke 0xC002 0x00
|
||||||
}
|
}
|
||||||
|
|
||||||
poke() {
|
poke() {
|
||||||
@@ -81,6 +88,20 @@ epilogue() {
|
|||||||
printf ' HALT\n#Vectors\n Boot start\n'
|
printf ' HALT\n#Vectors\n Boot start\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# One byte to the console, by its number.
|
||||||
|
emit() {
|
||||||
|
printf ' INIA 0d%d\n OUTA 0x00\n' "$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() {
|
||||||
|
local i
|
||||||
|
for (( i = 0; i < ${#1}; i++ )); do
|
||||||
|
printf ' INIA 0d%d\n OUTA 0x00\n' "'${1:$i:1}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# Assembles what is on standard input, runs it, and leaves the picture in $BUILD/<name>.ppm.
|
# Assembles what is on standard input, runs it, and leaves the picture in $BUILD/<name>.ppm.
|
||||||
run() {
|
run() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
@@ -112,6 +133,24 @@ size() {
|
|||||||
|
|
||||||
echo "Checking what the video device draws."
|
echo "Checking what the video device draws."
|
||||||
|
|
||||||
|
# ---- The machine wakes up able to show text ----
|
||||||
|
#
|
||||||
|
# Before any program has done anything: the font is in tile memory and the two colours a
|
||||||
|
# console needs are in the palette. Checked at the pixel, because a font that loaded into
|
||||||
|
# the wrong place would still be a font that loaded.
|
||||||
|
{ printf '#Program\nstart:\n'
|
||||||
|
# 'A' is ASCII 65, so glyph 33, and its top-left pixel is paper while its middle is ink.
|
||||||
|
printf ' INIA 0d65\n OUTA 0x00\n'
|
||||||
|
epilogue
|
||||||
|
} | run wakeup || exit 1
|
||||||
|
|
||||||
|
[ "$(pixel wakeup 0 0)" = "16,20,18" ] \
|
||||||
|
&& result ok "the machine wakes with paper" "before any program set one" \
|
||||||
|
|| result no "the machine wakes with paper" "got $(pixel wakeup 0 0)"
|
||||||
|
[ "$(pixel wakeup 2 1)" = "220,230,220" ] \
|
||||||
|
&& result ok "and with a font to write in" "a letter A, drawn in ink" \
|
||||||
|
|| result no "and with a font to write in" "got $(pixel wakeup 2 1)"
|
||||||
|
|
||||||
# ---- A tile lands where it is put ----
|
# ---- A tile lands where it is put ----
|
||||||
#
|
#
|
||||||
# Palette entry 1 is red, tile 1 is 64 pixels of index 1, and two cells name it: the corner
|
# Palette entry 1 is red, tile 1 is 64 pixels of index 1, and two cells name it: the corner
|
||||||
@@ -239,6 +278,86 @@ GOT="$(head -c 1 "$BUILD/badmode.out" | od -An -tu1 | tr -d ' ')"
|
|||||||
&& result ok "an impossible mode is not taken" "still 40 columns" \
|
&& result ok "an impossible mode is not taken" "still 40 columns" \
|
||||||
|| result no "an impossible mode is not taken" "got $GOT"
|
|| result no "an impossible mode is not taken" "got $GOT"
|
||||||
|
|
||||||
|
# ---- The console draws ----
|
||||||
|
#
|
||||||
|
# Nothing below asks the video device for anything. Every one of these programs does what
|
||||||
|
# every SplitBit program has always done - write a byte to port 0x00 - and the picture is
|
||||||
|
# the point. That is why CosmOS needed no changes to run on a screen.
|
||||||
|
#
|
||||||
|
# 'A' has ink at (2,1) inside its cell and paper at the corner, which is what makes a letter
|
||||||
|
# tellable from an empty cell one pixel at a time.
|
||||||
|
inked() { [ "$(pixel "$1" "$2" "$3")" = "220,230,220" ]; }
|
||||||
|
papered() { [ "$(pixel "$1" "$2" "$3")" = "16,20,18" ]; }
|
||||||
|
|
||||||
|
{ printf '#Program\nstart:\n'; say "AA"; epilogue; } | run twoletters || exit 1
|
||||||
|
inked twoletters 2 1 \
|
||||||
|
&& result ok "a character lands at the cursor" "cell 0 has a letter in it" \
|
||||||
|
|| result no "a character lands at the cursor" "nothing at 2,1"
|
||||||
|
inked twoletters 10 1 \
|
||||||
|
&& result ok "and the cursor moves along" "the second is in cell 1" \
|
||||||
|
|| result no "and the cursor moves along" "nothing at 10,1"
|
||||||
|
|
||||||
|
{ printf '#Program\nstart:\n'; say "A"; emit 10; say "A"; epilogue; } | run newline || exit 1
|
||||||
|
inked newline 2 9 \
|
||||||
|
&& result ok "a newline starts the next row" "the second is a row down" \
|
||||||
|
|| result no "a newline starts the next row" "nothing at 2,9"
|
||||||
|
papered newline 10 1 \
|
||||||
|
&& result ok "and goes back to the first column" "cell 1 of row 0 is untouched" \
|
||||||
|
|| result no "and goes back to the first column" "something at 10,1"
|
||||||
|
|
||||||
|
{ printf '#Program\nstart:\n'; say "A"; emit 8; epilogue; } | run backspace || exit 1
|
||||||
|
papered backspace 2 1 \
|
||||||
|
&& result ok "backspace rubs the letter out" "the cell is paper again" \
|
||||||
|
|| result no "backspace rubs the letter out" "still inked at 2,1"
|
||||||
|
|
||||||
|
# Forty columns, so the forty-first character is on the next row whether anybody asked for a
|
||||||
|
# newline or not.
|
||||||
|
{ printf '#Program\nstart:\n'
|
||||||
|
for i in $(seq 1 41); do say "A"; done
|
||||||
|
epilogue
|
||||||
|
} | run wrap || exit 1
|
||||||
|
inked wrap 2 9 \
|
||||||
|
&& result ok "the line wraps at the last column" "character 41 is on row 1" \
|
||||||
|
|| result no "the line wraps at the last column" "nothing at 2,9"
|
||||||
|
|
||||||
|
# ---- Scrolling, which is the reason a terminal is affordable here ----
|
||||||
|
#
|
||||||
|
# Twenty-five rows, so a twenty-sixth line moves the screen rather than the cursor. The
|
||||||
|
# check is the ORIGIN: a console blitting rows instead would leave it at zero, and would
|
||||||
|
# have moved 1,920 bytes to do the same thing.
|
||||||
|
{ printf '#Program\nstart:\n'
|
||||||
|
say "A"
|
||||||
|
for i in $(seq 1 25); do emit 10; done
|
||||||
|
show 0x34
|
||||||
|
say "B"
|
||||||
|
epilogue
|
||||||
|
} | run scrolled || exit 1
|
||||||
|
# COUNTED FROM THE FRONT, not the back: the emulator's own halt line follows whatever the
|
||||||
|
# program wrote, so the last byte of the file belongs to the machine rather than to the
|
||||||
|
# program. One 'A', twenty-five newlines, then the origin, which is byte 27. It is below 32
|
||||||
|
# so it goes to standard output without being drawn, and disturbs no pixel below.
|
||||||
|
GOT="$(head -c 27 "$BUILD/scrolled.out" | tail -c 1 | od -An -tu1 | tr -d ' ')"
|
||||||
|
[ "$GOT" = "1" ] \
|
||||||
|
&& result ok "the screen scrolls by moving a register" "the origin is 1, not 0" \
|
||||||
|
|| result no "the screen scrolls by moving a register" "the origin is $GOT"
|
||||||
|
inked scrolled 2 193 \
|
||||||
|
&& result ok "and the cursor stays on the bottom row" "the last line, row 24" \
|
||||||
|
|| result no "and the cursor stays on the bottom row" "nothing at 2,193"
|
||||||
|
papered scrolled 2 1 \
|
||||||
|
&& result ok "the row that came into view is clear" "not what was there a ring ago" \
|
||||||
|
|| result no "the row that came into view is clear" "something at 2,1"
|
||||||
|
|
||||||
|
# And what scrolled off the top is still in the map, which is scrollback nothing had to keep.
|
||||||
|
{ printf '#Program\nstart:\n'
|
||||||
|
say "A"
|
||||||
|
for i in $(seq 1 25); do emit 10; done
|
||||||
|
port 0x34 0x00
|
||||||
|
epilogue
|
||||||
|
} | run scrollback || exit 1
|
||||||
|
inked scrollback 2 1 \
|
||||||
|
&& result ok "what scrolled off is still there" "the origin went back and found it" \
|
||||||
|
|| result no "what scrolled off is still there" "nothing at 2,1"
|
||||||
|
|
||||||
echo
|
echo
|
||||||
if [ "$FAIL" -eq 0 ]; then
|
if [ "$FAIL" -eq 0 ]; then
|
||||||
echo "All $PASS video checks passed."
|
echo "All $PASS video checks passed."
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ OBJ_DIR = Object
|
|||||||
# SplitBit from Voyager is one file each: a terminal or a window. Anything that drifts out
|
# SplitBit from Voyager is one file each: a terminal or a window. Anything that drifts out
|
||||||
# of the shared list and into one of those is behaviour the other does not have, which is
|
# of the shared list and into one of those is behaviour the other does not have, which is
|
||||||
# the thing this split exists to prevent.
|
# the thing this split exists to prevent.
|
||||||
MACHINE_SRCS = machine.c io.c controller.c video.c utility.c cpu.c bootstrap.c assembly.c rom.c
|
MACHINE_SRCS = machine.c io.c controller.c video.c font.c utility.c cpu.c bootstrap.c assembly.c rom.c
|
||||||
EMU_SRCS = emulator.c $(MACHINE_SRCS)
|
EMU_SRCS = emulator.c $(MACHINE_SRCS)
|
||||||
VOY_SRCS = voyager.c $(MACHINE_SRCS)
|
VOY_SRCS = voyager.c $(MACHINE_SRCS)
|
||||||
ASM_SRCS = Assembler.c assembly.c firstPass.c Assm-util.c secondPass.c
|
ASM_SRCS = Assembler.c assembly.c firstPass.c Assm-util.c secondPass.c
|
||||||
|
|||||||
Reference in New Issue
Block a user