Files
SplitBit-Emulator/Source/Emulator/video.h
T
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

105 lines
3.9 KiB
C

// video.h
// The Voyager's video device.
// Written by Anachronaut
#ifndef VIDEO_H
#define VIDEO_H
#include <stdint.h>
// ---- What this is ----
//
// A tile engine. The CPU writes cell indices and the device expands them into pixels, which
// is the difference between a screen costing 2,000 bytes a frame and 64,000 - and at a
// megahertz that is the difference between a screen and no screen at all.
//
// It follows that COLOUR DEPTH IS FREE AT FRAME TIME. The map is the same size whether the
// tiles behind it are one bit deep or eight, because the depth lives in tile memory, which
// is written once when a program loads and not sixty times a second. So the tiles are eight
// bits: an 8x8 cell is 64 pixels and each one picks independently out of 256 colours, with
// no per-cell limit of the kind that made a Spectrum two and C64 multicolour four.
//
// ---- The device brings memory ----
//
// One bank, registered the way the disk's buffer is, so it costs a program nothing in Data
// Memory and keeps what is in it between frames. A program blits the region that changed
// and the rest stays as it was, which is the whole reason this is a bank rather than a
// window onto a port.
#define VIDEO_MEMORY_BYTES 0x10000
// Tile memory: 256 tiles of 8x8, one byte a pixel.
#define VIDEO_TILE_BASE 0x0000
#define VIDEO_TILE_BYTES 64
#define VIDEO_TILE_COUNT 256
// ---- The map, one page a row ----
//
// A row is padded to exactly 256 bytes whether the mode uses all of it or not, and that is
// not waste, it is arithmetic. THE MACHINE HAS NO MULTIPLY. On a 40 column screen every
// cursor move would otherwise need row times 40 in software, which is a tax on the most
// common operation in the whole system. At a page a row the address needs no arithmetic at
// all: the row number IS the high byte and the doubled column IS the low byte.
//
// It also frees the geometry from having to be a power of two, which is what lets the
// pixel resolution be whatever looks right.
#define VIDEO_MAP_BASE 0x4000
#define VIDEO_MAP_STRIDE 256
#define VIDEO_MAP_ROWS 128
#define VIDEO_MAP_COLUMNS (VIDEO_MAP_STRIDE / 2)
// Two bytes to a cell: which tile, and how to colour it.
#define VIDEO_CELL_BYTES 2
// ---- The palette ----
//
// Four bytes an entry rather than three, for the same reason a map row is a page: entry n
// begins at n times four, which is a shift. Three would need a multiply the machine does
// not have. The fourth byte is unused and reads as whatever was put there.
#define VIDEO_PALETTE_BASE 0xC000
#define VIDEO_PALETTE_BYTES 4
#define VIDEO_PALETTE_SIZE 256
// ---- Modes ----
//
// Both are 8x8 cells over the same engine; only how many of them differ. The pixel count
// costs the CPU nothing, because it only ever writes the map - which is why the larger mode
// is affordable at all.
#define VIDEO_MODE_40x25 0
#define VIDEO_MODE_80x50 1
#define VIDEO_MODE_COUNT 2
#define VIDEO_CELL_PIXELS 8
#define VIDEO_MAX_WIDTH (80 * VIDEO_CELL_PIXELS)
#define VIDEO_MAX_HEIGHT (50 * VIDEO_CELL_PIXELS)
// ---- Ports ----
//
// Sixteen, like the controller, and it interrupts on its base the way the disk established.
// Nothing interrupts yet; the frame interrupt is the next rung.
#define VIDEO_STATUS 0x30
#define VIDEO_MODE 0x31
#define VIDEO_COLUMNS 0x32
#define VIDEO_ROWS 0x33
#define VIDEO_SCROLL 0x34
void videoReset(void);
uint8_t *videoMemory(uint32_t *capacity);
uint8_t videoWrite(uint8_t value, uint8_t port);
uint8_t videoRead(uint8_t port);
// Turns what is in video memory into pixels. A pure function of that memory, so the same
// contents give the same picture with nobody watching - which is what lets the suite check
// a screen on a machine that has no display.
void videoRender(void);
// The pixels the last render produced, three bytes each, red then green then blue.
const uint8_t *videoPixels(int *width, int *height);
// Renders and writes a binary PPM. Returns 0 if it worked.
int videoWriteImage(const char *path);
#endif // VIDEO_H