A second screen, and one port to say which is shown
A screen drawn where it can be seen is seen half drawn. A program that moves forty things and rewrites the map underneath them is wrong for as long as it takes to put them all right, and at a megahertz that is long enough to look at. So the device brings a second screen bank, on port 0x3B, and port 0x3C says which of the two is displayed. Everything a program draws into the other one is invisible until one byte shows the whole of it at once. ONE REGISTER IS ENOUGH, where the hardware this imitates needed two. The other said which screen 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 screen that is not shown is a matter of naming its bank, and the device never has to be told. And a flip cannot tear: a frame is drawn from one bank in one go, so a flip either happened before that frame or happens before the next. There is nothing to race, where the real machines had to catch the few lines between frames to swap in. The console draws into whichever screen is displayed rather than one of its own, so a fault message lands where somebody can read it even if a game had flipped. And CosmOS puts the displayed screen back at exit, the way it already puts back the cursor and the ink: a program that faulted while flipped could not have, and a shell that only came out right for programs which remembered would come out wrong the day one crashed. Flip.asm is the worked example. It deliberately does NOT restore the display itself - that is the point of the paragraph above, and it is what makes the system's guarantee the thing under test rather than the program's good manners. Written the other way round first, where it passed with the guarantee deleted. 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
66e7b84272
commit
023362b05a
@@ -1304,7 +1304,7 @@ uint8_t *deviceMemory(uint8_t port, uint32_t *capacity) {
|
||||
*capacity = DISK_BLOCK_BYTES;
|
||||
return diskBuffer;
|
||||
}
|
||||
if (port == PORT_VIDEO || port == VIDEO_SCREEN) {
|
||||
if (port == PORT_VIDEO || port == VIDEO_SCREEN0 || port == VIDEO_SCREEN1) {
|
||||
// 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
|
||||
@@ -1378,10 +1378,14 @@ static const DeviceRecord *deviceOnPort(uint8_t port) {
|
||||
//
|
||||
// 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 videoScreen0Record =
|
||||
{ VIDEO_SCREEN0, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY };
|
||||
static const DeviceRecord videoScreen1Record =
|
||||
{ VIDEO_SCREEN1, DEVICE_VIDEO, DEVICE_FLAG_HAS_MEMORY };
|
||||
static const DeviceRecord videoPlainRecord = { PORT_VIDEO, DEVICE_VIDEO, 0 };
|
||||
return (port == VIDEO_SCREEN) ? &videoScreenRecord : &videoPlainRecord;
|
||||
if (port == VIDEO_SCREEN0) { return &videoScreen0Record; }
|
||||
if (port == VIDEO_SCREEN1) { return &videoScreen1Record; }
|
||||
return &videoPlainRecord;
|
||||
}
|
||||
if (port > PORT_SOUND && port <= PORT_SOUND_TOP) {
|
||||
return deviceOnPort(PORT_SOUND);
|
||||
|
||||
+28
-7
@@ -15,7 +15,12 @@
|
||||
// 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 videoScreen[VIDEO_SCREEN_COUNT][VIDEO_MEMORY_BYTES];
|
||||
|
||||
// Which screen is being shown. The console draws into THIS one rather than into a screen of
|
||||
// its own, so text goes where whoever is looking is looking - which matters most when the
|
||||
// text is a fault message printed over a game that had flipped.
|
||||
static uint8_t displayed = 0;
|
||||
|
||||
static uint8_t mode;
|
||||
// Which map row is drawn at the top. THE MAP IS A RING: rendering row r reads map row
|
||||
@@ -169,7 +174,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 = videoScreen + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE
|
||||
uint8_t *cell = videoScreen[displayed] + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE
|
||||
+ mapColumn * VIDEO_CELL_BYTES;
|
||||
cell[0] = tile;
|
||||
cell[1] = attribute;
|
||||
@@ -182,12 +187,14 @@ 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(videoScreen + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE, 0, VIDEO_MAP_STRIDE);
|
||||
memset(videoScreen[displayed] + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE, 0,
|
||||
VIDEO_MAP_STRIDE);
|
||||
}
|
||||
|
||||
void videoReset(void) {
|
||||
memset(videoAtlas, 0, sizeof(videoAtlas));
|
||||
memset(videoScreen, 0, sizeof(videoScreen));
|
||||
displayed = 0;
|
||||
mode = VIDEO_MODE_40x25;
|
||||
scroll = 0;
|
||||
scrollColumn = 0;
|
||||
@@ -211,8 +218,11 @@ uint8_t *videoMemory(uint8_t port, uint32_t *capacity) {
|
||||
if (port == VIDEO_STATUS) {
|
||||
return videoAtlas;
|
||||
}
|
||||
if (port == VIDEO_SCREEN) {
|
||||
return videoScreen;
|
||||
if (port == VIDEO_SCREEN0) {
|
||||
return videoScreen[0];
|
||||
}
|
||||
if (port == VIDEO_SCREEN1) {
|
||||
return videoScreen[1];
|
||||
}
|
||||
// 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.
|
||||
@@ -229,6 +239,14 @@ uint8_t videoWrite(uint8_t value, uint8_t port) {
|
||||
mode = value;
|
||||
}
|
||||
break;
|
||||
case VIDEO_DISPLAY:
|
||||
// A screen that does not exist is not taken, for the same reason a mode that
|
||||
// does not exist is not: whoever asked still has the screen they had, and
|
||||
// stopping the machine over it would be a poor trade.
|
||||
if (value < VIDEO_SCREEN_COUNT) {
|
||||
displayed = value;
|
||||
}
|
||||
break;
|
||||
case VIDEO_CONTROL:
|
||||
frameInterrupts = (value & VIDEO_CONTROL_FRAME) != 0;
|
||||
if (!frameInterrupts) {
|
||||
@@ -286,6 +304,8 @@ uint8_t videoWrite(uint8_t value, uint8_t port) {
|
||||
|
||||
uint8_t videoRead(uint8_t port) {
|
||||
switch (port) {
|
||||
case VIDEO_DISPLAY:
|
||||
return displayed;
|
||||
case VIDEO_STATUS: {
|
||||
uint8_t status = 0;
|
||||
if (frameWaiting) {
|
||||
@@ -334,7 +354,7 @@ void videoRender(void) {
|
||||
// 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 = videoAtlas + VIDEO_PALETTE_BASE;
|
||||
const uint8_t *from = videoScreen + VIDEO_BITMAP_BASE;
|
||||
const uint8_t *from = videoScreen[displayed] + 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;
|
||||
@@ -364,7 +384,8 @@ 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 = videoScreen + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE;
|
||||
const uint8_t *cells = videoScreen[displayed] + 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];
|
||||
|
||||
+32
-12
@@ -176,18 +176,37 @@ 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 ----
|
||||
// ---- 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 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.
|
||||
// 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 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
|
||||
// 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
|
||||
@@ -265,9 +284,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);
|
||||
|
||||
// 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.
|
||||
// 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);
|
||||
|
||||
Reference in New Issue
Block a user