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
This commit is contained in:
Anachronaut
2026-08-28 22:11:13 -04:00
co-authored by Claude Opus 5
parent e3ef25e3b3
commit 83623a3df3
14 changed files with 690 additions and 16 deletions
+58
View File
@@ -509,9 +509,67 @@ If nothing is installed for the vector a device refused with, the machine stops
| 0x20 - 0x23 | The disk. See Storage. It interrupts on 0x20, its base port. | 0x13 |
| 0x13 | The machine itself. Writing 1 asks it to start over: whatever put the first instruction in memory does it again, and the CPU begins where the boot vector points. A port rather than a service, because a reset has to work when the system does not - and a program that owns the whole machine has no system to ask. The disk is not unplugged and keeps what was written to it; the vector table is cleared, because a handler left behind would aim an interrupt into a program that is no longer running. | 0x04 |
| 0x12 | A device that owns 256 bytes of memory. Writing to its port fills that memory with the byte written, standing in for a disk controller reading a sector. Its memory is unreachable until it is registered as a bank. | 0x12 |
| 0x30 - 0x3F | The screen. See The Screen. It brings video memory, which is unreachable until it is registered as a bank. | 0x14 |
| 0xE0 - 0xEF | The memory controller. See The Memory Controller. | 0x03 |
| 0xFF | The bus registry. See Asking What Is There. | 0x01 |
## The Screen:
A tile engine, on ports 0x30 to 0x3F. The CPU writes cell indices and the device turns them into pixels.
That indirection is the whole reason a screen is affordable here. At a megahertz a frame is 16,667 cycles, and pushing a full 320 by 200 picture a byte at a time is 64,000 bytes - four frames of work for one frame of screen. A 40 by 25 map is 2,000 bytes, and a program that changes two cells writes four. **The cost of a screen becomes the number of cells that changed rather than the number of pixels on it.**
It follows that colour depth is free. The map is the same size whatever is behind it, so the tiles are eight bits deep: an 8 by 8 cell is 64 pixels and each one picks independently out of 256 colours. There is no limit of two to a cell, or four, or sixteen.
### Video Memory:
One bank, brought by the device and reached only through the memory controller, like the disk's buffer. It keeps what is in it between frames, so a program writes the part that changed and the rest stays as it was.
| Address | Holds |
| --- | --- |
| 0x0000 - 0x3FFF | Tile memory. 256 tiles of 8 by 8, one byte a pixel, so tile n begins at n times 64. |
| 0x4000 - 0xBFFF | The map. 128 rows of 256 bytes. |
| 0xC000 - 0xC3FF | The palette. 256 entries of four bytes: red, green, blue, and one unused. |
**A map row is a page whether the mode fills it or not**, and that is arithmetic rather than waste. This machine has no multiply, so on a 40 column screen every cursor move would otherwise cost a `row times 40` in software - a tax on the most common operation in the system. At a page a row there is no arithmetic at all: the row number is the high byte of the address and the doubled column is the low byte.
A palette entry is four bytes for the same reason. Entry n begins at n times four, which is a shift; three bytes would need a multiply.
### Cells:
Two bytes. The first says which tile, the second how to colour it.
The low nibble of the second byte is **added to every palette index in the tile, sixteen at a time**. A tile drawn in indices 0 to 15 therefore appears in any of sixteen colour schemes without a second copy of it in tile memory. A tile that wants all 256 colours leaves the nibble at zero and gets them. The addition wraps, because a byte plus a byte is a byte.
The high nibble is reserved and should be left at zero, so that a meaning can be given to it later without changing what already-written programs mean.
### Registers:
| Port | Register |
| --- | --- |
| 0x30 | Status. Reserved for the frame interrupt, and reads zero until there is one. |
| 0x31 | Mode. |
| 0x32 | Columns, read only. |
| 0x33 | Rows, read only. |
| 0x34 | Scroll. |
| Mode | Screen | Cells |
| --- | --- | --- |
| 0 | 320 by 200 | 40 by 25 |
| 1 | 640 by 400 | 80 by 50 |
Both are 8 by 8 cells over the same engine, and the pixel count costs a program nothing, because it only ever writes the map. A mode that does not exist is not taken, and is not a fault either: a screen is a poor place to stop the machine, and a program that asked for something impossible still has the screen it had.
How big the screen is, is asked for rather than assumed. A program written once can find out what it is running on.
### Scrolling:
**The map is a ring, and the Scroll register says which of its 128 rows is drawn at the top.** Screen row *r* shows map row *scroll + r*, wrapped.
Scrolling therefore moves a register and no memory at all. That is not a small saving. Moving a 40 by 25 screen up one line is 1,920 bytes inside one bank, which is 1,920 cycles even with the controller widened - twelve percent of a frame, for one line. A program printing a single page would spend six frames shuffling memory. Here it is one write to a port.
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.
## 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.