diff --git a/Programs/CosmOS/Apps/Grid.asm b/Programs/CosmOS/Apps/Grid.asm new file mode 100644 index 0000000..217f9a2 --- /dev/null +++ b/Programs/CosmOS/Apps/Grid.asm @@ -0,0 +1,395 @@ +; Grid, the first program to use the screen as a screen. +; +; Everything drawn on this machine so far has been text or a bitmap. The tile engine has +; been there since the screen was built and nothing had touched it: the console uses it, +; but only ever to put a letter in a cell, which is the one thing it can do that a plain +; character display could do too. +; +; This redefines a tile, fills a map bigger than the screen with it, and then scrolls that +; map by writing ONE BYTE A FRAME. No memory moves. The rows above and below the screen are +; already there, so what a scroll costs is not the 2,000 bytes of a screenful but the one +; byte that says which row is on top. +; +; ---- Where its tiles live ---- +; +; At 200, and the font is why. The machine wakes with the font in tile memory - glyph n at +; tile n, for 135 of the 256 - so a program that starts writing tiles at zero paints over +; the alphabet and the shell it is going to hand the machine back to. Above 135 is empty and +; nobody else's, so nothing here has to be put back afterwards except the map. +; +; Written by Anachronaut + +#Include services.asm + +#Program + + #Base 0x4000 + +start: + ; ---- Reaching video memory ---- + ; + ; The CPU cannot touch it. It belongs to the device, and the only way in is to give it a + ; bank number and go through the memory controller - the same as the disk's buffer. + INIA 0d3 + OUTA 0xE3 ; DestBank: the number it will answer to. + INIA 0x30 + OUTA 0xE2 ; SourceLow: the port of the device that owns it. + INIA 0x03 + OUTA 0xE8 ; RegisterBank. + + CALL putTile + CALL putPalette + CALL putMap + + ; Key mode, so that a key arrives when it is pressed rather than when Return is. Put back + ; before this returns, and CosmOS puts it back too if a program forgets. + INIA 0x01 + OUTA 0x02 + + ; ---- The loop ---- + ; + ; Wait for the screen to finish a frame, count four of them, move the origin down one row. + ; That is fifteen rows a second, which is slow enough to watch and fast enough to look + ; deliberate. + RSTA + SETD.0 Ticks + STA.0 + +everyFrame: + CALL waitFrame + + ; Anything typed ends it. Asked for, never waited for: the console holds the key until + ; somebody wants it, so nothing pressed between frames is lost. + INA 0x01 + INIB 0x01 ; READY + AND + BNQ finished + + SETD.0 Ticks + LDA.0 + INCA + STA.0 + INIB 0d4 + CCF + SUB + BNQ everyFrame + + RSTA + STA.0 ; DP0 is still Ticks, from the count just above. + + ; ---- The scroll itself ---- + ; + ; One byte. The map is 128 rows and the screen shows 25 of them, so this walks the origin + ; through a ring: what leaves the top has not gone anywhere and comes back round. + SETD.0 Origin + LDA.0 + INCA + INIB 0x7F + AND + MVQA + STA.0 + OUTA 0x34 + BRI everyFrame + +finished: + ; The key that stopped it, taken so the shell does not find it waiting. + INA 0x00 + + ; ---- Putting the screen back ---- + ; + ; The origin first, then every cell of the map and not just the visible ones. A console + ; that scrolls would otherwise walk down into rows this program filled, and find a grid + ; underneath its own output. + RSTA + OUTA 0x34 + + INIA 0d3 + OUTA 0xE3 + INIA 0x40 + OUTA 0xE4 + RSTA + OUTA 0xE5 + OUTA 0xE2 ; The byte to write: tile 0 is the space, attribute 0 is plain. + INIA 0x80 + OUTA 0xE6 + RSTA + OUTA 0xE7 ; 0x8000 bytes, which is the whole map. + INIA 0x02 + OUTA 0xE8 ; Fill. + + ; ---- And the colours it woke up with ---- + ; + ; Only the first pair, and that is worth being honest about. The console's own scheme is + ; sixteen banks - colours on black in 0 to 7, the same colours inverted in 8 to 15 - and + ; this program wrote over all of them, because the attribute nibble lands on exactly the + ; entries the console uses and there is nowhere else for it to land. Putting all thirty two + ; back would mean copying the device's own table into an application, which is the kind of + ; duplication that goes stale the first time somebody picks a nicer green. + ; + ; So it restores bank 0, grey on black, which is what plain text has always been and what + ; the shell will be using when it gets the machine back. The other fifteen keep this + ; program's colours until something else writes them, which is visible only to a program + ; that sets the console's attribute port. + ; + ; THE REAL ANSWER IS A COMMAND TO THE SCREEN saying "give me back what you woke up with", + ; the same way the console has one for clearing. There is not one, and this program is the + ; first thing that ever wanted it. + INIA 0d3 + OUTA 0xE3 + INIA 0xFC + OUTA 0xE4 + RSTA + OUTA 0xE5 + OUTA 0xE9 ; Entry 0, the paper: black. + OUTA 0xE9 + OUTA 0xE9 + OUTA 0xE9 + INIA 0xD8 + OUTA 0xE9 ; Entry 1, the ink: grey. + OUTA 0xE9 + OUTA 0xE9 + RSTA + OUTA 0xE9 + + OUTA 0x02 ; Line mode, the way it was found. + RSTA ; splitlint[redundant-assignment]: an exit status, not a mode + SWI osExit + +; ---- A frame ---- +; +; Asked for rather than waited on with an interrupt. Polling costs a program nothing it +; needs here and saves installing a vector, and the status bit is honest: reading it is what +; answers it, so this cannot see the same frame twice. +waitFrame: + INA 0x30 + INIB 0x01 + AND + BRQ waitFrame + RET + +; ---- The tile ---- +; +; Blitted rather than written a byte at a time, because it is already sixty four bytes of +; Data Segment and the controller will move it in one command. Tile n starts at n times 64, +; so tile 200 starts at 12,800, which is 0x3200. +putTile: + INIA 0x01 + OUTA 0xE0 ; SourceBank: Data Memory. + ; The pointer BEFORE storing through it. Written the other way round the first time, which + ; assembles perfectly and stores the address into wherever DP1 was last left - so the blit + ; read its sixty four bytes from nowhere in particular and tile 200 came out as noise. + SETD.1 TileArtAt + SETD.0 TileArt + STD.0.1 + LDA.1 + OUTA 0xE1 + INCD.1 + LDA.1 + OUTA 0xE2 + INIA 0d3 + OUTA 0xE3 + INIA 0x32 + OUTA 0xE4 + RSTA + OUTA 0xE5 + OUTA 0xE6 + INIA 0d64 + OUTA 0xE7 + INIA 0x01 + OUTA 0xE8 ; Blit. + RET + +; ---- The colours ---- +; +; The tile is drawn in indices 0 and 1, and the low nibble of a cell's attribute is ADDED to +; every index in it, sixteen at a time. So the same sixty four bytes appear in sixteen colour +; schemes, and what this writes is those schemes: entry 16n is the ground and 16n+1 the line. +; Nothing is duplicated to get them. +; +; ---- Nothing here works out an address ---- +; +; The first try computed where entry 16n lives - 0xFC00 plus 64n - and was wrong twice for +; the same reason, which is that this machine cannot multiply and pretending otherwise is +; where the bugs go. 64n reaches 960, so the address spans four pages and the high byte moves +; too; and doubling A by adding B to it needs B to hold A first, which RSTB is the opposite +; of. +; +; So it writes all 256 entries in order and never computes anything. The controller's Data +; port steps the address on after every byte, so the whole palette is one sweep of 1,024 +; writes with no arithmetic in it at all. The colours that change do so by adding sixteen to +; a running value, which is the same reason. +putPalette: + INIA 0d3 + OUTA 0xE3 + INIA 0xFC + OUTA 0xE4 + RSTA + OUTA 0xE5 + + SETD.0 Scheme + STA.0 + SETD.0 LineRed + STA.0 + INIA 0xFF + SETD.0 LineGreen + STA.0 + +everyScheme: + ; Entry 16n, the ground: the same dark under every scheme. + INIA 0d16 + OUTA 0xE9 + OUTA 0xE9 + INIA 0d24 + OUTA 0xE9 + RSTA + OUTA 0xE9 + + ; Entry 16n+1, the line: more red and less green the further down the map it is, so that + ; scrolling is visibly going somewhere rather than showing the same row again. + SETD.0 LineRed + LDA.0 + OUTA 0xE9 + SETD.0 LineGreen + LDA.0 + OUTA 0xE9 + INIA 0d96 + OUTA 0xE9 + RSTA + OUTA 0xE9 + + ; The fourteen this tile never asks for. Written anyway, because the sweep is what keeps + ; the address right and skipping them would mean working one out. + INIA 0d14 + SETD.0 Spare + STA.0 +everySpare: + RSTA + OUTA 0xE9 + OUTA 0xE9 + OUTA 0xE9 + OUTA 0xE9 + SETD.0 Spare + LDA.0 + DECA + STA.0 + BNA everySpare + + SETD.0 LineRed + LDA.0 + INIB 0d16 + CCF + ADD + STQ.0 + SETD.0 LineGreen + LDA.0 + CCF + SUB ; B is still sixteen, from the red just above. + STQ.0 + + SETD.0 Scheme + LDA.0 + INCA + STA.0 + CCF + SUB ; And still sixteen here, which is also how many schemes there are. + BNQ everyScheme + RET + +; ---- The map ---- +; +; All 128 rows, not the 25 the screen shows. That is the whole point of a map bigger than +; the screen: the rows above and below are already drawn, so scrolling is a change of origin +; rather than a change of anything. +; +; A ROW IS A PAGE, which is why the row number is written straight into DestHigh and the +; column arithmetic disappears. The controller's Data port steps the address on after every +; byte, so a row is a loop over two writes with no address handling in it at all. +putMap: + RSTA + SETD.0 MapRow + STA.0 + +everyRow: + INIA 0d3 + OUTA 0xE3 + SETD.0 MapRow + LDA.0 + INIB 0x40 + CCF + ADD + OUTQ 0xE4 ; The map starts at 0x4000 and a row is a page. + RSTA + OUTA 0xE5 + + ; The attribute is the row number's low nibble, so the schemes band down the map and + ; repeat every sixteen rows. DP0 is still MapRow, from the row's address above. + LDA.0 + INIB 0x0F + AND + SETD.0 RowAttribute + STQ.0 + + INIA 0d40 + SETD.0 RowCells + STA.0 + +everyCell: + INIA 0d200 + OUTA 0xE9 + SETD.0 RowAttribute + LDA.0 + OUTA 0xE9 + SETD.0 RowCells + LDA.0 + DECA + STA.0 + BNA everyCell + + SETD.0 MapRow + LDA.0 + INCA + STA.0 + INIB 0x80 + CCF + SUB + BNQ everyRow + RET + +#Data + + #Base 0x2000 + +Ticks: + 0x00 +Origin: + 0x00 +Scheme: + 0x00 +Spare: + 0x00 +LineRed: + 0x00 +LineGreen: + 0x00 +MapRow: + 0x00 +RowAttribute: + 0x00 +RowCells: + 0x00 +TileArtAt: + 0x00 0x00 + +; ---- Eight by eight, a byte a pixel ---- +; +; A line along the top and one down the left. Tiled edge to edge they meet, so a screenful +; of this one tile is a continuous grid rather than 1,000 separate boxes. +TileArt: + 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 + 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 + 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 + 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 + 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 + 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 + 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 + 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index 1b7332a..38036f2 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -531,6 +531,7 @@ from every assembly file in it. Several are old programs written for the bare ma | Settle | Says how the last start went and tells the machine to stop falling back, in 353 bytes. A program rather than a shell word, because the shell is for what cannot be done without it. | | 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 by writing one byte a frame. | | 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. | @@ -679,6 +680,31 @@ none, and a clean install having nothing to configure is the right default. outside with nothing on the disk consulted. That is what a debugger does, and it is what to use when the thing being debugged is the boot chain, since it skips the boot chain. +### Grid, and what a tile engine costs: + +Everything else drawn on this machine has been text or a bitmap. `Grid` is the first program +to use the tile engine as an engine, and it is worth reading for the size of the numbers. + +It writes **one byte a frame** to scroll. The map is 128 rows and the screen shows 25, so the +rows above and below are already drawn - scrolling moves the origin rather than 2,000 bytes of +screen, and the rows that leave the top have not gone anywhere. A screenful of movement costs +one `OUTA`. + +It puts its tile at **200**, because the machine wakes with the font in tile memory - glyph n +at tile n, for 135 of the 256 - so a program that starts at zero paints over the alphabet and +the shell it is about to hand the machine back to. Above 135 is empty and nobody else's. + +Its sixteen colour schemes are **one tile**, not sixteen. A cell's attribute nibble is added +to every palette index in it, sixteen at a time, so the same 64 bytes come out in sixteen +colourings and the map bands down the screen as it scrolls. + +**What it cannot give back is the palette.** The console's colours are sixteen banks at +exactly the entries the attribute nibble lands on, so any program using the nibble overwrites +them and there is nowhere else for it to write. `Grid` restores bank 0 - grey on black, what +plain text has always been - and leaves the other fifteen as it made them. The proper answer +is a command to the screen meaning "give me back what you woke up with", the way the console +has one for clearing. There is not one yet, and this is the first program that ever wanted it. + ## The Application Model: CosmOS divides the two SplitBit address spaces by convention: diff --git a/Programs/startup.sh b/Programs/startup.sh new file mode 100644 index 0000000..635aad7 --- /dev/null +++ b/Programs/startup.sh @@ -0,0 +1,5 @@ +#! shell +#quiet +clear +echo Welcome to CosmOS. +echo Ready. diff --git a/Tests/expected/cosmosRun.out b/Tests/expected/cosmosRun.out index 4eaf744..cddcfeb 100644 --- a/Tests/expected/cosmosRun.out +++ b/Tests/expected/cosmosRun.out @@ -7,6 +7,7 @@ Snake.sbx 2164 Keys.sbx 664 Say.sbx 156 Break.sbx 149 +Grid.sbx 510 notes.txt 21 hi.script 121 bad.script 45 @@ -16,7 +17,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -16 files +17 files > load what? > no such file > not a program diff --git a/Tests/expected/cosmosSlowDisk.out b/Tests/expected/cosmosSlowDisk.out index cf76281..3ad53d8 100644 --- a/Tests/expected/cosmosSlowDisk.out +++ b/Tests/expected/cosmosSlowDisk.out @@ -6,6 +6,7 @@ Snake.sbx 2164 Keys.sbx 664 Say.sbx 156 Break.sbx 149 +Grid.sbx 510 notes.txt 21 hi.script 121 bad.script 45 @@ -15,7 +16,7 @@ nonl.script 38 outer.script 376 inner.script 44 loop.script 35 -16 files +17 files > loaded, starting at 4000 > it says: the disk took its time finished diff --git a/Tests/makedisks.sh b/Tests/makedisks.sh index 7a04107..4a49ead 100755 --- a/Tests/makedisks.sh +++ b/Tests/makedisks.sh @@ -106,6 +106,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/Break.asm" -o "$WORK/Break.sbx" >/dev/null "$TOOL" put "$DISKS/cosmos.img" "$WORK/Break.sbx" >/dev/null +# Grid.sbx is the first program that uses the screen as a screen rather than as somewhere to +# put letters: it redefines a tile, fills a map bigger than the display, and scrolls it by +# moving the origin. Tests/video.sh boots this disk and looks at the pixels. +"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \ + "$ROOT/Programs/CosmOS/Apps/Grid.asm" -o "$WORK/Grid.sbx" >/dev/null +"$TOOL" put "$DISKS/cosmos.img" "$WORK/Grid.sbx" >/dev/null printf 'this is not a program' > notes.txt "$TOOL" put "$DISKS/cosmos.img" notes.txt >/dev/null diff --git a/Tests/video.sh b/Tests/video.sh index 67c8384..0fda325 100755 --- a/Tests/video.sh +++ b/Tests/video.sh @@ -718,6 +718,42 @@ TOTAL="$(grep -oE 'after [0-9]+' "$BUILD/poller.out" | grep -oE '[0-9]+')" && result ok "and the flag comes down when looked at" "$TOTAL cycles, so three frames passed" \ || result no "and the flag comes down when looked at" "$TOTAL cycles, too few to be three frames" +# ---- The tile engine, driven by a program rather than by the console ---- +# +# Everything above drives the screen from a bare test program. This boots the whole system +# and runs Grid.sbx on it, because Grid is the first thing that uses the engine as an engine: +# it redefines a tile above the font, fills all 128 map rows, and scrolls by moving the +# origin. What is checked is what came out of the renderer, not what the program believed. +# +# The keyboard file is what makes it possible to catch it MID-SCROLL: "Grid" and a return, +# then a long silence, so the machine is still running when the cycle limit stops it and the +# picture is taken. +"$ASM" -I "$ROOT/Programs/CosmOS/Source" "$ROOT/Programs/CosmOS/Source/cosmos.asm" \ + -o "$BUILD/cosmos.bin" > "$BUILD/cosmos.log" 2>&1 +python3 -c "open('$BUILD/grid.keys','wb').write(b'Grid\n' + b'\x00'*4000)" +timeout 30 "$EMU" --fast --cycles 8000000 --keyboard "$BUILD/grid.keys" \ + --screen "$BUILD/grid.ppm" --disk "$ROOT/Tests/build/disks/cosmos.img" \ + "$BUILD/cosmos.bin" > "$BUILD/grid.out" 2>&1 || true + +if [ -f "$BUILD/grid.ppm" ]; then + # A cell is eight by eight with a line along its top and down its left, so within one + # cell the corner is line and the middle is ground - and the cell to the right starts + # with a line again. That is a grid rather than a wash of colour. + corner="$(pixel grid 0 0)"; middle="$(pixel grid 4 4)"; nextcell="$(pixel grid 8 4)" + [ "$corner" != "$middle" ] && [ "$nextcell" = "$corner" ] \ + && result ok "a program drew a grid of its own tile" "line $corner, ground $middle" \ + || result no "a program drew a grid of its own tile" "corner $corner middle $middle next $nextcell" + + # The attribute nibble adds sixteen to every index in the tile, so consecutive map rows + # come out in consecutive schemes. Two cell rows apart must not be the same colour. + one="$(pixel grid 0 0)"; two="$(pixel grid 0 8)" + [ "$one" != "$two" ] \ + && result ok "the attribute nibble recolours it" "row 0 $one, row 1 $two" \ + || result no "the attribute nibble recolours it" "both rows are $one" +else + result no "a program drew a grid of its own tile" "no picture came out" +fi + # ---- 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