CosmOS had 1,161 bytes of Program Memory left before the address applications load at, and Tab completion is not going to fit in that with anything to spare. So the wall moves up one page: the system keeps below 0x4FFF and 0x2FFF, and an application is based at 0x5000 and 0x3000. A PAGE IS A CHEAP THING TO GIVE IT AND AN EXPENSIVE THING TO RUN OUT OF. An application still has 44K of Program Memory before the vector table and the largest one here uses 7.5K, so what was taken from applications is space nothing has ever asked for - while what the system gained is the difference between building the next thing and counting bytes while building it. Not doubling, which was the version that would have cost application space worth minding. One page, and the same again when it is needed. Nothing in the machine knows where the wall is, so this is 34 #Base lines, one threshold in the fault handler, and the table in the CosmOS README that Tests/docs.sh reads its limits out of. The native assembler's scratch map had to move with it, and docs.sh said so before anything ran: its data reached 0x40D6 and its buffers began at 0x4000, so they were sitting on its variables. That file already carries a paragraph about the floor coming up and the map staying where it was. It has happened twice now, and been caught by a check the first time wrote. Twenty three recordings are the same runs a page higher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
456 lines
12 KiB
NASM
456 lines
12 KiB
NASM
; 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 0x5000
|
|
|
|
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.
|
|
;
|
|
; FOUR, BECAUSE THREE IS THE DISK'S. Bank numbers are one namespace for the whole machine
|
|
; and nothing hands them out: 0 is Program Memory, 1 is Data, 2 is the bank table, and
|
|
; CosmOS gives 3 to the disk's buffer when it mounts. This asked for 3, which does not
|
|
; fail - it succeeds, and the disk's buffer quietly becomes the screen. Every read the
|
|
; filesystem made after that came out of video memory, so the shell found an empty disk
|
|
; and could not start anything by name. Nothing said a word.
|
|
INIA 0d4
|
|
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.
|
|
|
|
; ---- Asking for the screen back afterwards ----
|
|
;
|
|
; Everything below overwrites a tile, all sixteen colour schemes and every cell of the map,
|
|
; and none of that is this program's to keep. The system puts it somewhere and gives it back
|
|
; at exit - and if it says it cannot, this carries on anyway, because it did before there
|
|
; was anywhere to put it.
|
|
SWI osTakeScreen
|
|
MVQA
|
|
SETD.0 ScreenKept
|
|
STA.0 ; Nought means the system will put it back.
|
|
|
|
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 ----
|
|
;
|
|
; A frame, then one pixel down and one across. This used to move a whole cell every fourth
|
|
; frame, because a cell was as fine as the screen could be moved - eight pixels at a time,
|
|
; which reads as the picture jumping rather than travelling.
|
|
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
|
|
|
|
; ---- A pixel down, and the cell it belongs to ----
|
|
;
|
|
; Fine is the low three bits of the register and does not carry, so this does: eight steps
|
|
; inside the cell and then one step of the origin. The AND is both the wrap and the test -
|
|
; Q coming out as nought is exactly the moment the cell boundary was crossed.
|
|
SETD.0 FineDown
|
|
LDA.0
|
|
INCA
|
|
INIB 0x07
|
|
AND
|
|
STQ.0
|
|
OUTQ 0x38
|
|
BNQ stepAcross
|
|
|
|
; The map is 128 rows against a screen of 25 or 50, so the origin walks a ring: what leaves
|
|
; the top has not gone anywhere and comes back round.
|
|
SETD.0 OriginDown
|
|
LDA.0
|
|
INCA
|
|
INIB 0x7F
|
|
AND
|
|
STQ.0
|
|
OUTQ 0x34
|
|
|
|
stepAcross:
|
|
; And the same sideways, which is the axis that did not exist at all until now. There are
|
|
; 128 columns against the 80 shown, so this ring is shallower but it is the same ring.
|
|
SETD.0 FineAcross
|
|
LDA.0
|
|
INCA
|
|
INIB 0x07
|
|
AND
|
|
STQ.0
|
|
OUTQ 0x37
|
|
BNQ everyFrame
|
|
|
|
SETD.0 OriginAcross
|
|
LDA.0
|
|
INCA
|
|
INIB 0x7F
|
|
AND
|
|
STQ.0
|
|
OUTQ 0x36
|
|
BRI everyFrame
|
|
|
|
finished:
|
|
; The key that stopped it, taken so the shell does not find it waiting.
|
|
INA 0x00
|
|
|
|
; ---- Putting the screen back ----
|
|
;
|
|
; ---- What used to be here ----
|
|
;
|
|
; Four scroll registers put back, the whole map filled with spaces, the cursor sent home,
|
|
; and palette bank 0 written out by hand - and it was STILL wrong, because the other fifteen
|
|
; banks kept this program's colours and there was nowhere to have put the real ones.
|
|
;
|
|
; All of it is osTakeScreen's now, and it gives back what was actually there rather than
|
|
; what a clean machine looks like.
|
|
;
|
|
; ---- Unless it said no ----
|
|
;
|
|
; A machine with no volatile drive has nowhere to keep a screen, and answers so. Being told
|
|
; no is not a fault and not a reason to stop: it means doing what this program did before
|
|
; there was anywhere to put one, which is leaving a blank screen rather than a grid with
|
|
; somebody's prompt printed into it.
|
|
SETD.0 ScreenKept
|
|
LDA.0
|
|
BRA gridScreenKept
|
|
|
|
RSTA
|
|
OUTA 0x34
|
|
OUTA 0x36 ; The origins, or the shell looks at a corner of the map.
|
|
|
|
INIA 0d4
|
|
OUTA 0xE3
|
|
INIA 0x40
|
|
OUTA 0xE4
|
|
RSTA
|
|
OUTA 0xE5
|
|
OUTA 0xE2 ; Tile nought is the space and attribute nought is plain.
|
|
INIA 0x80
|
|
OUTA 0xE6
|
|
RSTA
|
|
OUTA 0xE7
|
|
INIA 0x02
|
|
OUTA 0xE8 ; Fill the whole map with it.
|
|
|
|
; And the colours the machine wakes up in, which is as near as this can get to the ones it
|
|
; took: grey on black is what plain text has always been here.
|
|
INIA 0d4
|
|
OUTA 0xE3
|
|
INIA 0xFC
|
|
OUTA 0xE4
|
|
RSTA
|
|
OUTA 0xE5
|
|
OUTA 0xE9
|
|
OUTA 0xE9
|
|
OUTA 0xE9
|
|
OUTA 0xE9
|
|
INIA 0xD8
|
|
OUTA 0xE9
|
|
OUTA 0xE9
|
|
OUTA 0xE9
|
|
RSTA
|
|
OUTA 0xE9
|
|
|
|
INIA 0x01
|
|
OUTA 0x05 ; Cleared, which also puts the cursor home.
|
|
|
|
gridScreenKept:
|
|
RSTA
|
|
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 0d4
|
|
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 0d4
|
|
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 0d4
|
|
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
|
|
|
|
; ---- The map's width, which is not the screen's ----
|
|
;
|
|
; A hundred and twenty-eight, because that is how many cells a map row holds: 256 bytes at
|
|
; two bytes a cell, whatever mode the screen is in. It is a property of video memory rather
|
|
; than of the display, so there is no register to ask and nothing to ask it of.
|
|
;
|
|
; This said forty first, and filled half of an eighty column screen. Then it asked the
|
|
; screen how wide it was, which fixed what could be seen and was still wrong: scrolling
|
|
; sideways walked off the 80 filled columns into the 48 that were not, and the grid went
|
|
; blank for six seconds before coming round again.
|
|
;
|
|
; ASKING THE SCREEN IS RIGHT FOR FILLING A SCREEN AND WRONG FOR FILLING A MAP. A program
|
|
; that writes one screenful wants the window; a program that scrolls wants everything the
|
|
; window can be moved over.
|
|
INIA 0d128
|
|
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 0x3000
|
|
|
|
ScreenKept:
|
|
0x00
|
|
FineDown:
|
|
0x00
|
|
FineAcross:
|
|
0x00
|
|
OriginDown:
|
|
0x00
|
|
OriginAcross:
|
|
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
|