Files
SplitBit-Emulator/Source/Emulator/video.h
T
AnachronautandClaude Opus 5 a916103a7f Sprites: things that move without the screen moving
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
2026-09-02 11:42:11 -04:00

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