Everything drawn on this machine was in a cell. Something between two cells meant rewriting both; something moving a pixel at a time meant rewriting them sixty times a second, which is affordable for one thing and not for twenty. A sprite is put at a pixel and the device draws it over whatever is behind, so moving it costs two bytes. MADE OF TILES, which is the decision the rest follows from: m by n taken in reading order from one index, so there is no second pixel format, no second kind of memory, and nothing a sprite can show that the map cannot. A 16 by 16 character is four tiles and the background can name the same four. 256 entries of 8 bytes at 0xC000 in the atlas - eight so the entry address is a shift, the same no-multiply argument as the palette's four. Position is signed and sixteen bits, because 640 by 400 does not fit in a byte and a sprite has to be able to sit half off the left rather than appearing whole at the edge. A PIXEL OF ZERO IS NOT DRAWN, or every sprite is a rectangle. Tested before the attribute is added, so a hole belongs to the art and not to the colour scheme. The same rule the other way round is what "behind" means: drawn only where the background pixel was zero, so a thing walks behind a pillar and in front of the floor in one frame. All of them draw, every frame, so they cannot flicker. Real machines dropped them per scanline because they had a fixed number of shift registers; this has a loop. The limit is the size of the table, which is a constant rather than a property of what is on screen. And the system takes them down at exit. The sprite table sits in the gap the screen save walks around - to the end of the map, then the palette - and that is right, because nothing the shell draws is a sprite: there is nothing to give back, only something to take away. Otherwise a program that put a ball up and left would leave it over the prompt, in front of everything, with nothing able to type it away. Sprite.asm deliberately leaves its own, because a program that faulted could not have cleared it. Every check here was re-broken and failed: transparency, reading order, draw order, priority, and size. Size needed breaking twice - the first attempt did not compile, and a silent build failure had left the old binary passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
383 lines
19 KiB
C
383 lines
19 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, 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
|
|
|
|
// ---- 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
|