From 023362b05a5855148dfba310e46e93d74c0d81a8 Mon Sep 17 00:00:00 2001 From: Anachronaut Date: Wed, 2 Sep 2026 10:31:29 -0400 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW --- Programs/CosmOS/Apps/Flip.asm | 145 ++++++++++++++++++++++++++++ Programs/CosmOS/README.md | 11 ++- Programs/CosmOS/Source/cosmos.asm | 21 +++- Source/Emulator/io.c | 12 ++- Source/Emulator/video.c | 35 +++++-- Source/Emulator/video.h | 44 ++++++--- SplitBit Programming Manual.md | 25 ++++- Tests/expected/cosmosCrossDisk.out | 3 +- Tests/expected/cosmosDrives.out | 3 +- Tests/expected/cosmosFault.out | 3 +- Tests/expected/cosmosGrid.out | 3 +- Tests/expected/cosmosMonitor.out | 3 +- Tests/expected/cosmosMonitorRun.out | 3 +- Tests/expected/cosmosRun.out | 3 +- Tests/expected/cosmosSlowDisk.out | 3 +- Tests/makedisks.sh | 6 ++ Tests/video.sh | 113 ++++++++++++++++++++++ 17 files changed, 399 insertions(+), 37 deletions(-) create mode 100644 Programs/CosmOS/Apps/Flip.asm diff --git a/Programs/CosmOS/Apps/Flip.asm b/Programs/CosmOS/Apps/Flip.asm new file mode 100644 index 0000000..18553a4 --- /dev/null +++ b/Programs/CosmOS/Apps/Flip.asm @@ -0,0 +1,145 @@ +; The screen nobody is looking at. +; +; The screen brings two map banks and shows one of them. Everything this program draws goes +; into the other, so the picture does not change at all while it is being built - and then +; one byte out of one port shows the whole of it at once. +; +; ---- Why that is worth a port ---- +; +; A screen drawn where it can be seen is seen half drawn. A game 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. The machines this one is pretending to +; be had the same problem and solved it the same way, except that they had to catch the few +; lines between one frame and the next to do the swap in. Here a frame is drawn from one bank +; in one go, so a flip cannot land halfway through one and there is nothing to race. +; +; ---- What it costs ---- +; +; A whole bank, which is 64K of somebody's memory - and nothing, which is the point. The +; second screen is memory the device brought, the same as the first, so a program that wants +; it registers it and a program that does not never pays for it. +; +; Written by Anachronaut + +#Include services.asm + +#Program + + #Base 0x5000 + +start: + ; ---- The two banks this touches ---- + ; + ; Four is the atlas, where the tiles are. Six is the other screen: three is the disk's and + ; four and five are the ones the system registers to save a screen with, so six is the + ; first number free. Nothing hands these out - see the table in the CosmOS README. + INIA 0d4 + OUTA 0xE3 + INIA 0x30 + OUTA 0xE2 + INIA 0x03 + OUTA 0xE8 + + INIA 0d6 + OUTA 0xE3 + INIA 0x3B + OUTA 0xE2 + INIA 0x03 + OUTA 0xE8 + + ; The screen back afterwards, because the tile overwritten below is one the shell spells + ; with. Refused is survivable and is not checked: there is nothing this could do about it, + ; and the system puts the font back at exit whether or not it saved anything. + SWI osTakeScreen + + SETD.0 Message + SWI osPrintString + + ; Key mode, so a key arrives when it is pressed rather than when Return is. + INIA 0x01 + OUTA 0x02 + + ; ---- Tile one, made solid ---- + ; + ; Sixty four pixels of index one, FILLED rather than blitted, which is why this program + ; carries no picture of its own. + INIA 0d4 + OUTA 0xE3 + RSTA + OUTA 0xE4 + INIA 0x40 + OUTA 0xE5 ; Tile one begins at sixty four. + INIA 0x01 + OUTA 0xE2 ; Fill takes the byte it writes from SourceLow. + RSTA + OUTA 0xE6 + INIA 0x40 + OUTA 0xE7 ; Sixty four bytes of it. + INIA 0x02 + OUTA 0xE8 + + ; ---- And every cell of the other screen ---- + ; + ; One byte fills both halves of a cell, so this is tile one and attribute one everywhere: + ; the tile is index one at every pixel and the attribute adds sixteen to all of them, which + ; comes out as a screen solidly in scheme one's ink. + INIA 0d6 + OUTA 0xE3 + INIA 0x40 + OUTA 0xE4 + RSTA + OUTA 0xE5 + INIA 0x01 + OUTA 0xE2 + INIA 0x80 + OUTA 0xE6 + RSTA + OUTA 0xE7 ; The whole map, which is 0x8000 bytes. + INIA 0x02 + OUTA 0xE8 + + ; NOTHING HAS CHANGED ON THE SCREEN. Every byte of that went where nobody can see it, and + ; the line printed above is still sitting there to prove it - which is the reason this + ; waits here rather than flipping straight away. What a back buffer is for is not visible + ; in the flip; it is visible in the time before one. + CALL waitKey + + ; And this is the whole of showing it. + INIA 0x01 + OUTA 0x3C + + CALL waitKey + + ; ---- Which screen is showing is NOT put back here ---- + ; + ; On purpose, and it is the one thing in this program worth arguing about. The system + ; restores it at exit, the same way it restores the cursor and the ink, and for the same + ; reason: a program that FAULTED while flipped could not have put it back, and a shell that + ; only came out right for programs which remembered would be a shell that came out wrong + ; the day one crashed. What the person is looking at belongs to the system. + ; + ; The console mode below IS put back, because that is this program's own borrowing rather + ; than something the system hands out. + RSTA + OUTA 0x02 ; Line mode again. + SWI osExit + +; ---- A key, asked for rather than waited on ---- +; +; The console holds one until somebody wants it, so nothing pressed while the map was being +; filled is lost - it is sitting there and this returns immediately, which is right. A key +; pressed is a key meant for this program. +waitKey: + INA 0x01 + INIB 0x01 ; READY + AND + BRQ waitKey + INA 0x00 ; Taken, so the shell is not handed a key meant for this. + RET + +#Data + + #Base 0x3000 + +Message: +"A whole screen is about to be drawn where you cannot see it - this line will still\nbe here when it is done. Press a key to show it, and another to come back.\n" diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index 87c9061..39d1211 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -725,6 +725,7 @@ from every assembly file in it. Several are old programs written for the bare ma | Files | Writes a file, reads it back, renames it and deletes it, in 675 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. | | Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. | | Grid | The first program to use the screen as a screen. Redefines a tile above the font, fills all 128 map rows, and scrolls it diagonally a pixel at a time. | +| Flip | Draws a whole screen into the bank that is not being shown, waits, and then shows it in one byte out of one port. It deliberately does not put the displayed screen back, because that is the system's to restore - a program that faulted while flipped could not have. | | Edit | A line editor. | | Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. | | Type | Prints a named text file a block at a time, including one too large to fit in Data Memory. | @@ -1199,7 +1200,15 @@ before quietly answers to nothing. | 1 | Data Memory. The machine's. | | 2 | The bank table. The machine's. | | 3 | The disk's buffer, given at mount by `sbfsMount`. CosmOS needs it for as long as it is running. | -| 4 and up | Free for a program to use. | +| 4 | The screen's atlas - its tiles and palette - while `osTakeScreen` is saving or restoring. | +| 5 | The screen's map, for the same. | +| 6 and up | Free for a program to use. | + +4 and 5 are only registered while a screen is being saved or given back, so a program is free +to point them somewhere else in between - but a program that takes the screen will find them +pointing at the screen again afterwards, so there is nothing to be gained by it. `Grid` uses +them for exactly what CosmOS uses them for. `Flip` wants a third bank, the screen nobody is +looking at, and takes 6. `Grid` learned this the hard way and is the reason the table is here. It asked for 3, took the disk's buffer, and every read the filesystem made afterwards came out of video memory - so the diff --git a/Programs/CosmOS/Source/cosmos.asm b/Programs/CosmOS/Source/cosmos.asm index 162e079..520165b 100644 --- a/Programs/CosmOS/Source/cosmos.asm +++ b/Programs/CosmOS/Source/cosmos.asm @@ -5123,6 +5123,19 @@ screenSane: RSTA OUTA 0x37 OUTA 0x38 ; No fraction of a cell in either direction. + OUTA 0x3C ; And the screen everybody else's map is in. + + ; ---- Which of the two screens is being shown ---- + ; + ; A program that draws into the back buffer and flips is showing screen one, and the shell + ; knows nothing about that. Its own scrollback, its prompt and every line the person typed + ; are in screen NOUGHT, so a program that exited while flipped would hand back a shell + ; drawing correctly onto a screen nobody had ever written to - blank, and looking for all + ; the world like the machine had lost everything. + ; + ; Put back rather than left, by the same rule as the cursor and the ink in handleExit: a + ; program that stopped early is not around to put anything back, and the shell owns what + ; the person is looking at. ; ---- And glyphs and colours that can be read ---- ; @@ -5139,8 +5152,12 @@ screenSane: OUTA 0x39 RET -; Video memory as bank 4, which is what makes it reachable at all. Bank 3 is the disk's; see -; the table in the CosmOS README, which exists because a program once took 3. +; Video memory as banks 4 and 5, which is what makes it reachable at all. Bank 3 is the +; disk's; see the table in the CosmOS README, which exists because a program once took 3. +; +; The BACK BUFFER is not given a number here. The system saves and restores what is on the +; screen, and what is on the screen is screen nought - a program that wants the other one +; registers it itself, the same as any program wanting memory nobody else is using. screenBank: INIA 0d4 OUTA 0xE3 diff --git a/Source/Emulator/io.c b/Source/Emulator/io.c index 0d3559e..52e5554 100644 --- a/Source/Emulator/io.c +++ b/Source/Emulator/io.c @@ -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); diff --git a/Source/Emulator/video.c b/Source/Emulator/video.c index e2f78f2..e7276cf 100644 --- a/Source/Emulator/video.c +++ b/Source/Emulator/video.c @@ -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]; diff --git a/Source/Emulator/video.h b/Source/Emulator/video.h index c984b04..63533eb 100644 --- a/Source/Emulator/video.h +++ b/Source/Emulator/video.h @@ -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); diff --git a/SplitBit Programming Manual.md b/SplitBit Programming Manual.md index a373409..6afb0bf 100644 --- a/SplitBit Programming Manual.md +++ b/SplitBit Programming Manual.md @@ -628,6 +628,8 @@ Two rather than one because the two halves of a screen are written at completely | Screen | 0x4000 - 0xBFFF | The map. 128 rows of 256 bytes. | | Screen | 0x0000 - 0xF9FF | In bitmap mode, the picture instead: 64,000 bytes, one to a pixel. | +**There are two screen banks and one atlas**, laid out identically, and the device shows one screen at a time. See Two Screens below. + **The bitmap is the same memory as the map**, which is what shared video memory has always been. Going to bitmap mode does not clear the text screen - it stops calling it one, and coming back finds the map holding whatever the picture put there. **What a picture no longer costs is the font.** The tiles and the palette are in the other bank, where a bitmap cannot reach them, so a program can draw a picture and then put readable text back on the screen without asking the character generator for its glyphs again. While the two shared a bank, drawing anything destroyed them. @@ -643,9 +645,24 @@ A bank is registered by naming the port that owns it, so a device with two banks | Port | Owns | | --- | --- | | 0x30 | The atlas. | -| 0x3A | The screen. | +| 0x3A | Screen 0. | +| 0x3B | Screen 1. | -Nothing is read or written at 0x3A - it is a name for a bank, and the bus registry is where a program finds out it brings one. Asking the registry about the block gives an honest answer: those two ports say they bring memory and the other fourteen say they do not. +Nothing is read or written at 0x3A or 0x3B - they are names for banks, and the bus registry is where a program finds out they bring one. Asking the registry about the block gives an honest answer: those three ports say they bring memory and the other thirteen say they do not. + +### Two Screens: + +There are two screen banks and the device shows one of them. **Port 0x3C says which**, 0 or 1, and reads back what it was told; a screen that does not exist is not taken, the same as a mode that does not exist. + +That is a **back buffer**: a whole screen's worth of map written where nobody can see it, and then shown all at once. A screen drawn where it can be seen is seen half drawn, and 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 - which at a megahertz is long enough to look at. + +**One register is enough, where real hardware needed two.** The other said which screen the CPU's window pointed at. There is no window here: a program reaches a bank through the memory controller by its number, so writing to the screen that is not being shown is a matter of naming its bank, and the device 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. The machines this one imitates had to catch the few lines between one frame and the next to do the swap in. + +**The console draws into whichever screen is displayed**, rather than into one of its own. A game that flipped and then faulted needs the message to land where somebody can read it, and the console has no way of knowing that happened. + +What a second screen costs is a whole bank of somebody's memory, and nothing else: it is memory the device brought, so a program that wants it registers it and a program that does not never pays for it. `Programs/CosmOS/Apps/Flip.asm` is the shortest thing to read that uses one. The base port owns the atlas because tiles have been at 0x0000 since there was a screen at all. `Programs/Examples/picture.asm` registers both, and is the shortest thing to read that does. @@ -677,7 +694,9 @@ The high nibble is reserved and should be left at zero, so that a meaning can be | 0x37 | Fine X. How many pixels into that column the screen begins, 0 to 7. | | 0x38 | Fine Y. How many pixels into that row the screen begins, 0 to 7. | | 0x39 | Command. Bit 0 copies the font back, bit 1 the sixteen colour schemes. | -| 0x3A | Owns the screen bank. Not read or written. | +| 0x3A | Owns screen 0. Not read or written. | +| 0x3B | Owns screen 1. Not read or written. | +| 0x3C | Display. Which of the two screens is being shown. | | Mode | Screen | Cells | | --- | --- | --- | diff --git a/Tests/expected/cosmosCrossDisk.out b/Tests/expected/cosmosCrossDisk.out index 8a371e4..bf72de1 100644 --- a/Tests/expected/cosmosCrossDisk.out +++ b/Tests/expected/cosmosCrossDisk.out @@ -22,6 +22,7 @@ Break.sbx 149 Grid.sbx 571 Press.sbx 872 Mode.sbx 48 +Flip.sbx 291 Crash.sbx 632 vars.script 50 blocks.script 343 @@ -38,7 +39,7 @@ outer.script 376 inner.script 44 loop.script 35 crossed.txt 560 -25 files, 1 directory +26 files, 1 directory > exit halted Execution halted. diff --git a/Tests/expected/cosmosDrives.out b/Tests/expected/cosmosDrives.out index 7e81177..5d22b1d 100644 --- a/Tests/expected/cosmosDrives.out +++ b/Tests/expected/cosmosDrives.out @@ -12,6 +12,7 @@ Break.sbx 149 Grid.sbx 571 Press.sbx 872 Mode.sbx 48 +Flip.sbx 291 Crash.sbx 632 vars.script 50 blocks.script 343 @@ -27,7 +28,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -24 files, 1 directory +25 files, 1 directory > drive 1 > dir other.txt 28 diff --git a/Tests/expected/cosmosFault.out b/Tests/expected/cosmosFault.out index e263af5..4040d16 100644 --- a/Tests/expected/cosmosFault.out +++ b/Tests/expected/cosmosFault.out @@ -29,6 +29,7 @@ Break.sbx 149 Grid.sbx 571 Press.sbx 872 Mode.sbx 48 +Flip.sbx 291 Crash.sbx 632 vars.script 50 blocks.script 343 @@ -44,7 +45,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -24 files, 1 directory +25 files, 1 directory > exit halted Execution halted. diff --git a/Tests/expected/cosmosGrid.out b/Tests/expected/cosmosGrid.out index 548dd92..ca906bc 100644 --- a/Tests/expected/cosmosGrid.out +++ b/Tests/expected/cosmosGrid.out @@ -19,6 +19,7 @@ Break.sbx 149 Grid.sbx 571 Press.sbx 872 Mode.sbx 48 +Flip.sbx 291 Crash.sbx 632 vars.script 50 blocks.script 343 @@ -34,7 +35,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -24 files, 1 directory +25 files, 1 directory > exit halted Execution halted. diff --git a/Tests/expected/cosmosMonitor.out b/Tests/expected/cosmosMonitor.out index cda18db..7d27726 100644 --- a/Tests/expected/cosmosMonitor.out +++ b/Tests/expected/cosmosMonitor.out @@ -105,6 +105,7 @@ Break.sbx 149 Grid.sbx 571 Press.sbx 872 Mode.sbx 48 +Flip.sbx 291 Crash.sbx 632 vars.script 50 blocks.script 343 @@ -120,7 +121,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -24 files, 1 directory +25 files, 1 directory > exit halted Execution halted. diff --git a/Tests/expected/cosmosMonitorRun.out b/Tests/expected/cosmosMonitorRun.out index f162ae8..01734a2 100644 --- a/Tests/expected/cosmosMonitorRun.out +++ b/Tests/expected/cosmosMonitorRun.out @@ -24,6 +24,7 @@ Break.sbx 149 Grid.sbx 571 Press.sbx 872 Mode.sbx 48 +Flip.sbx 291 Crash.sbx 632 vars.script 50 blocks.script 343 @@ -39,7 +40,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -24 files, 1 directory +25 files, 1 directory > exit halted Execution halted. diff --git a/Tests/expected/cosmosRun.out b/Tests/expected/cosmosRun.out index a2f47d6..1d524c0 100644 --- a/Tests/expected/cosmosRun.out +++ b/Tests/expected/cosmosRun.out @@ -12,6 +12,7 @@ Break.sbx 149 Grid.sbx 571 Press.sbx 872 Mode.sbx 48 +Flip.sbx 291 Crash.sbx 632 vars.script 50 blocks.script 343 @@ -27,7 +28,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -24 files, 1 directory +25 files, 1 directory > load load what? > load nosuch.sbx diff --git a/Tests/expected/cosmosSlowDisk.out b/Tests/expected/cosmosSlowDisk.out index d32518d..a821e5e 100644 --- a/Tests/expected/cosmosSlowDisk.out +++ b/Tests/expected/cosmosSlowDisk.out @@ -10,6 +10,7 @@ Break.sbx 149 Grid.sbx 571 Press.sbx 872 Mode.sbx 48 +Flip.sbx 291 Crash.sbx 632 vars.script 50 blocks.script 343 @@ -25,7 +26,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -24 files, 1 directory +25 files, 1 directory > load Say.sbx loaded, starting at 5000 > run the disk took its time diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index d41ebbb..c439497 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -124,6 +124,12 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/ "$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ "$ROOT/Programs/CosmOS/Apps/Mode.asm" -o "$WORK/Mode.sbx" >/dev/null "$TOOL" put "$DISKS/cosmos.img" "$WORK/Mode.sbx" >/dev/null +# Flip.sbx draws a whole screen into the bank that is not being shown, waits, and then shows +# it. It is the only program here that leaves the machine displaying the OTHER screen, which +# is what makes it the thing that checks the system puts that back. +"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ + "$ROOT/Programs/CosmOS/Apps/Flip.asm" -o "$WORK/Flip.sbx" >/dev/null +"$TOOL" put "$DISKS/cosmos.img" "$WORK/Flip.sbx" >/dev/null # Crash.sbx breaks in each of the four ways the system now catches, which is the only way to # reach a fault screen from a recorded test. "$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ diff --git a/Tests/video.sh b/Tests/video.sh index 0276cd0..4934f61 100755 --- a/Tests/video.sh +++ b/Tests/video.sh @@ -63,6 +63,12 @@ start: OUTA 0xE2 INIA 0x03 OUTA 0xE8 ; And the screen - the map, or a bitmap - becomes bank 4 + INIA 0d5 + OUTA 0xE3 + INIA 0x3B + OUTA 0xE2 + INIA 0x03 + OUTA 0xE8 ; And the other screen, the one nobody is looking at, bank 5 ASM # ---- Said rather than assumed ---- # @@ -87,6 +93,7 @@ pokeTo() { pokeAtlas() { pokeTo 3 "$1" "$2"; } # Tiles and the palette. pokeScreen() { pokeTo 4 "$1" "$2"; } # The map, or a bitmap. +pokeBack() { pokeTo 5 "$1" "$2"; } # The same, in the screen not being shown. # Many bytes from one address, using the Data port's own stepping rather than naming the # address again for each. What a run of tile memory is for, and so far only the atlas needs @@ -322,6 +329,74 @@ echo "Checking what the video device draws." && result ok "the map is a ring" "row 0 follows row 127" \ || result no "the map is a ring" "got $(pixel ring 0 8)" +# ---- Two screens, and the flip between them ---- +# +# One red cell in each screen, in different rows: row one of the screen being shown, and the +# corner of the one that is not. BOTH HALVES ARE CHECKED, because either alone is weak - that +# the corner stayed empty would also be true of a bank that went nowhere, and that row one +# appeared would also be true of a device with one screen written twice. Together they say +# the two banks are different memory and only one of them is the screen. +{ prologue + pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0x00; pokeAtlas 0xFC06 0x00 + for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done + pokeScreen $((0x4000 + 256)) 0x01 + pokeBack 0x4000 0x01; pokeBack 0x4001 0x00 + epilogue +} | run backbuffer || exit 1 +[ "$(pixel backbuffer 0 8)" = "255,0,0" ] && [ "$(pixel backbuffer 0 0)" = "0,0,0" ] \ + && result ok "the back buffer is not the screen" "row one showed, the other screen did not" \ + || result no "the back buffer is not the screen" "row one $(pixel backbuffer 0 8), corner $(pixel backbuffer 0 0)" + +# The same program, and one more byte out of one more port. +{ prologue + pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0x00; pokeAtlas 0xFC06 0x00 + for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done + pokeBack 0x4000 0x01; pokeBack 0x4001 0x00 + port 0x3C 0x01 + epilogue +} | run flipped || exit 1 +[ "$(pixel flipped 0 0)" = "255,0,0" ] \ + && result ok "and the flip is what shows it" "one write to 0x3C, a whole new screen" \ + || result no "and the flip is what shows it" "got $(pixel flipped 0 0)" + +# And back again, which is the half that says the first screen was kept rather than copied +# over. A program that flips to draw and flips back must find what it left. +{ prologue + pokeAtlas 0xFC04 0xFF; pokeAtlas 0xFC05 0x00; pokeAtlas 0xFC06 0x00 + for i in $(seq 0 63); do pokeAtlas $((0x0040 + i)) 0x01; done + pokeScreen $((0x4000 + 256)) 0x01 + pokeBack 0x4000 0x01 + port 0x3C 0x01 + port 0x3C 0x00 + epilogue +} | run flippedback || exit 1 +[ "$(pixel flippedback 0 8)" = "255,0,0" ] && [ "$(pixel flippedback 0 0)" = "0,0,0" ] \ + && result ok "and flipping back finds what was there" "row one kept, the corner still empty" \ + || result no "and flipping back finds what was there" "corner $(pixel flippedback 0 0), row one $(pixel flippedback 0 8)" + +# Asked for rather than remembered, like every other register on this device. And a screen +# that does not exist is not taken, the same as a mode that does not exist. +{ prologue; port 0x3C 0x01; show 0x3C; port 0x3C 0x07; show 0x3C; epilogue +} | run whichscreen || exit 1 +[ "$(said whichscreen)" = "1 1" ] \ + && result ok "which screen is shown can be asked" "and screen seven was not taken" \ + || result no "which screen is shown can be asked" "got $(said whichscreen)" + +# ---- The console draws where the person is looking ---- +# +# Not into a screen of its own. A game that flipped and then faulted needs the message to +# land where somebody can read it, and the console has no way of knowing that happened. +{ prologue; port 0x3C 0x01; say "A"; epilogue; } | run textflipped || exit 1 +[ "$(pixel textflipped 2 1)" = "216,216,216" ] \ + && result ok "the console follows the flip" "the letter is on the screen being shown" \ + || result no "the console follows the flip" "got $(pixel textflipped 2 1)" + +# And it really went to the other one: flipping back finds the first screen as it was. +{ prologue; port 0x3C 0x01; say "A"; port 0x3C 0x00; epilogue; } | run textnotback || exit 1 +[ "$(pixel textnotback 2 1)" = "0,0,0" ] \ + && result ok "and wrote it in that screen only" "screen nought never saw the letter" \ + || result no "and wrote it in that screen only" "got $(pixel textnotback 2 1)" + # ---- Modes ---- { prologue; port 0x31 0x01; epilogue; } | run wide || exit 1 [ "$(size wide)" = "640 400" ] \ @@ -929,6 +1004,44 @@ LEFT="$(python3 "$ROOT/Tests/periodic.py" "$BUILD/noscratch.ppm" grid)" && result ok "and clears up when it cannot be kept" "no grid left on the screen" \ || result no "and clears up when it cannot be kept" "the grid is still there" +# ---- The back buffer, from inside the system ---- +# +# Flip draws a whole screen into the bank nobody is looking at, waits, shows it, waits, and +# puts it back. Caught here while it is showing: the map it filled is one tile and one +# attribute everywhere, so the picture is a SINGLE COLOUR and counting them says so without +# depending on which colour scheme one happens to be. +python3 -c "open('$BUILD/flip.keys','wb').write(b'Flip\n' + b'\x00'*3000 + b' ' + b'\x00'*9000)" +timeout 30 "$EMU" --fast --cycles 200000000 --keyboard "$BUILD/flip.keys" \ + --screen "$BUILD/flip.ppm" --disk "$ROOT/Tests/build/disks/cosmos.img" \ + --ram-disk 2048 "$BUILD/cosmos.bin" > "$BUILD/flip.out" 2>&1 || true +FLIPPED="$(python3 -c " +d = open('$BUILD/flip.ppm', 'rb').read() +px = d[d.index(b'255\n') + 4:] +print(len({px[o:o + 3] for o in range(0, len(px), 3)})) +" 2>/dev/null || echo 0)" +[ "$FLIPPED" = "1" ] \ + && result ok "a program shows the other screen" "the whole picture is the one it filled" \ + || result no "a program shows the other screen" "$FLIPPED colours, not a filled screen" + +# ---- And the system takes it back ---- +# +# The shell's scrollback, its prompt and every line the person typed are in screen NOUGHT. +# A program that exited while showing screen one would hand back a shell drawing perfectly +# onto a screen nobody had ever written to - blank, and looking for all the world like the +# machine had lost everything. Compared cell by cell against the same session without Flip +# in it, which is the same way Grid's restore is checked. +python3 -c "open('$BUILD/flipbefore.keys','wb').write(b'dir\n' + b'Say a line to come back to\n' + b'\x00'*200)" +python3 -c "open('$BUILD/flipafter.keys','wb').write(b'dir\n' + b'Say a line to come back to\n' + b'Flip\n' + b'\x00'*3000 + b' ' + b'\x00'*3000 + b' ' + b'\x00'*3000)" +for phase in flipbefore flipafter; do + timeout 30 "$EMU" --fast --cycles 200000000 --keyboard "$BUILD/$phase.keys" \ + --screen "$BUILD/$phase.ppm" --disk "$ROOT/Tests/build/disks/cosmos.img" \ + --ram-disk 2048 "$BUILD/cosmos.bin" > "$BUILD/$phase.out" 2>&1 || true +done +BACK="$(python3 "$ROOT/Tests/samescreen.py" "$BUILD/flipbefore.ppm" "$BUILD/flipafter.ppm")" +[ "$BACK" = "yes" ] \ + && result ok "and the system puts the screen back" "screen nought, with what was on it" \ + || result no "and the system puts the screen back" "$BACK" + # ---- Clearing puts the cursor back at the top ---- # # A screen with nothing on it and a cursor half way down it is not a cleared screen. This