Commit Graph
56 Commits
Author SHA1 Message Date
AnachronautandClaude Opus 5 d6feddd1b6 Give the console colour and a cursor
COLOUR COSTS A NIBBLE AND NO HARDWARE. A glyph is drawn in palette indices 0 and 1, paper
and ink, and a cell's attribute nibble adds sixteen to both - so sixteen banks is already
sixteen ink and paper pairs, and all that was missing was a register saying which one the
console draws in. That is port 0x06, read as well as written like the rest.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-29 08:13:28 -04:00
AnachronautandClaude Opus 5 43a05b3df1 Replace the escape parser with cursor registers
The console had grown an ANSI parser, and that was the wrong shape. ANSI exists because a
screen used to be on the other end of a serial line and a byte stream was the only channel
there was. This screen is memory the program can already address, so reaching it by sending
characters for a state machine to take apart is a middleman for something the machine does
better - and it meant accepting an open protocol somebody else defines, in hardware, with no
natural end to it. Everything else on this machine is registers.

So the console gets three: cursor row at 0x03, cursor column at 0x04, and a command port at
0x05 where 1 clears the screen. Both cursor registers are READ as well as written, which is
the thing an escape cannot do without sending a query and parsing a reply - a routine that
wants to put the cursor back where it found it can now ask.

Clearing is one command against a thousand cells walked one at a time. Snake and Life are
smaller for it: 2,168 bytes to 2,163 and 1,410 to 1,396.

A HOST TERMINAL STILL SPEAKS ANSI, and bridging to the host is the emulator's job, the same
job it does reading standard input. So the escapes are now GENERATED, outbound, for the set
this device chooses, rather than parsed inbound as though the machine were a terminal. The
set cannot grow behind our backs because we are the ones saying it. The cursor is announced
lazily, at the next character rather than at the register write, so setting a row and a
column costs one sequence rather than two.

The console's block widens from three ports to six, which registryTest noticed: it had been
asking about port 0x05 precisely BECAUSE nothing was there, and the console had just moved
in. Re-blessing it would have left it checking nothing, so it asks about 0x80 instead -
clear of the console, the disk, the screen, the controller, and the sound device coming to
0x40.

Six checks in Tests/video.sh swapped from the sequences to the registers, including that the
cursor reads back and that one sent past the edge is clamped rather than refusing. Those
checks also stopped counting bytes from the ends of a file, which had quietly started
measuring an escape the moment the console began announcing the cursor.

SplitLint caught the one thing worth catching in the port: the clear command leaves A at 1
and key mode is also 1, so the second load looks redundant. Acting on it would tie a console
command to a console mode by coincidence, and break silently if either ever moved, so it is
suppressed with that reason rather than removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-28 23:08:52 -04:00
AnachronautandClaude Opus 5 556a14b288 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
2026-08-28 22:43:27 -04:00
AnachronautandClaude Opus 5 6f8ad42277 Fill the window, let it be resized, and make black black
Three things a person looking at a real screen found in five minutes, none of which the
headless tests could have seen.

THE PICTURE FILLED A QUARTER OF THE WINDOW. The window opened at the largest screen the
device can make, doubled, and then drew a 320 by 200 mode at that same doubling - so three
quarters of it was bezel. It now takes the largest whole-number scale that fits: the two
modes are exactly a factor of two apart and the window opens at twice the larger, so both
fill it exactly, at four and at two. Changing mode changes how sharp the screen is rather
than how big it is.

Whole numbers on purpose. A 320 by 200 picture stretched by 2.7 has some rows twice as tall
as their neighbours, which on eight pixel glyphs is the difference between text and mush.

THE WINDOW WOULD NOT RESIZE. It does now, and the picture rescales to whatever it becomes,
still in whole pixels and still centred. How big somebody wants a screen is not the
machine's business.

AND BLACK WAS NOT BLACK. Both the paper and the bezel were tinted towards green, on the
theory that a phosphor never was neutral. On a real screen that reads as a fault rather
than as character: a background that is nearly black looks like a background that failed to
be black. Paper is black now and ink is a neutral grey, because a default should be the
unsurprising thing - anything with a point of view about colour is 254 palette entries away
and belongs to a program. The bezel is a clearly lighter grey, so what is left over when
the window's shape does not match the picture's looks like a bezel rather than like more
screen.

The two checks that name the waking colours name the new ones. That they had to change is
the check working: they say what they depend on rather than assuming it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-28 22:32:27 -04:00
AnachronautandClaude Opus 5 773b0f8add 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
2026-08-28 22:26:27 -04:00
AnachronautandClaude Opus 5 83623a3df3 Give the Voyager a screen
A tile engine on ports 0x30 to 0x3F, bringing one bank of video memory registered the way
the disk's buffer is. The CPU writes cell indices and the device turns them into pixels,
which is the whole reason a screen is affordable at a megahertz: a frame is 16,667 cycles,
a full 320 by 200 picture is 64,000 bytes, and a 40 by 25 map is 2,000. A program that
changes two cells writes four bytes. The cost of a screen becomes the number of cells that
changed rather than the number of pixels on it.

Which makes colour depth free, so the tiles are eight bits: an 8 by 8 cell is 64 pixels and
each picks independently out of 256 colours, with no per-cell limit of the kind that made a
Spectrum two and C64 multicolour four. The low nibble of a cell's attribute is ADDED to
every index in its tile, sixteen at a time, so a tile drawn in 0 to 15 appears in any of
sixteen schemes without a second copy in tile memory - and a tile wanting all 256 leaves the
nibble at zero and gets them. Neither use costs the other anything.

Two decisions are arithmetic rather than taste, and both come from the machine having no
multiply. A map row is a page whether the mode fills it or not, so a cell address is the row
number as the high byte and the doubled column as the low byte with no arithmetic at all;
otherwise every cursor move on a 40 column screen would cost a row-times-40 in software. And
a palette entry is four bytes rather than three, so entry n is at n times four, a shift.

THE MAP IS A RING and the Scroll register says which of its 128 rows is on top. Scrolling
moves a register and no memory: blitting a 40 by 25 screen up one line is 1,920 bytes inside
one bank, which is twelve percent of a frame even with the controller widened, and a program
printing one page would spend six frames shuffling memory. It is now one port write - and
the rows that scrolled off are still there, which is where a terminal gets scrollback it
never had.

The device is part of the machine rather than part of the window. It renders into a buffer
that is a pure function of video memory, so the same program draws the same picture with
nobody watching; Voyager puts that buffer on the glass and decides nothing. Both binaries
take --screen, which saves a PPM when the machine stops, and that is what makes a screen
checkable on a host with no display at all.

Tests/video.sh checks fourteen named behaviours rather than comparing a recorded image,
because a recorded image would say "something changed" and leave which of the palette, the
tile, the attribute, the map or the scroll register broke to be found by hand. Verified by
breaking three things in turn: the additive nibble failed exactly one check, the scroll
origin exactly two, and moving every cell one pixel sideways exactly the four about
placement.

Tests/docs.sh could not count past nine, which is how a suite of ten scripts reported
itself as wrong for the wrong reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-28 22:11:13 -04:00