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

142 lines
5.6 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);
// ---- 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);
// ---- The cursor ----
//
// Drawn by the device rather than by whatever is presenting, because on a machine with a
// screen the cursor IS a hardware feature - a display controller blinks it from a counter,
// and one drawn by the window would not be in a picture the machine saved.
//
// It blinks on the machine's own clock, 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.
#define VIDEO_BLINK_CYCLES 500000
void videoSetCursor(int row, int column, int visible);
// The machine's clock, for anything that has to know time has passed.
void videoTick(unsigned long now);
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 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