videoReset zeroed video memory and then wrote the font and the sixteen colour schemes into it, and the comment above that said out loud what was wrong with it: "everything here is ordinary video memory". RAM does not wake up with anything in it. That was the last piece of magic in this device, and it looked harmless until something wanted the font BACK - a program that redefines a glyph had destroyed the only copy there was. So the device has a character generator, the way the machines this one is pretending to be really did, and the copy into RAM is a thing it DOES rather than a state it mysteriously starts in. Command port 0x39: bit 0 for the font, bit 1 for the schemes. THE RAM IS STILL RAM. A program may overwrite every glyph and every colour and should be able to, which is what makes this a tile engine rather than a text display. What changed is that it is no longer a one way door. NEITHER COMMAND CLEARS WHAT IT DOES NOT OWN. The font used to clear the whole of tile memory before writing itself, which was harmless while it happened only at reset and is wrong the moment a program can ask: a program that defined a tile of its own and then wanted its text back would have paid for it with the tile. The reason it is a chip rather than a file on the disk, which was the other candidate: the boot chain prints before CosmOS exists. Stage one prints "?" when there is nothing to boot, and if the font came off the disk then the message about the disk having failed would be the one thing that could not be drawn. A system that wants its own font still loads one over the top - the ROM is the floor, not the policy. Two things that had been worked around now simply work. The shell asks for both whenever a program exits, so a program that redefined a letter no longer leaves it unable to spell, and Grid no longer needs to have saved the screen to avoid handing back green text on blue. And the fault screen asks for the glyphs first, because a message spelled in somebody's tile graphics is no message at all. Five video checks. Two runs each for the font and the schemes, since the map holds a tile NUMBER and the glyph is looked up when the frame is drawn - so restoring changes every cell using it, including ones drawn before, and what the two runs differ by is the command. The third guards the decision not to clear: tile 200 has to survive the font coming back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
245 lines
11 KiB
C
245 lines
11 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
|
|
|
|
// ---- A bitmap, over the top of the tiles and the map ----
|
|
//
|
|
// THE SAME MEMORY MEANING DIFFERENT THINGS IN DIFFERENT MODES, which is what shared video
|
|
// memory has always been. There is no room for it to be anywhere else: 320 by 200 at a byte
|
|
// a pixel is 64,000 bytes and the whole bank is 65,536, so a bitmap that sat beside the
|
|
// tiles rather than on top of them would need a second bank for no reason except tidiness.
|
|
//
|
|
// What it costs is that the two do not coexist. Going to bitmap mode does not clear the text
|
|
// screen; it stops calling it a text screen. Coming back finds the tiles and the map holding
|
|
// whatever the picture put there, which is what taking the screen means.
|
|
#define VIDEO_BITMAP_BASE 0x0000
|
|
#define VIDEO_BITMAP_WIDTH 320
|
|
#define VIDEO_BITMAP_HEIGHT 200
|
|
|
|
// ---- 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.
|
|
//
|
|
// At the TOP of video memory, clear of everything else, because it is the one thing that has
|
|
// to mean the same in every mode - a bitmap needs colours as much as a tile does, and 64,000
|
|
// bytes of picture leaves nowhere in the middle for it to hide.
|
|
#define VIDEO_PALETTE_BASE 0xFC00
|
|
#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_BITMAP 2
|
|
#define VIDEO_MODE_COUNT 3
|
|
|
|
#define VIDEO_CELL_PIXELS 8
|
|
#define VIDEO_MAX_WIDTH (80 * VIDEO_CELL_PIXELS)
|
|
#define VIDEO_MAX_HEIGHT (50 * VIDEO_CELL_PIXELS)
|
|
|
|
// How many characters across and down the screen is, and ZERO IN BITMAP MODE, where there is
|
|
// no such thing. The console asks, and a console told there are no columns has nowhere to
|
|
// put a glyph and does not try.
|
|
int videoTextRows(void);
|
|
|
|
// ---- 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
|
|
#define VIDEO_CONTROL 0x35
|
|
|
|
// ---- The other three quarters of scrolling ----
|
|
//
|
|
// 0x34 moves the view a whole cell at a time and only downwards, which is a scrolling text
|
|
// screen and not a scrolling picture. These are the rest of it: a column origin so the map
|
|
// can be wider than the screen as well as taller, and a pixel remainder for each axis so the
|
|
// step can be one pixel rather than eight.
|
|
//
|
|
// COARSE AND FINE DO NOT CARRY INTO EACH OTHER. Fine is the low three bits of what is
|
|
// written and nothing else, so a program that scrolls past a cell edge advances the coarse
|
|
// register itself. That is what the machines this one is pretending to be did, it keeps each
|
|
// register meaning exactly one thing, and it means a program always knows where it is
|
|
// without reading anything back off the screen.
|
|
#define VIDEO_SCROLL_COLUMN 0x36
|
|
#define VIDEO_FINE_X 0x37
|
|
#define VIDEO_FINE_Y 0x38
|
|
|
|
// ---- The character generator, which is a chip and not a memory that remembers ----
|
|
//
|
|
// The font and the sixteen colour schemes used to be WRITTEN INTO VIDEO RAM at reset, and
|
|
// that was the one piece of magic left in this device: RAM does not wake up with anything
|
|
// in it. It looked harmless until something needed the font BACK - a program that redefines
|
|
// a tile has overwritten a glyph, and there was nowhere to get it from, because the only
|
|
// copy was the one that had just been drawn over.
|
|
//
|
|
// So the device has a ROM, the way the machines this one is pretending to be really did,
|
|
// and the copy into RAM is a thing it DOES rather than a state it mysteriously starts in.
|
|
// Reset performs it, and a program can ask for it again.
|
|
//
|
|
// THE RAM IS STILL RAM. A program may overwrite every glyph and every colour, and should be
|
|
// able to: that is what makes a tile engine a tile engine rather than a text display. What
|
|
// has changed is that doing so is no longer a one way door.
|
|
//
|
|
// A SYSTEM THAT WANTS ITS OWN FONT still loads one over the top. This is the floor, not the
|
|
// policy - it is what makes a machine with no disk able to say so, and what lets a program
|
|
// with no system behind it put readable text on a screen.
|
|
#define VIDEO_COMMAND 0x39
|
|
// Copy the 135 glyphs back into the tiles they live in, leaving every other tile alone.
|
|
#define VIDEO_COMMAND_FONT 0x01
|
|
// Copy the sixteen ink and paper pairs back, leaving the rest of the palette alone.
|
|
#define VIDEO_COMMAND_PALETTE 0x02
|
|
|
|
// Eight pixels to a cell, so three bits say where inside one the view begins.
|
|
#define VIDEO_FINE_MASK 0x07
|
|
|
|
// ---- The frame ----
|
|
//
|
|
// A screen finishes drawing sixty times a second and then has a moment before it starts
|
|
// again, and that moment is the one safe time to change what it is drawing. It is also the
|
|
// only regular beat this machine has: there is no clock, and every program that wanted to
|
|
// happen at a certain speed has until now counted instructions and hoped.
|
|
//
|
|
// Sixty a second at a megahertz. On the MACHINE'S clock rather than the host's, so a program
|
|
// runs the same number of frames in the same number of cycles however fast anything really
|
|
// went - which is what makes a frame something a test can count.
|
|
#define VIDEO_FRAME_CYCLES 16667
|
|
|
|
// Set when a frame has gone by, and cleared by reading the status port. A program with no
|
|
// handler installed can wait on this instead, the way a program can poll the console rather
|
|
// than being interrupted by it.
|
|
#define VIDEO_STATUS_FRAME 0x01
|
|
// Whether the screen is set to interrupt, so that a program can ask what it asked for.
|
|
#define VIDEO_STATUS_INTERRUPT 0x02
|
|
|
|
// Asks to be interrupted at each frame, on hardware vector 0x30. OFF WHEN THE MACHINE
|
|
// STARTS, because an interrupt with nothing installed to catch it is a fault, and a machine
|
|
// that began interrupting the moment it was switched on would take any program that had not
|
|
// thought about frames down with it.
|
|
#define VIDEO_CONTROL_FRAME 0x01
|
|
|
|
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.
|
|
// Copies the character generator ROM into the tiles the console draws from, and the default
|
|
// schemes into the palette entries they live in. Reset does both; the Command port is how a
|
|
// program asks for either afterwards.
|
|
//
|
|
// NEITHER CLEARS WHAT IT DOES NOT OWN. The font writes glyphs 0 to 134 and stops, so a tile
|
|
// a program defined above them survives; the palette writes the two entries of each of the
|
|
// sixteen schemes and stops, so a program's own colours in between survive. Asking for the
|
|
// font back should not cost a program the tile it was drawing with.
|
|
void videoLoadFont(void);
|
|
void videoLoadPalette(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
|