The screen is two banks: an atlas and a screen
Tiles and colours are written when a program loads; the map is written whenever anything moves. Sharing one 64K bank made them compete for room neither needed all of, and had a worse consequence than being cramped: a bitmap covers 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 bank. The atlas holds the tiles and the palette, the screen holds the map or a bitmap, and a picture now costs the map and nothing else. It also leaves 48K free in the atlas, which is where the sprite table and a second page of tiles are going. No new mechanism was needed. A bank is registered by naming the port that owns it, so a device with two banks needs two ports that own memory: the base port keeps the atlas, since tiles have been at 0x0000 since there was a screen at all, and 0x3A owns the screen. The registry now answers honestly about which ports in the block bring memory, where it used to say all sixteen did. CosmOS never addresses video memory except in one place - the screen save, which walks 196 pages of it. The page number already says which bank a page is in, so screenBankFor works it out rather than keeping a second list beside screenPageFor. Grid and picture.asm register both banks; colours.asm only touches the palette and needed none of it. Tests/video.sh names the memory every write is for, because an address cannot: tile 5 and bitmap pixel 5 are both 0x0005, and a helper that guessed would be right for the tiles and silently wrong for a picture. And picture.asm gained a check, because this change broke it and nothing noticed - registering the second bank leaves DestBank pointing at it, so the palette went into the wrong one and the picture came out black. It was the only thing here found by looking rather than by a test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
2abc8281df
commit
66e7b84272
+18
-7
@@ -1304,11 +1304,12 @@ uint8_t *deviceMemory(uint8_t port, uint32_t *capacity) {
|
||||
*capacity = DISK_BLOCK_BYTES;
|
||||
return diskBuffer;
|
||||
}
|
||||
if (port == PORT_VIDEO) {
|
||||
// Tiles, the map and the palette, in one bank. A program blits the part that
|
||||
// changed and the rest stays as it was, which is the whole reason the screen is a
|
||||
// bank rather than a window onto a port.
|
||||
return videoMemory(capacity);
|
||||
if (port == PORT_VIDEO || port == VIDEO_SCREEN) {
|
||||
// Two banks: the atlas of tiles and colours on the base port, and the map or the
|
||||
// bitmap on its own. A program blits the part that changed and the rest stays as it
|
||||
// was, which is the whole reason the screen is memory rather than a window onto a
|
||||
// port - and having two means a picture costs the map and not the font.
|
||||
return videoMemory(port, capacity);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -1369,8 +1370,18 @@ static const DeviceRecord *deviceOnPort(uint8_t port) {
|
||||
return deviceOnPort(PORT_DISK);
|
||||
}
|
||||
if (port > PORT_VIDEO && port <= PORT_VIDEO_TOP) {
|
||||
// Sixteen ports, one device, and the same rule again.
|
||||
return deviceOnPort(PORT_VIDEO);
|
||||
// Sixteen ports, one device, and the same rule again - with one difference, because
|
||||
// this device owns TWO banks. Forwarding the whole block to the base record used to
|
||||
// say that all sixteen ports brought memory, which was harmless only while nobody
|
||||
// believed it: a program that enumerated the block and registered everything
|
||||
// claiming memory would have faulted on the fourteen that have none.
|
||||
//
|
||||
// So the block answers honestly. The screen port says it brings memory because it
|
||||
// does, and the rest of the block says it does not.
|
||||
static const DeviceRecord videoScreenRecord =
|
||||
{ VIDEO_SCREEN, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY };
|
||||
static const DeviceRecord videoPlainRecord = { PORT_VIDEO, DEVICE_VIDEO, 0 };
|
||||
return (port == VIDEO_SCREEN) ? &videoScreenRecord : &videoPlainRecord;
|
||||
}
|
||||
if (port > PORT_SOUND && port <= PORT_SOUND_TOP) {
|
||||
return deviceOnPort(PORT_SOUND);
|
||||
|
||||
+26
-13
@@ -11,7 +11,11 @@
|
||||
// The bank the device brings. Registered by whoever enumerates the hardware, reached only
|
||||
// through the memory controller, and never by the CPU directly - the same arrangement the
|
||||
// disk's buffer has always had.
|
||||
static uint8_t videoRAM[VIDEO_MEMORY_BYTES];
|
||||
// The two banks. Which one an address is in is a property of the address and never of the
|
||||
// mode: tiles and the palette are always in the atlas, the map and a bitmap always in the
|
||||
// screen. That is what makes the split cost nothing to think about at a call site.
|
||||
static uint8_t videoAtlas[VIDEO_MEMORY_BYTES];
|
||||
static uint8_t videoScreen[VIDEO_MEMORY_BYTES];
|
||||
|
||||
static uint8_t mode;
|
||||
// Which map row is drawn at the top. THE MAP IS A RING: rendering row r reads map row
|
||||
@@ -129,7 +133,7 @@ void videoLoadFont(void) {
|
||||
// can ask for it. A program that defined a tile of its own above the font and then wanted
|
||||
// its text back would have lost the tile to get it.
|
||||
for (int glyph = 0; glyph < CONSOLE_FONT_GLYPHS && glyph < VIDEO_TILE_COUNT; glyph++) {
|
||||
uint8_t *tile = videoRAM + VIDEO_TILE_BASE + glyph * VIDEO_TILE_BYTES;
|
||||
uint8_t *tile = videoAtlas + VIDEO_TILE_BASE + glyph * VIDEO_TILE_BYTES;
|
||||
for (int y = 0; y < CONSOLE_FONT_BYTES; y++) {
|
||||
const unsigned char row = consoleFont[glyph * CONSOLE_FONT_BYTES + y];
|
||||
for (int x = 0; x < VIDEO_CELL_PIXELS; x++) {
|
||||
@@ -140,7 +144,7 @@ void videoLoadFont(void) {
|
||||
}
|
||||
|
||||
void videoLoadPalette(void) {
|
||||
uint8_t *palette = videoRAM + VIDEO_PALETTE_BASE;
|
||||
uint8_t *palette = videoAtlas + VIDEO_PALETTE_BASE;
|
||||
for (int bank = 0; bank < 8; bank++) {
|
||||
// Colour on black, and then the same colour as paper with black ink, sixteen banks
|
||||
// apart so that one bit turns either into the other.
|
||||
@@ -165,7 +169,7 @@ void videoPutCell(int screenRow, int column, uint8_t tile, uint8_t attribute) {
|
||||
// less than a cell, and there is no such thing as less than a cell to write into.
|
||||
const int mapRow = (scroll + screenRow) % VIDEO_MAP_ROWS;
|
||||
const int mapColumn = (scrollColumn + column) % VIDEO_MAP_COLUMNS;
|
||||
uint8_t *cell = videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE
|
||||
uint8_t *cell = videoScreen + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE
|
||||
+ mapColumn * VIDEO_CELL_BYTES;
|
||||
cell[0] = tile;
|
||||
cell[1] = attribute;
|
||||
@@ -178,11 +182,12 @@ void videoScrollUp(void) {
|
||||
// hundred rows of what has already been said, still sitting in the map.
|
||||
const int bottom = rowsFor(mode) - 1;
|
||||
const int mapRow = (scroll + bottom) % VIDEO_MAP_ROWS;
|
||||
memset(videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE, 0, VIDEO_MAP_STRIDE);
|
||||
memset(videoScreen + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE, 0, VIDEO_MAP_STRIDE);
|
||||
}
|
||||
|
||||
void videoReset(void) {
|
||||
memset(videoRAM, 0, sizeof(videoRAM));
|
||||
memset(videoAtlas, 0, sizeof(videoAtlas));
|
||||
memset(videoScreen, 0, sizeof(videoScreen));
|
||||
mode = VIDEO_MODE_40x25;
|
||||
scroll = 0;
|
||||
scrollColumn = 0;
|
||||
@@ -201,9 +206,17 @@ void videoReset(void) {
|
||||
videoLoadPalette();
|
||||
}
|
||||
|
||||
uint8_t *videoMemory(uint32_t *capacity) {
|
||||
uint8_t *videoMemory(uint8_t port, uint32_t *capacity) {
|
||||
*capacity = VIDEO_MEMORY_BYTES;
|
||||
return videoRAM;
|
||||
if (port == VIDEO_STATUS) {
|
||||
return videoAtlas;
|
||||
}
|
||||
if (port == VIDEO_SCREEN) {
|
||||
return videoScreen;
|
||||
}
|
||||
// Every other port in the block owns no memory. Saying so is what stops a bank being
|
||||
// registered onto one of them and pointing at nothing.
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint8_t videoWrite(uint8_t value, uint8_t port) {
|
||||
@@ -320,8 +333,8 @@ void videoRender(void) {
|
||||
// No tile to look up and no attribute to add: the byte IS the palette index. Which
|
||||
// is the whole difference between the two kinds of screen - a tile mode costs the
|
||||
// CPU the number of cells that changed, and this costs it the number of pixels.
|
||||
const uint8_t *palette = videoRAM + VIDEO_PALETTE_BASE;
|
||||
const uint8_t *from = videoRAM + VIDEO_BITMAP_BASE;
|
||||
const uint8_t *palette = videoAtlas + VIDEO_PALETTE_BASE;
|
||||
const uint8_t *from = videoScreen + VIDEO_BITMAP_BASE;
|
||||
uint8_t *out = pixels;
|
||||
for (int at = 0; at < VIDEO_BITMAP_WIDTH * VIDEO_BITMAP_HEIGHT; at++) {
|
||||
const uint8_t *entry = palette + from[at] * VIDEO_PALETTE_BYTES;
|
||||
@@ -351,7 +364,7 @@ void videoRender(void) {
|
||||
// The ring. Rows that scrolled off the top are still in the map, which is what
|
||||
// makes scrollback free rather than something the console has to keep itself.
|
||||
const int mapRow = (scroll + row) % VIDEO_MAP_ROWS;
|
||||
const uint8_t *cells = videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE;
|
||||
const uint8_t *cells = videoScreen + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE;
|
||||
for (int column = 0; column <= columns; column++) {
|
||||
const int mapColumn = (scrollColumn + column) % VIDEO_MAP_COLUMNS;
|
||||
const uint8_t tile = cells[mapColumn * VIDEO_CELL_BYTES];
|
||||
@@ -377,7 +390,7 @@ void videoRender(void) {
|
||||
// tile that wants all 256 colours simply leaves the nibble at zero and gets
|
||||
// them. One adder in hardware, and neither use costs the other anything.
|
||||
const uint8_t bank = (uint8_t)((attribute & 0x0F) << 4);
|
||||
const uint8_t *art = videoRAM + VIDEO_TILE_BASE + tile * VIDEO_TILE_BYTES;
|
||||
const uint8_t *art = videoAtlas + VIDEO_TILE_BASE + tile * VIDEO_TILE_BYTES;
|
||||
for (int y = 0; y < VIDEO_CELL_PIXELS; y++) {
|
||||
// Where this row of the cell lands once the view has been slid up by the
|
||||
// fine offset. Negative means it is the part of the top cell that is off
|
||||
@@ -395,7 +408,7 @@ void videoRender(void) {
|
||||
// high end of the palette with a nibble set comes round the bottom,
|
||||
// which is what an adder does and what the manual says it does.
|
||||
const uint8_t index = (uint8_t)(art[y * VIDEO_CELL_PIXELS + x] + bank);
|
||||
const uint8_t *entry = videoRAM + VIDEO_PALETTE_BASE
|
||||
const uint8_t *entry = videoAtlas + VIDEO_PALETTE_BASE
|
||||
+ index * VIDEO_PALETTE_BYTES;
|
||||
uint8_t *out = pixels + (atY * width + atX) * 3;
|
||||
out[0] = entry[0];
|
||||
|
||||
+54
-11
@@ -19,16 +19,31 @@
|
||||
// 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 ----
|
||||
// ---- The device brings memory, in two banks ----
|
||||
//
|
||||
// One bank, registered the way the disk's buffer is, so it costs a program nothing in Data
|
||||
// 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 a bank rather than a
|
||||
// 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
|
||||
|
||||
// Tile memory: 256 tiles of 8x8, one byte a pixel.
|
||||
// ---- 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
|
||||
@@ -43,6 +58,11 @@
|
||||
//
|
||||
// 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
|
||||
@@ -59,8 +79,12 @@
|
||||
// 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 tiles and the map holding
|
||||
// whatever the picture put there, which is what taking the screen means.
|
||||
// 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
|
||||
@@ -71,9 +95,12 @@
|
||||
// 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 video memory, 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 64,000
|
||||
// bytes of picture leaves nowhere in the middle for it to hide.
|
||||
// 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
|
||||
@@ -149,6 +176,19 @@ int videoTextRows(void);
|
||||
// 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
|
||||
|
||||
@@ -225,7 +265,10 @@ void videoPutCell(int screenRow, int column, uint8_t tile, uint8_t attribute);
|
||||
// bottom - which is holding whatever was there 128 rows ago, since the map is a ring.
|
||||
void videoScrollUp(void);
|
||||
|
||||
uint8_t *videoMemory(uint32_t *capacity);
|
||||
// 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);
|
||||
|
||||
Reference in New Issue
Block a user