// 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. #define VIDEO_TILE_BASE 0x0000 #define VIDEO_TILE_BYTES 64 #define VIDEO_TILE_COUNT 256 // ---- Four pages of them ---- // // A tile number is a byte and a byte reaches 256, which is not many once a font has taken // 135 of them and a game wants a character, a background and a wall. So two bits of the // ATTRIBUTE say which page of 256 the number is in - bits that were already there, already // written on every cell and every sprite, and reserved from the day the attribute was // defined for exactly this. // // Four pages of 16K is 64K, which is the whole atlas, so THE FOURTH PAGE IS THE MEMORY THE // SPRITE TABLE AND THE PALETTE ARE IN. That is not a hole in the design, it is the same // answer shared video memory has always given: the atlas is 1024 tiles, and what a program // spends on sprites and colours comes out of them. A program that wants no sprites may have // page three for art, and one that wants sprites has 768 tiles and knows why. // // The page is per CELL and per SPRITE rather than a mode, so a screen can show tiles from // all four at once and a program never has to decide which page it is "in". #define VIDEO_TILE_PAGE_BYTES (VIDEO_TILE_COUNT * VIDEO_TILE_BYTES) #define VIDEO_TILE_PAGES 4 // ---- 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 // ---- What the attribute byte means, in both places it appears ---- // // A map cell's second byte and a sprite's byte 1 are the same thing and are read the same // way, which is what lets the same art be a background in one place and a moving thing in // another with nothing rewritten. #define VIDEO_ATTRIBUTE_SCHEME 0x0F #define VIDEO_ATTRIBUTE_PAGE 0x30 #define VIDEO_ATTRIBUTE_SHIFT 4 // Bits 6 and 7 are still reserved and should be left at nought. // ---- 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 // ---- Sprites ---- // // Things that move without the map moving. A map cell is where it is, and a program that // wanted something between two cells had to redraw both of them; a sprite is put at a PIXEL // and the device draws it over whatever is behind. // // MADE OF TILES, which is the decision the rest follows from. A sprite is m by n tiles taken // in reading order from one index, so it needs no pixel format of its own, no second kind of // memory, and no way for its art to be anything the map could not also show. A 16 by 16 // character is four tiles and a program that wants it in the background too just names the // same four. // // The table is 256 entries of 8 bytes. Eight so that entry n begins at n times eight, which // is a shift - the same no-multiply argument that makes a palette entry four bytes and a map // row a page. It sits above the tiles with the whole of 0x4000 to 0xBFFF still clear beneath // it, which is two more 16K pages of tiles if they are ever wanted. #define VIDEO_SPRITE_BASE 0xC000 #define VIDEO_SPRITE_COUNT 256 #define VIDEO_SPRITE_BYTES 8 // Byte 0 is the top left tile, byte 1 the attribute, which means what a map cell's attribute // means: its low nibble times sixteen is added to every index in the art. #define VIDEO_SPRITE_TILE 0 #define VIDEO_SPRITE_ATTRIBUTE 1 // Bytes 2 to 5, low byte first, and SIGNED - a screen is 640 by 400 in the larger mode, so // neither axis fits in a byte, and a sprite has to be able to sit half off the left or the // top rather than appearing whole at the edge. #define VIDEO_SPRITE_X 2 #define VIDEO_SPRITE_Y 4 // Byte 6: how many tiles across in the high nibble, how many down in the low. Fifteen each // way, so 120 by 120 pixels. // // A SPRITE OF NO WIDTH OR NO HEIGHT DRAWS NOTHING, and that is the off switch. It saves a // flag, it is per sprite rather than a global the whole table shares, and it means the table // is already off when the machine starts, since the atlas wakes up cleared. // // Deliberately the OPPOSITE of what a length of zero means to the memory controller, where // it means the whole 64K. The reason is the same both times: moving no bytes is a useless // thing to ask for, so zero was free to mean something else there - and drawing no sprite is // the commonest state in this table, so zero has to mean nothing here. #define VIDEO_SPRITE_SIZE 6 // Byte 7. #define VIDEO_SPRITE_FLAGS 7 #define VIDEO_SPRITE_HFLIP 0x01 #define VIDEO_SPRITE_VFLIP 0x02 // Drawn only where the background had nothing, so a thing can walk behind a pillar. See // below for what "nothing" means. #define VIDEO_SPRITE_BEHIND 0x04 // ---- What a sprite does not cover ---- // // A PIXEL OF ZERO IS NOT DRAWN. Without that every sprite is a rectangle, and there is no // other candidate: the font already uses index 0 for paper, so it is the value art in this // machine has always left empty. // // Tested BEFORE the attribute is added, so it is a property of the art and not of the colour // scheme it is being shown in. A sprite drawn in indices 1 to 15 is transparent in the same // places in all sixteen schemes, which is the whole point of the additive nibble. // // The same rule read the other way is what "behind" means: a sprite marked behind draws only // where the BACKGROUND pixel was zero. One rule, applied to whichever layer is in front. // // ---- How many at once ---- // // All of them. Every entry in the table is drawn every frame, so sprites cannot flicker. // Real machines dropped them per scanline because they had a fixed number of shift registers // and a fixed time to fill them; this has a loop. The limit is how many entries there are, // which is a constant a program can count on rather than a property of what it happens to be // drawing this frame. // // Where they overlap, THE LOWER NUMBER IS IN FRONT. // ---- 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 ports that own banks ---- // // A bank is registered by naming THE PORT THAT OWNS IT, which the controller settled long // before the screen had more than one. So a device with several banks needs several ports // that own memory, and needs no new mechanism at all. // // The base port keeps the atlas rather than a screen because tiles have been at 0x0000 since // there was a screen at all, and whichever way round that went, one of the two meanings had // to move. Nothing is READ OR WRITTEN at any of these - they are names for banks, and the // registry is where a program finds out which of them bring one. #define VIDEO_SCREEN0 0x3A #define VIDEO_SCREEN1 0x3B // ---- Two screens, and only one register to say which ---- // // A back buffer is a whole screen's worth of map written where nobody can see it, and then // shown all at once. It is what stops a picture being seen half finished - a game that moves // forty sprites and rewrites the map underneath them is not finished being wrong until the // last of them has been put right. // // THE DEVICE ONLY NEEDS TO KNOW WHICH IS DISPLAYED. Real machines needed a second register // saying which one the CPU's window pointed at; there is no window here, because a program // reaches a bank through the memory controller BY ITS NUMBER. Writing to the one that is not // being shown is a matter of naming its bank, and the screen never has to be told. // // A FLIP CANNOT TEAR. A frame is drawn from one bank in one go, so a flip either happened // before that frame or it happens before the next one; there is no state of having flipped // halfway. That is worth saying because it is the thing the hardware this imitates had to // work for, with an interrupt and a register written in the few lines between frames. #define VIDEO_DISPLAY 0x3C #define VIDEO_SCREEN_COUNT 2 // 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 memory-owning ports: VIDEO_STATUS for the atlas, // VIDEO_SCREEN0 and VIDEO_SCREEN1 for the two screens. 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