// video.h // The Voyager's video device. // Written by Anachronaut #ifndef VIDEO_H #define VIDEO_H #include // ---- 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, in two banks ---- // // Registered the way the disk's buffer is, so the screen 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 memory rather than a // window onto a port. // // TWO BANKS AND NOT ONE, because the two halves of a screen are written at completely // different rates. Tiles and colours are an ATLAS: put there when a program loads and then // left alone. The map is a SCREEN: rewritten as often as anything moves. Sharing one bank // made them compete for 64K they did not both need, and it had a worse consequence than // being cramped - a bitmap took the whole bank, so ENTERING BITMAP MODE DESTROYED THE FONT. // A program could not draw a picture and then say anything about it. // // Split, each gets a whole 64K and neither can tread on the other. A bitmap now overwrites // the map, which is the same memory meaning a different thing in a different mode and is // exactly what it should overwrite. The tiles behind the text survive it. // // Each bank is the same size, and every address below says which of the two it is in. #define VIDEO_MEMORY_BYTES 0x10000 // ---- In the ATLAS bank ---- // // Tile memory: 256 tiles of 8x8, one byte a pixel. Everything above it is free, and is // where more tiles and the sprite table are going. #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. // // IN THE SCREEN BANK, and still at 0x4000 rather than at the bottom of a bank it now has to // itself. Moving it would have been tidier and would have meant changing which bank a // program registers AND which address it writes to in the same breath - so a screen that // came out wrong would have had two possible causes. The address costs nothing where it is. #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 map holding whatever the // picture put there, which is what taking the screen means. // // IN THE SCREEN BANK, so what a picture costs is the map and nothing else. THE TILES AND THE // PALETTE ARE IN THE OTHER BANK AND SURVIVE IT, which is what lets a program draw a picture // and then put text back on the screen without reloading the character generator first. #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 the ATLAS bank, 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 it // is written when a program loads rather than per frame, which is what the atlas is for. // // Being out of the screen bank is what leaves a bitmap the WHOLE of one: 64,000 bytes of // picture in 65,536, with nothing it has to dodge. #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 // ---- The port that owns the screen bank ---- // // A bank is registered by naming THE PORT THAT OWNS IT, which the controller settled long // before the screen had two of them. So a device with two banks needs two ports that own // memory, and needs no new mechanism at all: the base port owns the atlas, and this one // owns the screen. // // The base port keeps the atlas rather than the screen because tiles have been at 0x0000 // since there was a screen at all, and whichever way round this went, one of the two // meanings had to move. Nothing is READ OR WRITTEN here - it is a name for a bank, and the // registry is where a program finds out it brings one. #define VIDEO_SCREEN 0x3A // 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); // The memory behind one of the device's two ports: VIDEO_STATUS for the atlas, VIDEO_SCREEN // for the screen. NULL for any other port, because the rest of the block owns no memory and // registering a bank onto one would put a number in the table that leads nowhere. uint8_t *videoMemory(uint8_t port, 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