Teach the console the sequences the corpus already speaks, and let the status port see the window
Three things Snake found the moment somebody ran it in a window, and all three are the same kind of mistake: the console grew a screen and kept asking the terminal. IT COULD NOT CLEAR THE SCREEN. Every program here that moves a cursor does it with ANSI escapes, because until there was a screen the thing on the other end was somebody's terminal. The controller drew "[2J" as three letters and left the board underneath. It now parses them, which is what a video terminal did - a VT100 is exactly this. The whole corpus uses two, ESC[2J and ESC[H, and the general shape is recognised so anything else is swallowed rather than drawn: a sequence nobody implemented should leave no marks. Cursor positioning is in too, since it is the same parse and one line more. IT DID NOT SEE KEYS FROM THE WINDOW, but did when the terminal behind it was focused, which is the whole diagnosis in one sentence. Snake polls the READY bit and never blocks, and consoleFetch - what the status port asks - was polling standard input regardless of whether a front end had installed a hook. So a window's keys were invisible to every program that looks before it reads, and a keystroke aimed at the terminal would be picked up instead. The hook now takes a question. Zero is the status port looking, and must not present or sleep: a program polling in a loop would otherwise be charged a frame for every glance. One is the data port blocking, where presenting is exactly right, because a machine waiting for a key is still a machine somebody is looking at. One value for both would have made either polling ruinous or waiting dead. AND IT RAN SLOWLY, which was the same bug wearing a hat: a game that never receives a steering key is a game that only ever goes one way. Six more checks in Tests/video.sh, to 32: that ESC[2J clears, that ESC[H goes to the corner without disturbing what is drawn, that ESC[3;5H counts rows and columns from one, and that an unknown sequence is swallowed and leaves nothing behind. The hook itself is still the one thing here the suite cannot reach - it exists only when there is a window, and this host has no display. It was found by a person playing Snake, which is where the Test Manual says these go on being found. 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
6f8ad42277
commit
556a14b288
+113
-3
@@ -232,7 +232,100 @@ static void consoleNewLine(void) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- The sequences this machine already speaks ----
|
||||||
|
//
|
||||||
|
// Every program in the corpus that moves a cursor does it with ANSI escapes, because until
|
||||||
|
// now the thing on the other end was somebody's terminal. A display controller that did not
|
||||||
|
// understand them would draw "[2J" on the screen and leave the board underneath it, which is
|
||||||
|
// exactly what happened the first time Snake was run in a window.
|
||||||
|
//
|
||||||
|
// So the controller parses them, the way a video terminal did - that is what a VT100 was.
|
||||||
|
// The whole corpus uses two, ESC[2J and ESC[H, and the general shape is recognised so that
|
||||||
|
// anything else is SWALLOWED RATHER THAN DRAWN: a sequence nobody implemented should leave
|
||||||
|
// no marks, which is what a real terminal does with one it does not know.
|
||||||
|
#define SEQUENCE_PARAMS 2
|
||||||
|
|
||||||
|
static enum { DRAW_TEXT, DRAW_SAW_ESCAPE, DRAW_IN_SEQUENCE } drawState = DRAW_TEXT;
|
||||||
|
static int sequenceParam[SEQUENCE_PARAMS];
|
||||||
|
static int sequenceParams;
|
||||||
|
|
||||||
|
static void consoleClearScreen(int fromCursor) {
|
||||||
|
const int rows = videoRows();
|
||||||
|
const int columns = videoColumns();
|
||||||
|
for (int row = fromCursor ? cursorRow : 0; row < rows; row++) {
|
||||||
|
const int from = (fromCursor && row == cursorRow) ? cursorColumn : 0;
|
||||||
|
for (int column = from; column < columns; column++) {
|
||||||
|
videoPutCell(row, column, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns 1 if the byte was part of a sequence and so is not a character to draw.
|
||||||
|
static int consoleSequence(uint8_t byte) {
|
||||||
|
switch (drawState) {
|
||||||
|
case DRAW_TEXT:
|
||||||
|
if (byte != 0x1B) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
drawState = DRAW_SAW_ESCAPE;
|
||||||
|
return 1;
|
||||||
|
case DRAW_SAW_ESCAPE:
|
||||||
|
// Only the bracket form. An escape followed by anything else is not a sequence
|
||||||
|
// this machine has ever sent, and swallowing the escape alone is enough.
|
||||||
|
drawState = DRAW_TEXT;
|
||||||
|
if (byte == '[') {
|
||||||
|
drawState = DRAW_IN_SEQUENCE;
|
||||||
|
sequenceParams = 0;
|
||||||
|
sequenceParam[0] = 0;
|
||||||
|
sequenceParam[1] = 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
case DRAW_IN_SEQUENCE:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (byte >= '0' && byte <= '9') {
|
||||||
|
if (sequenceParams < SEQUENCE_PARAMS) {
|
||||||
|
sequenceParam[sequenceParams] = sequenceParam[sequenceParams] * 10 + (byte - '0');
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (byte == ';') {
|
||||||
|
if (sequenceParams < SEQUENCE_PARAMS - 1) {
|
||||||
|
sequenceParams++;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
drawState = DRAW_TEXT;
|
||||||
|
switch (byte) {
|
||||||
|
case 'J':
|
||||||
|
// 2 is the whole screen, which is the one the corpus uses. Without a number it
|
||||||
|
// is from the cursor down, which is what the standard says and costs nothing.
|
||||||
|
consoleClearScreen(sequenceParam[0] != 2);
|
||||||
|
break;
|
||||||
|
case 'H': case 'f': {
|
||||||
|
// Row then column, one-based on the wire and zero-based here. Missing or zero
|
||||||
|
// means one, which is what makes a bare ESC[H the corner.
|
||||||
|
int row = sequenceParam[0];
|
||||||
|
int column = (sequenceParams >= 1) ? sequenceParam[1] : 0;
|
||||||
|
if (row < 1) row = 1;
|
||||||
|
if (column < 1) column = 1;
|
||||||
|
cursorRow = row - 1;
|
||||||
|
cursorColumn = column - 1;
|
||||||
|
if (cursorRow >= videoRows()) cursorRow = videoRows() - 1;
|
||||||
|
if (cursorColumn >= videoColumns()) cursorColumn = videoColumns() - 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Recognised as a sequence and not implemented, so it leaves no marks.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
static void consoleDraw(uint8_t byte) {
|
static void consoleDraw(uint8_t byte) {
|
||||||
|
if (consoleSequence(byte)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (byte) {
|
switch (byte) {
|
||||||
case '\n':
|
case '\n':
|
||||||
consoleNewLine();
|
consoleNewLine();
|
||||||
@@ -266,9 +359,9 @@ static void consoleDraw(uint8_t byte) {
|
|||||||
// stop drawing and stop answering. So a front end with a window installs a hook: called
|
// 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
|
// 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.
|
// when one is typed, or -1 to say the window has gone.
|
||||||
static int (*inputHook)(void) = NULL;
|
static int (*inputHook)(int mayWait) = NULL;
|
||||||
|
|
||||||
void consoleSetInputHook(int (*hook)(void)) {
|
void consoleSetInputHook(int (*hook)(int mayWait)) {
|
||||||
inputHook = hook;
|
inputHook = hook;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +379,7 @@ uint8_t consoleReadByte(void) {
|
|||||||
consoleShowWhatIsWritten();
|
consoleShowWhatIsWritten();
|
||||||
if (inputHook != NULL) {
|
if (inputHook != NULL) {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
int got = inputHook();
|
int got = inputHook(1);
|
||||||
if (got >= 0) {
|
if (got >= 0) {
|
||||||
// ---- The screen is the terminal now ----
|
// ---- The screen is the terminal now ----
|
||||||
//
|
//
|
||||||
@@ -348,6 +441,23 @@ static void consoleFetch(void) {
|
|||||||
// just as entitled to have the drawing appear, and it never reaches the read that
|
// just as entitled to have the drawing appear, and it never reaches the read that
|
||||||
// would otherwise have flushed for it.
|
// would otherwise have flushed for it.
|
||||||
consoleShowWhatIsWritten();
|
consoleShowWhatIsWritten();
|
||||||
|
// ---- Where a byte comes from when there is no standard input ----
|
||||||
|
//
|
||||||
|
// A window's keys arrive through the hook, and THE STATUS PORT HAS TO ASK IT TOO. It did
|
||||||
|
// not, so a program polling READY in a window was asking a standard input nobody was
|
||||||
|
// typing at: Snake saw no keys at all, and would suddenly see one if the terminal behind
|
||||||
|
// the window happened to be focused. Asked without waiting, because a poll is a poll -
|
||||||
|
// the front end presents a frame when the console genuinely blocks, not when it looks.
|
||||||
|
if (inputHook != NULL) {
|
||||||
|
int got = inputHook(0);
|
||||||
|
if (got >= 0) {
|
||||||
|
consolePushback = got;
|
||||||
|
consoleAnnounce();
|
||||||
|
} else if (got == CONSOLE_GONE) {
|
||||||
|
consoleEnded = 1;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
|
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
|
||||||
if (poll(&waiting, 1, 0) <= 0 || (waiting.revents & (POLLIN | POLLHUP)) == 0) {
|
if (poll(&waiting, 1, 0) <= 0 || (waiting.revents & (POLLIN | POLLHUP)) == 0) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -147,7 +147,12 @@ void consoleHome(void);
|
|||||||
// Without a hook the console reads standard input, which is what it has always done.
|
// Without a hook the console reads standard input, which is what it has always done.
|
||||||
#define CONSOLE_NOTHING_YET (-1)
|
#define CONSOLE_NOTHING_YET (-1)
|
||||||
#define CONSOLE_GONE (-2)
|
#define CONSOLE_GONE (-2)
|
||||||
void consoleSetInputHook(int (*hook)(void));
|
//
|
||||||
|
// mayWait says which question is being asked. Zero is the status port looking, and must not
|
||||||
|
// present or sleep: a program polling in a loop would otherwise run at the frame rate. One
|
||||||
|
// is the data port blocking, where presenting is exactly right, because a machine waiting
|
||||||
|
// for a key is still a machine somebody is looking at.
|
||||||
|
void consoleSetInputHook(int (*hook)(int mayWait));
|
||||||
|
|
||||||
// ---- Device classes ----
|
// ---- Device classes ----
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -113,12 +113,22 @@ static void presentFrame(void) {
|
|||||||
// What the console asks while it is waiting. Presenting from in here is what keeps the
|
// 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
|
// window answering, and EndDrawing paces it, so waiting for a key costs a frame rather
|
||||||
// than a spin.
|
// than a spin.
|
||||||
static int voyagerKey(void) {
|
static int voyagerKey(int mayWait) {
|
||||||
if (!windowOpen || WindowShouldClose()) {
|
if (!windowOpen) {
|
||||||
|
return CONSOLE_GONE;
|
||||||
|
}
|
||||||
|
if (mayWait) {
|
||||||
|
if (WindowShouldClose()) {
|
||||||
windowOpen = 0;
|
windowOpen = 0;
|
||||||
return CONSOLE_GONE;
|
return CONSOLE_GONE;
|
||||||
}
|
}
|
||||||
|
// Presenting is what keeps the window answering while the machine waits, and
|
||||||
|
// EndDrawing paces it, so waiting for a key costs a frame rather than a spin.
|
||||||
presentFrame();
|
presentFrame();
|
||||||
|
}
|
||||||
|
// Asked without waiting, this takes whatever the last frame's event poll left behind
|
||||||
|
// and returns at once. A program polling the status port sixty times between frames
|
||||||
|
// must not be charged a frame for each look.
|
||||||
int character = GetCharPressed();
|
int character = GetCharPressed();
|
||||||
if (character > 0 && character < 128) {
|
if (character > 0 && character < 128) {
|
||||||
return character;
|
return character;
|
||||||
|
|||||||
@@ -589,6 +589,19 @@ 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.
|
||||||
|
|
||||||
|
**And it understands the escape sequences this machine already sends.** Every program here that moves a cursor does it with ANSI escapes, because until there was a screen the thing on the other end was somebody's terminal. A controller that ignored them would draw `[2J` on the screen and leave the picture underneath it, so it parses them - which is what a video terminal did.
|
||||||
|
|
||||||
|
| Sequence | Does |
|
||||||
|
| --- | --- |
|
||||||
|
| `ESC [ 2 J` | Clears the whole screen. The cursor does not move. |
|
||||||
|
| `ESC [ J` | Clears from the cursor to the end of the screen. |
|
||||||
|
| `ESC [ H` | Puts the cursor in the corner. |
|
||||||
|
| `ESC [ row ; column H` | Puts the cursor there, counting from one. |
|
||||||
|
|
||||||
|
Anything else in that shape - an escape, a bracket, some numbers, a letter - is recognised and **swallowed rather than drawn**. A sequence nobody implemented should leave no marks, which is what a terminal does with one it does not know, and drawing it would be worse than ignoring it.
|
||||||
|
|
||||||
|
Clearing does not touch the scrollback. It clears what is on the screen, and what has already gone off the top is still in the map where the Scroll register can find it.
|
||||||
|
|
||||||
**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.
|
**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.
|
**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.
|
||||||
|
|||||||
@@ -347,6 +347,43 @@ papered scrolled 2 1 \
|
|||||||
&& result ok "the row that came into view is clear" "not what was there a ring ago" \
|
&& 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"
|
|| result no "the row that came into view is clear" "something at 2,1"
|
||||||
|
|
||||||
|
# ---- The sequences the corpus already speaks ----
|
||||||
|
#
|
||||||
|
# Every program here that moves a cursor does it with ANSI escapes, because until there was
|
||||||
|
# a screen the thing on the other end was somebody's terminal. A controller that did not
|
||||||
|
# understand them drew "[2J" on the screen and left the board underneath, which is what Snake
|
||||||
|
# did the first time it was run in a window.
|
||||||
|
|
||||||
|
{ printf '#Program\nstart:\n'; say "A"; emit 27; say "[2J"; epilogue; } | run clearscreen || exit 1
|
||||||
|
papered clearscreen 2 1 \
|
||||||
|
&& result ok "ESC[2J clears the screen" "the letter is gone" \
|
||||||
|
|| result no "ESC[2J clears the screen" "still inked at 2,1"
|
||||||
|
|
||||||
|
{ printf '#Program\nstart:\n'; emit 10; emit 10; say "A"; emit 27; say "[H"; say "A"
|
||||||
|
epilogue
|
||||||
|
} | run home || exit 1
|
||||||
|
inked home 2 1 \
|
||||||
|
&& result ok "ESC[H goes back to the corner" "the second letter is at row 0" \
|
||||||
|
|| result no "ESC[H goes back to the corner" "nothing at 2,1"
|
||||||
|
inked home 2 17 \
|
||||||
|
&& result ok "and leaves what was drawn alone" "the first is still on row 2" \
|
||||||
|
|| result no "and leaves what was drawn alone" "nothing at 2,17"
|
||||||
|
|
||||||
|
{ printf '#Program\nstart:\n'; emit 27; say "[3;5H"; say "A"; epilogue; } | run position || exit 1
|
||||||
|
inked position 34 17 \
|
||||||
|
&& result ok "ESC[3;5H puts the cursor there" "row 3, column 5, counting from one" \
|
||||||
|
|| result no "ESC[3;5H puts the cursor there" "nothing at 34,17"
|
||||||
|
|
||||||
|
# A sequence nobody implemented should leave no marks, which is what a real terminal does
|
||||||
|
# with one it does not know. Drawing it would be worse than ignoring it.
|
||||||
|
{ printf '#Program\nstart:\n'; emit 27; say "[9m"; say "A"; epilogue; } | run unknownseq || exit 1
|
||||||
|
inked unknownseq 2 1 \
|
||||||
|
&& result ok "an unknown sequence is swallowed" "the letter after it is at cell 0" \
|
||||||
|
|| result no "an unknown sequence is swallowed" "nothing at 2,1"
|
||||||
|
papered unknownseq 10 1 \
|
||||||
|
&& result ok "and leaves nothing behind" "cell 1 is untouched" \
|
||||||
|
|| result no "and leaves nothing behind" "something at 10,1"
|
||||||
|
|
||||||
# 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"
|
||||||
|
|||||||
Reference in New Issue
Block a user