// 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 16 bytes. Sixteen so that entry n begins at n times sixteen, // which is a shift - the same no-multiply argument that makes a palette entry four bytes and // a map row a page. // // It was eight, and grew when scaling arrived: four bytes for a target size and one for a // depth would not fit beside what was already there. Grown NOW rather than later, because // the cost of moving it is a rebuild of the two programs that use it, and the cost of moving // it once somebody has written a game on top of it is not. #define VIDEO_SPRITE_BASE 0xC000 #define VIDEO_SPRITE_COUNT 256 #define VIDEO_SPRITE_BYTES 16 // 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 // ---- How big to draw it, which is not the same as how big it is ---- // // Bytes 8 to 11: a target width and a target height in PIXELS, low byte first. The device // stretches the m by n tiles to fill that, so a program says how big the thing should look // and never works out a ratio. // // A TARGET IN PIXELS RATHER THAN A MULTIPLIER, which is the whole of why this is usable on // a machine with no divide. A billboard at distance d wants to be k/d pixels tall, and that // is a number the program has anyway - out of a lookup table, most likely. A multiplier // would have to be a fixed point fraction, computed by dividing, which is the one thing // this CPU cannot do. // // NOUGHT MEANS NATURAL SIZE, eight times the tile count on that axis. So every sprite // written before scaling existed still means what it meant, and the common case - a thing // drawn at the size it was drawn at - costs nothing to say. #define VIDEO_SPRITE_WIDTH 8 #define VIDEO_SPRITE_HEIGHT 10 // ---- And how far away it is ---- // // Byte 12. Nought means no depth test at all, which is what every ordinary sprite wants and // what a cleared table already says. // // Otherwise it is compared against the DEPTH BUFFER below, one column at a time, and the // sprite draws only in the columns it is in front of. That is the thing table order cannot // do: a billboard can be nearer than the wall in one column and further in the next, and no // amount of sorting the table expresses that. #define VIDEO_SPRITE_DEPTH 12 // Bytes 13 to 15 are reserved and should be left at nought. // ---- The depth buffer ---- // // One byte a screen column, written by the program and read by the device. A wall pass says // how far away the thing it drew in each column was; a sprite with a depth says how far away // it is; and a sprite pixel is drawn only where it is nearer. // // NOUGHT IN A COLUMN MEANS NOTHING IS THERE, so a program that never writes this has a // buffer of noughts and every sprite draws - which is the behaviour there was before it // existed. It is not cleared between frames: it belongs to the program, and a program that // draws walls rewrites all of it every frame anyway. // // It is in the atlas because that is where the things a program sets up live, and because // the alternative is a port and this is 640 bytes. #define VIDEO_DEPTH_BASE 0xD000 // ---- 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 // ---- The window: a layer that does not scroll ---- // // A status bar over a playfield. The map moves and this does not, which the map alone cannot // express: the scroll registers move ALL of it, so a score printed into the map is a score // that slides away, and one printed into the rows the view happens to be showing is one that // jumps a pixel at a time as the fine offset changes. // // ITS OWN MEMORY, and that is the whole argument. The first design drew the top rows of the // MAP without the scroll applied, which needs no new memory and costs one register - and // makes those rows part of the playfield's ring, so a game that scrolls vertically has to // route its world around its own scoreboard for ever. The whole point of a status bar is that // it is not somewhere in the level. // // 0xC000 to 0xFFFF in the screen bank, which the map does not reach: it ends at 0xBFFF. Sixty // four rows of the same 256 byte cells, so a window row is a page like a map row is, and a // cell in it means exactly what a cell in the map means - same tiles, same pages, same // colour schemes. // // PER SCREEN, because it is in the screen bank: flipping to the other buffer flips the status // bar with it, which is what a double buffered game wants and would be surprising the other // way round. #define VIDEO_WINDOW_BASE 0xC000 #define VIDEO_WINDOW_ROWS 64 // How many screen rows tall it is, and which screen row it starts at. NOUGHT IS OFF, so a // machine that wakes up with a cleared screen has no window and every program written before // this one means what it meant. // // A start row is worth a register because a status bar at the bottom is as common as one at // the top, and working it out from the screen height is a thing every program would have to // do again. #define VIDEO_WINDOW_HEIGHT 0x3D #define VIDEO_WINDOW_AT 0x3E // ---- 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